From 62df918a4919c65f5277dd360647dd2b200d7023 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 16 Apr 2026 23:35:06 +0700 Subject: [PATCH 001/244] bug fix: constrain cuda.core test installs to the requested CTK minor (#1921) * test: pin core toolkit installs to the requested CTK minor Keep the core test environment aligned with the matrix-selected CUDA toolkit so pip does not resolve a newer runtime and invalidate the job coverage. Made-with: Cursor * test: simplify core wheel install selection Centralize the core wheel target selection so both install modes share one pip invocation while preserving when the published cuda.bindings extra is requested. Made-with: Cursor --- ci/tools/run-tests | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ci/tools/run-tests b/ci/tools/run-tests index 891a20905c9..d42634a7073 100755 --- a/ci/tools/run-tests +++ b/ci/tools/run-tests @@ -74,15 +74,15 @@ elif [[ "${test_module}" == "core" ]]; then pushd ./cuda_core CUDA_VER_MINOR="$(cut -d '.' -f 1-2 <<< "${CUDA_VER}")" - if [[ "${LOCAL_CTK}" == 1 ]]; then - # We already installed cuda-bindings, and all CTK components exist locally, - # so just install the test dependencies. - # Constrain cuda-toolkit to match the local CTK version to avoid - # pip pulling in a newer nvidia-cuda-runtime that conflicts with it. - pip install "${CUDA_CORE_ARTIFACTS_DIR}"/*.whl --group "test-cu${TEST_CUDA_MAJOR}${FREE_THREADING}" "cuda-toolkit==${CUDA_VER_MINOR}.*" - else - pip install $(ls "${CUDA_CORE_ARTIFACTS_DIR}"/*.whl)["cu${TEST_CUDA_MAJOR}"] --group "test-cu${TEST_CUDA_MAJOR}${FREE_THREADING}" + # Start from the built wheel path, then add the published cuda.bindings extra + # when this job is resolving against wheel-installed CTK packages. + WHL_EXTRA=("${CUDA_CORE_ARTIFACTS_DIR}"/*.whl) + if [[ "${LOCAL_CTK}" != 1 ]]; then + WHL_EXTRA=("${WHL_EXTRA[0]}[cu${TEST_CUDA_MAJOR}]") fi + # Constrain cuda-toolkit to the requested CTK version to avoid + # pip pulling in a newer nvidia-cuda-runtime that conflicts with it. + pip install "${WHL_EXTRA[@]}" --group "test-cu${TEST_CUDA_MAJOR}${FREE_THREADING}" "cuda-toolkit==${CUDA_VER_MINOR}.*" echo "Running core tests" ${SANITIZER_CMD} pytest -rxXs -v --durations=0 --randomly-dont-reorganize tests/ # Currently our CI always installs the latest bindings (from either major version). From cb3c13262d85c3ff80e9d5c891837cd6a84909e3 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:08:14 -0400 Subject: [PATCH 002/244] Fix is_managed reporting for pool-allocated managed memory (#1924) * Fix is_managed reporting for pool-allocated managed memory Pool-allocated managed memory via cuMemAllocFromPoolAsync (from a pool created with CU_MEM_ALLOCATION_TYPE_MANAGED) does not set CU_POINTER_ATTRIBUTE_IS_MANAGED=1. _query_memory_attrs therefore classified the allocation as pinned host memory, causing classify_dl_device to return kDLCUDAHost instead of kDLCUDAManaged. CCCL's make_tma_descriptor only accepts kDLCUDA or kDLCUDAManaged, so as_tensor_map() failed with "Device type must be kDLCUDA or kDLCUDAManaged" on managed buffers. Buffer.is_device_accessible / is_host_accessible already delegate to the memory resource when one is attached. Apply the same pattern to is_managed, and expose is_managed on the MemoryResource base (defaulting to False) with ManagedMemoryResource overriding it to True. Also ignore .claude/settings.local.json in .gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) * test(memory): add direct is_managed coverage for ManagedMemoryResource The existing test_managed_buffer_dlpack_roundtrip_device_type uses a DummyUnifiedMemoryResource backed by cuMemAllocManaged, which sets CU_POINTER_ATTRIBUTE_IS_MANAGED and so never exercised the pool-allocated path that surfaced the bug. Add two targeted tests: - test_managed_memory_resource_buffer_dlpack_device_type: allocates from ManagedMemoryResource (cuMemAllocFromPoolAsync on a managed pool) and asserts is_managed and kDLCUDAManaged through Buffer and view. - test_non_managed_resources_report_not_managed: parametrized smoke test ensuring DeviceMemoryResource and PinnedMemoryResource still report is_managed=False so the new MemoryResource.is_managed default does not silently misclassify non-managed resources. Co-Authored-By: Claude Opus 4.6 (1M context) * Fix Buffer.is_managed to merge driver and resource signals Previous fix unconditionally delegated Buffer.is_managed to _memory_resource.is_managed, which returns False for any MemoryResource subclass that does not opt in. That broke DummyUnifiedMemoryResource (and any user-defined MR wrapping cuMemAllocManaged) where the driver pointer attribute correctly reports IS_MANAGED=1 but the resource does not override is_managed. Query the driver first; only fall back to the memory resource when the driver does not report IS_MANAGED (the pool-allocated managed memory path). This keeps both old-style cuMemAllocManaged buffers and ManagedMemoryResource pool allocations correctly classified. Also rework the regression test parametrization to skip the pinned case when PinnedMemoryResource is unavailable (CUDA < 13.0), and pick up the ruff-format reflow of the helper call site. Co-Authored-By: Claude Opus 4.6 (1M context) * chore: refresh pixi.lock for upstream package updates Pick up cuda-nvrtc 13.2.78, libcufile 1.17.1.22, and other transitive package updates from conda-forge. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .gitignore | 1 + cuda_bindings/pixi.lock | 82 +++- cuda_core/cuda/core/_memory/_buffer.pyx | 11 +- .../core/_memory/_managed_memory_resource.pyx | 5 + cuda_core/pixi.lock | 366 ++++++++++-------- cuda_core/tests/test_memory.py | 41 ++ 6 files changed, 331 insertions(+), 175 deletions(-) diff --git a/.gitignore b/.gitignore index 9bead862d8f..da6748ddbe3 100644 --- a/.gitignore +++ b/.gitignore @@ -196,3 +196,4 @@ cython_debug/ # Cursor .cursorrules +.claude/settings.local.json diff --git a/cuda_bindings/pixi.lock b/cuda_bindings/pixi.lock index 5224450b6e3..c0a897611c7 100644 --- a/cuda_bindings/pixi.lock +++ b/cuda_bindings/pixi.lock @@ -567,7 +567,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-static-13.2.51-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-64-13.2.51-h376f20c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.51-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-13.2.51-h69a702a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.2.51-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-13.2.51-h4bc722e_0.conda @@ -612,7 +612,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.77-h3ff7636_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.0.44-h85c024f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda @@ -763,7 +763,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-static-13.2.51-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-aarch64-13.2.51-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.51-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.78-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-13.2.51-he9431aa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-13.2.51-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-impl-13.2.51-h7b14b0b_0.conda @@ -805,7 +805,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.11.0-5_haddc8a3_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.77-h68e9139_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.11.0-5_hd72aa62_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.0.44-h4243460_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.1.22-h4243460_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda @@ -946,7 +946,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-cudart-static-13.2.51-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_win-64-13.2.51-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_win-64-13.2.51-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.51-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.78-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-13.2.51-h719f0c7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.2.51-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.2.51-h2466b09_0.conda @@ -1468,7 +1468,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-cudart-static-13.2.51-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_win-64-13.2.51-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_win-64-13.2.51-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.51-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.78-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-13.2.51-h719f0c7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.2.51-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.2.51-h2466b09_0.conda @@ -3025,7 +3025,7 @@ packages: - cuda-pathfinder - libnvjitlink - cuda-nvrtc - - cuda-nvrtc >=13.2.51,<14.0a0 + - cuda-nvrtc >=13.2.78,<14.0a0 - cuda-nvvm - libnvfatbin - vc >=14.3,<15 @@ -3079,11 +3079,11 @@ packages: - cuda-pathfinder - libnvjitlink - cuda-nvrtc - - cuda-nvrtc >=13.2.51,<14.0a0 + - cuda-nvrtc >=13.2.78,<14.0a0 - cuda-nvvm - libnvfatbin - libcufile - - libcufile >=1.17.0.44,<2.0a0 + - libcufile >=1.17.1.22,<2.0a0 - libgcc >=15 - libgcc >=15 - libstdcxx >=15 @@ -3135,11 +3135,11 @@ packages: - cuda-pathfinder - libnvjitlink - cuda-nvrtc - - cuda-nvrtc >=13.2.51,<14.0a0 + - cuda-nvrtc >=13.2.78,<14.0a0 - cuda-nvvm - libnvfatbin - libcufile - - libcufile >=1.17.0.44,<2.0a0 + - libcufile >=1.17.1.22,<2.0a0 - libgcc >=15 - libgcc >=15 - libstdcxx >=15 @@ -3763,6 +3763,17 @@ packages: purls: [] size: 35736655 timestamp: 1773100338749 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda + sha256: 73fbc9d15c062c3ea60891e8183002f6b055fa6638402d17581677af0aaa20d8 + md5: 66623d882c42506fa3f1780b90841400 + depends: + - __glibc >=2.17,<3.0.a0 + - cuda-version >=13.2,<13.3.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 35670504 + timestamp: 1776109867257 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-12.9.86-h8f3c8d4_1.conda sha256: e7f8d835d7bf993dcad9fba6db5af89c35b2b4f0282799b729bf6ad2c3bd896d md5: 48187c09673a42f9930764e8170b8787 @@ -3786,6 +3797,17 @@ packages: purls: [] size: 33927374 timestamp: 1773100385281 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.78-h8f3c8d4_0.conda + sha256: dcc9a9890d04850ffecc59748d546dcde9c80d3040b726cf3839b8191c6d3548 + md5: b6632980124d529a2b87f68831c58743 + depends: + - arm-variant * sbsa + - cuda-version >=13.2,<13.3.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 33929238 + timestamp: 1776109887303 - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-12.9.86-hac47afa_1.conda sha256: d90ef446ac859db26286a5d39d39333c4e4cee31ba5042b5c7922bd25de531f6 md5: d68b5d96a53c80dc3dbbd8f7c3b8106d @@ -3809,6 +3831,17 @@ packages: purls: [] size: 31221551 timestamp: 1773100427009 +- conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.78-hac47afa_0.conda + sha256: 5e2b53023ceafae1f7f4d83e83ebf175befb04f50fa131ba7c78d0cc6b299477 + md5: 3c8fa05aa0fe9d3cb4638e8bd7bb84e3 + depends: + - cuda-version >=13.2,<13.3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 31226924 + timestamp: 1776110029365 - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-12.9.86-h69a702a_6.conda sha256: 5c70d91e6d30eb6000ea036558a20fd0b1bc13cdd2c04fd5dbf94d9caa0a7fbc md5: 704956f67e44ddf046565ead01f9efcd @@ -7056,6 +7089,18 @@ packages: purls: [] size: 1085341 timestamp: 1773100191342 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda + sha256: a24ad0ca488aa3e237049cd5b5c6d7fe3d2d4330682ed329203064e332ea1d74 + md5: 056a67706108efd1f9c24682ba8d3685 + depends: + - __glibc >=2.28,<3.0.a0 + - cuda-version >=13.2,<13.3.0a0 + - libgcc >=14 + - libstdcxx >=14 + - rdma-core >=61.0 + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 1082447 + timestamp: 1776110053053 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.14.1.1-had8bf56_1.conda sha256: fbc1fa6b3ddf946b2999c9820310682739505df71e1e2ac513a72efb951fa3e5 md5: ee136db5a5409dddc78eaf7658fccffe @@ -7085,6 +7130,21 @@ packages: purls: [] size: 973639 timestamp: 1773100202181 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.1.22-h4243460_0.conda + sha256: a55f0380307d719ea6edfdc92a69b80c9a149d9cc1f2c70cd91c26d56469d793 + md5: 7edb7b0865c4d4309a0a337783022a3c + depends: + - __glibc >=2.28,<3.0.a0 + - arm-variant * sbsa + - cuda-version >=13.2,<13.3.0a0 + - libgcc >=14 + - libstdcxx >=14 + - rdma-core >=61.0 + constrains: + - arm-variant * sbsa + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 965937 + timestamp: 1776110037973 - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda sha256: aa8e8c4be9a2e81610ddf574e05b64ee131fab5e0e3693210c9d6d2fba32c680 md5: 6c77a605a7a689d17d4819c0f8ac9a00 diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index a6147cff6ce..bb6fd97df6f 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -391,7 +391,11 @@ cdef class Buffer: def is_managed(self) -> bool: """Return True if this buffer is CUDA managed (unified) memory, otherwise False.""" _init_mem_attrs(self) - return self._mem_attrs.is_managed + if self._mem_attrs.is_managed: + return True + # Pool-allocated managed memory does not set CU_POINTER_ATTRIBUTE_IS_MANAGED, + # so fall back to the memory resource when it advertises managed allocations. + return self._memory_resource is not None and self._memory_resource.is_managed @property def is_mapped(self) -> bool: @@ -535,6 +539,11 @@ cdef class MemoryResource: """Whether buffers allocated by this resource are host-accessible.""" raise TypeError("MemoryResource.is_host_accessible must be implemented by subclasses.") + @property + def is_managed(self) -> bool: + """Whether buffers allocated by this resource are CUDA managed (unified) memory.""" + return False + @property def device_id(self) -> int: """Device ID associated with this memory resource, or -1 if not applicable.""" diff --git a/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx b/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx index 9562ae1335c..205d3c77545 100644 --- a/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx @@ -121,6 +121,11 @@ cdef class ManagedMemoryResource(_MemPool): """Return True. This memory resource provides host-accessible buffers.""" return True + @property + def is_managed(self) -> bool: + """Return True. This memory resource provides managed (unified) memory buffers.""" + return True + IF CUDA_CORE_BUILD_MAJOR >= 13: cdef tuple _VALID_LOCATION_TYPES = ("device", "host", "host_numa") diff --git a/cuda_core/pixi.lock b/cuda_core/pixi.lock index d48619be4aa..4b7d2809cf1 100644 --- a/cuda_core/pixi.lock +++ b/cuda_core/pixi.lock @@ -591,14 +591,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cccl_linux-64-13.2.27-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-crt-dev_linux-64-13.2.51-ha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-dev-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-static-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.51-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.75-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-dev-13.2.75-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-64-13.2.75-h376f20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-static-13.2.75-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-64-13.2.75-h376f20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.75-h376f20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.78-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-13.2.51-h69a702a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.2.51-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-13.2.51-h4bc722e_0.conda @@ -646,7 +646,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.77-h3ff7636_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.0.44-h85c024f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda @@ -801,14 +801,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cccl_linux-aarch64-13.2.27-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-crt-dev_linux-aarch64-13.2.51-h579c4fd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-dev-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-static-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-dev-13.2.51-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-dev-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-aarch64-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-static-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-aarch64-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.78-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-dev-13.2.78-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-13.2.51-he9431aa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-13.2.51-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-impl-13.2.51-h7b14b0b_0.conda @@ -853,7 +853,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.2.0-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.77-h68e9139_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.11.0-5_hd72aa62_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.0.44-h4243460_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.1.22-h4243460_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda @@ -1002,8 +1002,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_win-64-13.2.51-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_win-64-13.2.51-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_win-64-13.2.51-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.51-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-dev-13.2.51-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.78-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-dev-13.2.78-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-13.2.51-h719f0c7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.2.51-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.2.51-h2466b09_0.conda @@ -1149,14 +1149,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cccl_linux-64-13.2.27-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-crt-dev_linux-64-13.2.51-ha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-dev-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-static-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.51-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.75-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-dev-13.2.75-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-64-13.2.75-h376f20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-static-13.2.75-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-64-13.2.75-h376f20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.75-h376f20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.78-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-13.2.51-h69a702a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.2.51-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-13.2.51-h4bc722e_0.conda @@ -1204,7 +1204,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.77-h3ff7636_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.0.44-h85c024f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda @@ -1359,14 +1359,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cccl_linux-aarch64-13.2.27-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-crt-dev_linux-aarch64-13.2.51-h579c4fd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-dev-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-static-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-dev-13.2.51-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-dev-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-aarch64-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-static-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-aarch64-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.78-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-dev-13.2.78-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-13.2.51-he9431aa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-13.2.51-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-impl-13.2.51-h7b14b0b_0.conda @@ -1411,7 +1411,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.2.0-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.77-h68e9139_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.11.0-5_hd72aa62_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.0.44-h4243460_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.1.22-h4243460_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda @@ -1560,8 +1560,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_win-64-13.2.51-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_win-64-13.2.51-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_win-64-13.2.51-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.51-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-dev-13.2.51-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.78-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-dev-13.2.78-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-13.2.51-h719f0c7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.2.51-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.2.51-h2466b09_0.conda @@ -1720,9 +1720,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.3-py314hd8ed1ab_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cssutils-2.11.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.51-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.75-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.75-h376f20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-13.2.51-h69a702a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.2.51-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-13.2.51-h4bc722e_0.conda @@ -1766,7 +1766,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-6_h4a7cf45_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.77-hd0affe5_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.0.44-h85c024f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.5-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda @@ -1903,9 +1903,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.3-py314hd8ed1ab_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cssutils-2.11.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.51-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.75-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.78-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-13.2.51-he9431aa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-13.2.51-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-impl-13.2.51-h7b14b0b_0.conda @@ -1949,7 +1949,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.11.0-6_haddc8a3_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.77-hf9559e3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.11.0-6_hd72aa62_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.0.44-h4243460_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.1.22-h4243460_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.5-hfae3067_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda @@ -2084,7 +2084,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.3-py314hd8ed1ab_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cssutils-2.11.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.51-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.78-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-13.2.51-h719f0c7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.2.51-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.2.51-h2466b09_0.conda @@ -2251,15 +2251,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.3-py314hd8ed1ab_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cccl_linux-64-13.2.27-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-crt-tools-13.2.51-ha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.51-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.75-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-64-13.2.51-h376f20c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.51-h376f20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.75-h376f20c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cuobjdump-13.2.51-hffce074_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cupti-13.2.23-h676940d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvcc-tools-13.2.51-he02047a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvdisasm-13.2.51-hffce074_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.51-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvtx-13.2.20-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-13.2.51-h69a702a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.2.51-ha770c72_0.conda @@ -2314,7 +2314,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libcudnn-9.20.0.48-ha4b6413_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcudss-0.7.1.4-h7bcfba5_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufft-12.2.0.37-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.0.44-h85c024f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurand-10.4.2.51-h676940d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcusolver-12.1.0.51-h676940d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcusparse-12.7.9.17-hecca717_0.conda @@ -2481,15 +2481,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.3-py314hd8ed1ab_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cccl_linux-aarch64-13.2.27-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-crt-tools-13.2.51-h579c4fd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.51-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.75-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-aarch64-13.2.51-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-aarch64-13.2.51-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.51-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.75-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cuobjdump-13.2.51-h2079400_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cupti-13.2.23-he38c790_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvcc-tools-13.2.51-h614329b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvdisasm-13.2.51-h40ab4d6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.51-h8f3c8d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.78-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvtx-13.2.20-h8f3c8d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-13.2.51-he9431aa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-13.2.51-h579c4fd_0.conda @@ -2541,7 +2541,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcudnn-9.20.0.48-h0bf6004_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcudss-0.7.1.4-he387df4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufft-12.2.0.37-h8f3c8d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.0.44-h4243460_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.1.22-h4243460_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurand-10.4.2.51-he38c790_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcusolver-12.1.0.51-he38c790_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcusparse-12.7.9.17-h8f3c8d4_0.conda @@ -2696,7 +2696,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.51-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.78-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-13.2.51-h719f0c7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.2.51-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.2.51-h2466b09_0.conda @@ -3474,7 +3474,7 @@ packages: - cuda-pathfinder - libnvjitlink - cuda-nvrtc - - cuda-nvrtc >=13.2.51,<14.0a0 + - cuda-nvrtc >=13.2.78,<14.0a0 - cuda-nvvm - libnvfatbin - vc >=14.3,<15 @@ -3500,11 +3500,11 @@ packages: - cuda-pathfinder - libnvjitlink - cuda-nvrtc - - cuda-nvrtc >=13.2.51,<14.0a0 + - cuda-nvrtc >=13.2.78,<14.0a0 - cuda-nvvm - libnvfatbin - libcufile - - libcufile >=1.17.0.44,<2.0a0 + - libcufile >=1.17.1.22,<2.0a0 - libgcc >=15 - libgcc >=15 - libstdcxx >=15 @@ -3528,11 +3528,11 @@ packages: - cuda-pathfinder - libnvjitlink - cuda-nvrtc - - cuda-nvrtc >=13.2.51,<14.0a0 + - cuda-nvrtc >=13.2.78,<14.0a0 - cuda-nvvm - libnvfatbin - libcufile - - libcufile >=1.17.0.44,<2.0a0 + - libcufile >=1.17.1.22,<2.0a0 - libgcc >=15 - libgcc >=15 - libstdcxx >=15 @@ -3673,8 +3673,8 @@ packages: - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - cuda-nvrtc >=13.2.51,<14.0a0 - python_abi 3.14.* *_cp314 + - cuda-nvrtc >=13.2.78,<14.0a0 license: Apache-2.0 - conda: . name: cuda-core @@ -3718,9 +3718,9 @@ packages: - libgcc >=15 - libgcc >=15 - libstdcxx >=15 - - cuda-cudart >=13.2.51,<14.0a0 - - cuda-nvrtc >=13.2.51,<14.0a0 - python_abi 3.14.* *_cp314 + - cuda-nvrtc >=13.2.78,<14.0a0 + - cuda-cudart >=13.2.75,<14.0a0 license: Apache-2.0 - conda: . name: cuda-core @@ -3762,9 +3762,9 @@ packages: - libgcc >=15 - libgcc >=15 - libstdcxx >=15 - - cuda-cudart >=13.2.51,<14.0a0 - - cuda-nvrtc >=13.2.51,<14.0a0 - python_abi 3.14.* *_cp314 + - cuda-nvrtc >=13.2.78,<14.0a0 + - cuda-cudart >=13.2.75,<14.0a0 license: Apache-2.0 - conda: . name: cuda-core @@ -3892,19 +3892,19 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 23242 timestamp: 1749218416505 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.51-hecca717_0.conda - sha256: 9cc44fd4914738a32cf5c801925a08c61ce45b5534833cf1df1621236a9a321d - md5: 29f5b46965bd82b0e9cc27a96d13f2bd +- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-13.2.75-hecca717_0.conda + sha256: 633bc9ba458a12a20a42776bf3fa25cecfddc65a22e4ed207fe09b9adcd9de58 + md5: 9b7dcd83f8a965efcf7377dc54203619 depends: - __glibc >=2.17,<3.0.a0 - - cuda-cudart_linux-64 13.2.51 h376f20c_0 + - cuda-cudart_linux-64 13.2.75 h376f20c_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 24534 - timestamp: 1773104357094 + size: 24542 + timestamp: 1776110472025 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-12.9.79-h3ae8b8a_0.conda sha256: 3d6699fc27ffabf28a9d359b48e7b88437e4d945844718a58608627998db5d1b md5: df78e19e5fe656631d1470aa0fcf6ced @@ -3917,19 +3917,19 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 23466 timestamp: 1749218349235 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.51-h8f3c8d4_0.conda - sha256: 29be349d467aa2d2a8b6ccc738fca46e7a34ae30d220b6a70a2c99116f904801 - md5: 9dd153714cb42e271205d8b91d6da397 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-13.2.75-h8f3c8d4_0.conda + sha256: bfcf3a00ee2ac1632409564c5c6369e4bd385101da7cf3bce7a044ea6ae660dc + md5: 200ffa107a11ece7e72b4931f5e40650 depends: - arm-variant * sbsa - - cuda-cudart_linux-aarch64 13.2.51 h8f3c8d4_0 + - cuda-cudart_linux-aarch64 13.2.75 h8f3c8d4_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 24693 - timestamp: 1773104347563 + size: 24677 + timestamp: 1776110462886 - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-cudart-12.9.79-he0c23c2_0.conda sha256: a30cd9adf3a70d069d4d87c5728ec16778b77071629612ca5d8513cd92d89c09 md5: 0a243d4f000a0d2f51dd94ee9132b234 @@ -3956,20 +3956,20 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 23687 timestamp: 1749218464010 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-dev-13.2.51-hecca717_0.conda - sha256: f6d81c961b6212389c07ffc9dc1268966db63aa351d46875effee40447eb9dd8 - md5: 9b35a56418b6cbbde5ea5f7d84c26317 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-dev-13.2.75-hecca717_0.conda + sha256: c11c338b24c37ae05d39ae752a661b199c6530f2f189be1cc718b23485cd8626 + md5: 145b05176a16bf8ffa64defccde19162 depends: - __glibc >=2.17,<3.0.a0 - - cuda-cudart 13.2.51 hecca717_0 - - cuda-cudart-dev_linux-64 13.2.51 h376f20c_0 - - cuda-cudart-static 13.2.51 hecca717_0 + - cuda-cudart 13.2.75 hecca717_0 + - cuda-cudart-dev_linux-64 13.2.75 h376f20c_0 + - cuda-cudart-static 13.2.75 hecca717_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 24961 - timestamp: 1773104406956 + size: 25017 + timestamp: 1776110522210 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-dev-12.9.79-h3ae8b8a_0.conda sha256: d70f85411992e03494f2fe94a9852d79f366a92f40ba791611eda5551044afe9 md5: d58cc487273764a11637456c06399ff0 @@ -3984,20 +3984,20 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 23911 timestamp: 1749218369632 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-dev-13.2.51-h8f3c8d4_0.conda - sha256: 85b87e97cffc45aac2a85104f7afcf376f90d589afaa8026cefe5e518130e238 - md5: 0ac4ca914e1974f7651884841628fe9d +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-dev-13.2.75-h8f3c8d4_0.conda + sha256: acb99e3172b838d09019f8c07f8a175387f616a32cf84b00eb7438067c48cd15 + md5: 06d642fda27317458fe38c4e830dbcb1 depends: - arm-variant * sbsa - - cuda-cudart 13.2.51 h8f3c8d4_0 - - cuda-cudart-dev_linux-aarch64 13.2.51 h8f3c8d4_0 - - cuda-cudart-static 13.2.51 h8f3c8d4_0 + - cuda-cudart 13.2.75 h8f3c8d4_0 + - cuda-cudart-dev_linux-aarch64 13.2.75 h8f3c8d4_0 + - cuda-cudart-static 13.2.75 h8f3c8d4_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 25169 - timestamp: 1773104375869 + size: 25151 + timestamp: 1776110491423 - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-cudart-dev-12.9.79-he0c23c2_0.conda sha256: 1ee68f0ffd37889f0fc438d4da7124054b124632e1c3bc15950b9851b002473e md5: e5bb074108bc2501f8374e80748aa181 @@ -4034,6 +4034,17 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 399921 timestamp: 1773104368666 +- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-64-13.2.75-h376f20c_0.conda + sha256: feb6d90170dbdbbc873d065f17c55845b03e1bd132d5727ba16c9dc5048c3a98 + md5: 0104d270d83f6c3f6b4f8f761da37bf4 + depends: + - cuda-cccl_linux-64 + - cuda-cudart-static_linux-64 + - cuda-cudart_linux-64 + - cuda-version >=13.2,<13.3.0a0 + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 398384 + timestamp: 1776110485442 - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-aarch64-12.9.79-h3ae8b8a_0.conda sha256: ad64a1ecfc933172dbc6407d71b1abb78dc7ffcd5cc871baee238350307a7c0c md5: 60e07c05a51d5549bec1e7ee38849feb @@ -4058,6 +4069,18 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 400484 timestamp: 1773104355769 +- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_linux-aarch64-13.2.75-h8f3c8d4_0.conda + sha256: f1426051de025dade0baedd91c53d34e8764433664968b03350fd80b2b3fae64 + md5: 94866ab4accc1772d96ab8c1054ebd02 + depends: + - arm-variant * sbsa + - cuda-cccl_linux-aarch64 + - cuda-cudart-static_linux-aarch64 + - cuda-cudart_linux-aarch64 + - cuda-version >=13.2,<13.3.0a0 + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 397963 + timestamp: 1776110471219 - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-dev_win-64-12.9.79-he0c23c2_0.conda sha256: e022d36a333420130faf6473c49f8dab54bf976cf320577ffb06db0a0797b734 md5: 3c3e2f6b5455783fd332a072d632ea78 @@ -4092,18 +4115,18 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 23283 timestamp: 1749218442382 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-static-13.2.51-hecca717_0.conda - sha256: d4a316038b02161e04a864c8cd146d2ec62cbd114eb951197c6ef6042d3c46c4 - md5: daec4c4dc0355adcdf009dceb3b94259 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-cudart-static-13.2.75-hecca717_0.conda + sha256: bb55bbd1d5961953889abef8c1c2ec011eff0c4d3dd92f46d06fd4176285f430 + md5: 42208a65f539b7dca4c900681649f599 depends: - __glibc >=2.17,<3.0.a0 - - cuda-cudart-static_linux-64 13.2.51 h376f20c_0 + - cuda-cudart-static_linux-64 13.2.75 h376f20c_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 24494 - timestamp: 1773104383494 + size: 24532 + timestamp: 1776110498692 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-static-12.9.79-h3ae8b8a_0.conda sha256: dac33edcebbf557563a41521f67961039186efbc276903d937b32243ef3be937 md5: 365adcddf99b81eb323698fda31d507c @@ -4116,18 +4139,18 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 23507 timestamp: 1749218358755 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-static-13.2.51-h8f3c8d4_0.conda - sha256: 2d9ca3121765a7e9567285ff7d2f285381d35bda65c08333205084370ee174f0 - md5: fb60e62fd7fc3cb9076e9e328597d525 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-cudart-static-13.2.75-h8f3c8d4_0.conda + sha256: f66bc88f20b69b5cacbf316fcedc3c056d2e97cf13c13bfcd631fb282923a50e + md5: e622522d95a66ba77990682fd74ca9ed depends: - arm-variant * sbsa - - cuda-cudart-static_linux-aarch64 13.2.51 h8f3c8d4_0 + - cuda-cudart-static_linux-aarch64 13.2.75 h8f3c8d4_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 24659 - timestamp: 1773104359540 + size: 24673 + timestamp: 1776110474896 - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-cudart-static-12.9.79-he0c23c2_0.conda sha256: 02d3ff9ec59c7f59132ffe9398746ad9422a75706e7cad19acc6c30a5c0fc763 md5: 718879691b8119c893f587f46c734fca @@ -4156,6 +4179,14 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 1103784 timestamp: 1773104321614 +- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-64-13.2.75-h376f20c_0.conda + sha256: f4e8c80fe897a426bb6a413b685d7e16eaf52cdbbcf3fa73cf24c994da82b0ef + md5: 6e8700fbcdf3a916d4494db9811d955a + depends: + - cuda-version >=13.2,<13.3.0a0 + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 1105717 + timestamp: 1776110435801 - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-aarch64-12.9.79-h3ae8b8a_0.conda sha256: d4be038bad9abf0eac1e88dc57c8db6a469db8eb5d7c281085dfbb018ef84212 md5: 52498fedeb43bbd4c45f84a0fb722d21 @@ -4174,6 +4205,15 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 1117788 timestamp: 1773104327628 +- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-aarch64-13.2.75-h8f3c8d4_0.conda + sha256: b8bd750f6e74b37b3c779c60611adb7975dcd0d83d9182207a30dd3b80e1e842 + md5: 6b30bfd1de99feaefa85810344d5536d + depends: + - arm-variant * sbsa + - cuda-version >=13.2,<13.3.0a0 + license: LicenseRef-NVIDIA-End-User-License-Agreement + size: 1109948 + timestamp: 1776110443043 - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_win-64-12.9.79-he0c23c2_0.conda sha256: 6a3410cd7ce07955cb705801055ef129ebee1cd6390c6fe9e5f607b67c3dba36 md5: 0dd152a1493d90356037604a865f050f @@ -4198,15 +4238,15 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 197249 timestamp: 1749218394213 -- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.51-h376f20c_0.conda - sha256: e1d943a5582c8e171c9dcf2c0c72ddd5bf0a2ac9acd6ed15898d69d618cf53c6 - md5: 51a1624c7e26d8821b5d959ee7ecb517 +- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.75-h376f20c_0.conda + sha256: cd03c67b2005e2e74ff278f6f8b17ca7d6f18cf43fb00775833669508d301a83 + md5: ff98f2b9b87eb8b3a4b36745d3d5b93e depends: - cuda-version >=13.2,<13.3.0a0 license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 203460 - timestamp: 1773104333900 + size: 203339 + timestamp: 1776110448238 - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-12.9.79-h3ae8b8a_0.conda sha256: 4900ff2f000a4f8a70a7bc8576469640aa6590618fa9e73c84e066e025dcb760 md5: cc2459ad427431e089d78d760cf24437 @@ -4216,16 +4256,16 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 212993 timestamp: 1749218341193 -- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.51-h8f3c8d4_0.conda - sha256: 187a80a3c5b1e5006c21cce319f54a3cb01f29e671f9468e5d8bb99969128fab - md5: 8f795465876173a2f2f39fe5e14fd3a9 +- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-aarch64-13.2.75-h8f3c8d4_0.conda + sha256: 810fb567425b5b72bf9babb5d627a41166dd00fcfe4096fae840665bf5fa280b + md5: 1583c2df45cb4b17c20d15aa870a8940 depends: - arm-variant * sbsa - cuda-version >=13.2,<13.3.0a0 license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 217385 - timestamp: 1773104336884 + size: 217387 + timestamp: 1776110452294 - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_win-64-12.9.79-he0c23c2_0.conda sha256: 6a89a53cdbcfafa0bb55abee1b58492c6a9a28e688abe04f48f0d01649c5f3e4 md5: 71c9c2ab52226f990f268164381d8494 @@ -4487,9 +4527,9 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 67168282 timestamp: 1760723629347 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.51-hecca717_0.conda - sha256: 9de235d328b7124f715805715e9918eb7f8aa5b9c56a2afa62b84f84f98077a5 - md5: 0413baaa73be1a39d5d8e442184acc78 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda + sha256: 73fbc9d15c062c3ea60891e8183002f6b055fa6638402d17581677af0aaa20d8 + md5: 66623d882c42506fa3f1780b90841400 depends: - __glibc >=2.17,<3.0.a0 - cuda-version >=13.2,<13.3.0a0 @@ -4497,8 +4537,8 @@ packages: - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 35736655 - timestamp: 1773100338749 + size: 35670504 + timestamp: 1776109867257 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-12.9.86-h8f3c8d4_1.conda sha256: e7f8d835d7bf993dcad9fba6db5af89c35b2b4f0282799b729bf6ad2c3bd896d md5: 48187c09673a42f9930764e8170b8787 @@ -4510,9 +4550,9 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 33382016 timestamp: 1760723722396 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.51-h8f3c8d4_0.conda - sha256: 1fd23b87b7184b2ca38cf3e7c3197bb44b897d8f42010bb87edf5d689a041452 - md5: 5005bdb2a590f169dcb3ce3a321f6009 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-13.2.78-h8f3c8d4_0.conda + sha256: dcc9a9890d04850ffecc59748d546dcde9c80d3040b726cf3839b8191c6d3548 + md5: b6632980124d529a2b87f68831c58743 depends: - arm-variant * sbsa - cuda-version >=13.2,<13.3.0a0 @@ -4520,8 +4560,8 @@ packages: - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 33927374 - timestamp: 1773100385281 + size: 33929238 + timestamp: 1776109887303 - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-12.9.86-hac47afa_1.conda sha256: d90ef446ac859db26286a5d39d39333c4e4cee31ba5042b5c7922bd25de531f6 md5: d68b5d96a53c80dc3dbbd8f7c3b8106d @@ -4533,9 +4573,9 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 58467504 timestamp: 1760723834711 -- conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.51-hac47afa_0.conda - sha256: 217b5194ecd6834e361329d6132c2dfc96fa588715438231503dc12d59e0fac8 - md5: 15f6f2629264309d48712c499ac6f3f5 +- conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-13.2.78-hac47afa_0.conda + sha256: 5e2b53023ceafae1f7f4d83e83ebf175befb04f50fa131ba7c78d0cc6b299477 + md5: 3c8fa05aa0fe9d3cb4638e8bd7bb84e3 depends: - cuda-version >=13.2,<13.3.0a0 - ucrt >=10.0.20348.0 @@ -4543,8 +4583,8 @@ packages: - vc14_runtime >=14.44.35208 license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 31221551 - timestamp: 1773100427009 + size: 31226924 + timestamp: 1776110029365 - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-12.9.86-hecca717_1.conda sha256: ae620051c16eabf7720a47c5115634d64f7703d32124555ad0afccfd4b8d7cf4 md5: 0d28090f4e63410e20397c7975612837 @@ -4559,20 +4599,20 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 36819 timestamp: 1760723845601 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.51-hecca717_0.conda - sha256: be60eb4e84ff4846b27b323eca402b075f52caf6c138ebb06268fbaa26ef1879 - md5: 83535200a9e77165d5291b4ac82ebf6a +- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.78-hecca717_0.conda + sha256: 12505f1bbc222acf2a63da5c84e4176d2f9c18b458e2bde28939fdf326b6d292 + md5: cc313f0ea18ebc6e713a8980611431f5 depends: - __glibc >=2.17,<3.0.a0 - - cuda-nvrtc 13.2.51 hecca717_0 + - cuda-nvrtc 13.2.78 hecca717_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 constrains: - - cuda-nvrtc-static >=13.2.51 + - cuda-nvrtc-static >=13.2.78 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 36305 - timestamp: 1773100458841 + size: 36312 + timestamp: 1776109983818 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-dev-12.9.86-h8f3c8d4_1.conda sha256: b1d1a74cbbdcf46c4ee737279df3220eb7a29393999bc96b3c1398f7de78c912 md5: 2346ee558cbfb7b857c8353ffc2553fa @@ -4588,21 +4628,21 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 36250 timestamp: 1760723865518 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-dev-13.2.51-h8f3c8d4_0.conda - sha256: 23fc90d4101d3edfe10f47b83182373abf5913fbea42789d8c7dba49cb4bbaad - md5: 80925c8301f889d18e5854385cecb82a +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvrtc-dev-13.2.78-h8f3c8d4_0.conda + sha256: 20f7cf1514ecbcefc2740823864aa79aa545860ae6aa6c2fa8d1a8c97f739e10 + md5: 812af6ce75d4aae7c76e75dcdb8b434b depends: - arm-variant * sbsa - - cuda-nvrtc 13.2.51 h8f3c8d4_0 + - cuda-nvrtc 13.2.78 h8f3c8d4_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 constrains: - arm-variant * sbsa - - cuda-nvrtc-static >=13.2.51 + - cuda-nvrtc-static >=13.2.78 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 36381 - timestamp: 1773100508513 + size: 36316 + timestamp: 1776109996052 - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-dev-12.9.86-hac47afa_1.conda sha256: 1e1c41f95d606eaf6581fccf9546ed6ee4053c42b78e11057cd6d2801b96f0e2 md5: b97225dd005cb0dcdca7911c61ca38e5 @@ -4615,18 +4655,18 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 35214 timestamp: 1760724506186 -- conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-dev-13.2.51-hac47afa_0.conda - sha256: 7ed6a75c1e3bd9a89490e1094694375ae0ee1870b5ab2e298b3d985a148c5933 - md5: 4993115c33f07ff3b338992a8e4008f2 +- conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvrtc-dev-13.2.78-hac47afa_0.conda + sha256: 2b40a8ce9b1496b45232d20e15d8cb6e797792d9f7990feeb3149a4f089a2d8b + md5: f5a4101ba76f052b882a83f957298252 depends: - - cuda-nvrtc 13.2.51 hac47afa_0 + - cuda-nvrtc 13.2.78 hac47afa_0 - cuda-version >=13.2,<13.3.0a0 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 35405 - timestamp: 1773101046259 + size: 35456 + timestamp: 1776110679144 - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvtx-13.2.20-hecca717_0.conda sha256: bba34d2a2c3c5e8dea759e3a8ba99e2b4524deaebad3dd77b4b67d586cac05e0 md5: 1141dc393a63e2054bbf60b2be31e730 @@ -7339,9 +7379,9 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 969845 timestamp: 1761098818759 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.0.44-h85c024f_0.conda - sha256: dc2b0c43aeacbaa686061353807e718236d8c5b346f624e76fed98b066898e19 - md5: 6d8ed8335d144ec7303b8d3587b2205c +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda + sha256: a24ad0ca488aa3e237049cd5b5c6d7fe3d2d4330682ed329203064e332ea1d74 + md5: 056a67706108efd1f9c24682ba8d3685 depends: - __glibc >=2.28,<3.0.a0 - cuda-version >=13.2,<13.3.0a0 @@ -7350,8 +7390,8 @@ packages: - rdma-core >=61.0 license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 1085341 - timestamp: 1773100191342 + size: 1082447 + timestamp: 1776110053053 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.14.1.1-had8bf56_1.conda sha256: fbc1fa6b3ddf946b2999c9820310682739505df71e1e2ac513a72efb951fa3e5 md5: ee136db5a5409dddc78eaf7658fccffe @@ -7365,9 +7405,9 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 909365 timestamp: 1761098964619 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.0.44-h4243460_0.conda - sha256: 37615867c9cf3727289fb8f0fabf43e5e5e9989091d6ba86c1eccd9323b54492 - md5: 62177c2a0b2d8ab2cfa065df0ef656b7 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcufile-1.17.1.22-h4243460_0.conda + sha256: a55f0380307d719ea6edfdc92a69b80c9a149d9cc1f2c70cd91c26d56469d793 + md5: 7edb7b0865c4d4309a0a337783022a3c depends: - __glibc >=2.28,<3.0.a0 - arm-variant * sbsa @@ -7379,8 +7419,8 @@ packages: - arm-variant * sbsa license: LicenseRef-NVIDIA-End-User-License-Agreement purls: [] - size: 973639 - timestamp: 1773100202181 + size: 965937 + timestamp: 1776110037973 - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurand-10.4.2.51-h676940d_0.conda sha256: 9da49c9402e74798dae01a975eaef340994dffa9721ccf8798a99d6b8e2bab8f md5: e212e2e729b8e67dbf0a288fb5ed0777 diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index a8e44a7946f..57de22bb9a0 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -605,6 +605,47 @@ def test_managed_buffer_dlpack_roundtrip_device_type(): assert view.__dlpack_device__() == (int(DLDeviceType.kDLCUDAManaged), 0) +def test_managed_memory_resource_buffer_dlpack_device_type(): + """Verify that pool-allocated managed memory reports kDLCUDAManaged. + + Allocations from ManagedMemoryResource go through cuMemAllocFromPoolAsync + on a CU_MEM_ALLOCATION_TYPE_MANAGED pool, which does not set + CU_POINTER_ATTRIBUTE_IS_MANAGED. The Buffer must still report itself as + managed via its memory resource so that DLPack consumers (e.g. CCCL's + make_tma_descriptor) accept the buffer. + """ + device = Device() + device.set_current() + skip_if_managed_memory_unsupported(device) + mr = create_managed_memory_resource_or_skip(ManagedMemoryResourceOptions(preferred_location=device.device_id)) + buf = mr.allocate(1024) + + assert mr.is_managed + assert buf.is_managed + assert buf.__dlpack_device__() == (DLDeviceType.kDLCUDAManaged, 0) + + view = StridedMemoryView.from_any_interface(buf, stream_ptr=-1) + assert view.__dlpack_device__() == (int(DLDeviceType.kDLCUDAManaged), 0) + + +@pytest.mark.parametrize("mr_kind", ["device", "pinned"]) +def test_non_managed_resources_report_not_managed(mr_kind): + """Non-managed memory resources must report is_managed=False.""" + device = Device() + device.set_current() + if not device.properties.memory_pools_supported: + pytest.skip("Device does not support mempool operations") + if mr_kind == "device": + mr = DeviceMemoryResource(device) + else: + skip_if_pinned_memory_unsupported(device) + mr = PinnedMemoryResource() + assert mr.is_managed is False + buf = mr.allocate(1024) + assert buf.is_managed is False + buf.close() + + @pytest.mark.parametrize("use_device_object", [True, False]) def test_device_memory_resource_initialization(use_device_object): """Test that DeviceMemoryResource can be initialized successfully. From fd5700dfcbfd1bcd89344f0a6ff920230503d878 Mon Sep 17 00:00:00 2001 From: Rui Luo Date: Fri, 17 Apr 2026 01:42:40 +0800 Subject: [PATCH 003/244] [no-ci] coverage: fix Linux git ownership and add Windows stack wrapper (#1829) * coverage: add git safe.directory for linux builds * coverage: run coverage for linux without root * coverage: change github workspace owner for Linux coverage * coverage: run cuda core tests in coverage using 8M thread * Add coverage test helper to run pytest using larger stack --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- .github/workflows/coverage.yml | 51 ++++++++++------------------- ci/tools/run_pytest_with_stack.py | 54 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 ci/tools/run_pytest_with_stack.py diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e862867b3dd..95deb813f04 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -68,6 +68,10 @@ jobs: with: fetch-depth: 0 + - name: Fix workspace ownership + run: | + chown -R $(id -u):$(id -g) "$GITHUB_WORKSPACE" + - name: Install dependencies uses: ./.github/actions/install_unix_deps continue-on-error: false @@ -342,46 +346,25 @@ jobs: cd "${{ steps.install-root.outputs.INSTALL_ROOT }}" "$GITHUB_WORKSPACE/.venv/Scripts/pytest" -v --cov=./cuda --cov-append --cov-context=test --cov-config="$GITHUB_WORKSPACE/.coveragerc" "$GITHUB_WORKSPACE/cuda_pathfinder/tests" + # Cython linetrace under coverage on Windows needs more stack than the + # default 1 MB thread size. The helper runs pytest on an 8 MB thread. - name: Run cuda.bindings tests (with 8MB stack) continue-on-error: true run: | - cd "${{ steps.install-root.outputs.INSTALL_ROOT }}" - # Run pytest in 8MB stack thread (Cython linetrace requirement) - "$GITHUB_WORKSPACE/.venv/Scripts/python" << PYTEST_EOF - import os - import sys - import threading - import pytest - - os.chdir(r'${{ steps.install-root.outputs.INSTALL_ROOT }}') - threading.stack_size(8 * 1024 * 1024) - result = {'code': 1} - - def _run(): - workspace = os.environ['GITHUB_WORKSPACE'] - result['code'] = pytest.main([ - '-v', - '--cov=./cuda', - '--cov-append', - '--cov-context=test', - f'--cov-config={workspace}/.coveragerc', - f'{workspace}/cuda_bindings/tests' - ]) - - t = threading.Thread(target=_run) - t.start() - t.join() - - print(f'Bindings tests exit code: {result["code"]}') - # Exit with actual code (continue-on-error handles it) - sys.exit(result['code']) - PYTEST_EOF + "$GITHUB_WORKSPACE/.venv/Scripts/python" "$GITHUB_WORKSPACE/ci/tools/run_pytest_with_stack.py" \ + --cwd "${{ steps.install-root.outputs.INSTALL_ROOT }}" \ + -v --cov=./cuda --cov-append --cov-context=test \ + --cov-config="$GITHUB_WORKSPACE/.coveragerc" \ + "$GITHUB_WORKSPACE/cuda_bindings/tests" - - name: Run cuda.core tests + - name: Run cuda.core tests (with 8MB stack) continue-on-error: true run: | - cd "${{ steps.install-root.outputs.INSTALL_ROOT }}" - "$GITHUB_WORKSPACE/.venv/Scripts/pytest" -v --cov=./cuda --cov-append --cov-context=test --cov-config="$GITHUB_WORKSPACE/.coveragerc" "$GITHUB_WORKSPACE/cuda_core/tests" + "$GITHUB_WORKSPACE/.venv/Scripts/python" "$GITHUB_WORKSPACE/ci/tools/run_pytest_with_stack.py" \ + --cwd "${{ steps.install-root.outputs.INSTALL_ROOT }}" \ + -v --cov=./cuda --cov-append --cov-context=test \ + --cov-config="$GITHUB_WORKSPACE/.coveragerc" \ + "$GITHUB_WORKSPACE/cuda_core/tests" - name: Copy Windows coverage file to workspace run: | diff --git a/ci/tools/run_pytest_with_stack.py b/ci/tools/run_pytest_with_stack.py new file mode 100644 index 00000000000..e1e1f782ba1 --- /dev/null +++ b/ci/tools/run_pytest_with_stack.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Run pytest on a thread with a larger stack size. + +Cython linetrace instrumentation under coverage on Windows can exceed the +default 1 MB thread stack. This helper spawns a single worker thread with +a configurable stack (default 8 MB) so the rest of the CI workflow stays +readable. + +Usage: + python run_pytest_with_stack.py [--stack-mb N] [--cwd DIR] [pytest args ...] +""" + +import argparse +import concurrent.futures +import os +import sys +import threading + +import pytest + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--stack-mb", + type=int, + default=8, + help="Thread stack size in megabytes (default: 8)", + ) + parser.add_argument( + "--cwd", + default=None, + help="Working directory for the test run", + ) + args, pytest_args = parser.parse_known_args() + + if args.cwd: + os.chdir(args.cwd) + + threading.stack_size(args.stack_mb * 1024 * 1024) + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + code = pool.submit(pytest.main, pytest_args).result() + + sys.exit(code) + + +if __name__ == "__main__": + main() From d818a754f03274351300fa622476a6d31b109ee6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 17 Apr 2026 01:50:01 +0700 Subject: [PATCH 004/244] Fix cuda_bindings conjugate_gradient_multi_block_cg.py example (#1922) * fix: re-enable and fix the conjugate gradient multi-block example Remove the unconditional NVRTC waiver from the renamed example so CI can exercise its real execution path again. While re-enabling it, replace the standalone pytest.skip() checks with requirement_not_met() and simplify the platform gating. The waived code path had been hiding several Python-side runtime bugs that required these fixes: - replaced the invalid C-style %d f-string formatting - fixed gen_tridiag() variable shadowing so the CSR row-offset array is actually populated - passed the computed dynamic shared-memory size into cuLaunchCooperativeKernel() and made that size integer-valued - stopped overwriting managed-memory pointer variables with loop indices before kernel launch and cleanup - cached the residual before freeing dot_result, which removed the teardown segfault Made-with: Cursor * fix: check the QNX example gate via platform.system() QNX is an operating system rather than a machine architecture, so checking platform.machine() can miss the requirement_not_met() path on QNX hosts. Use platform.system() so the example is waived consistently on that platform. Made-with: Cursor --- .../global_to_shmem_async_copy.py | 2 +- .../conjugate_gradient_multi_block_cg.py | 93 +++++++++---------- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/cuda_bindings/examples/3_CUDA_Features/global_to_shmem_async_copy.py b/cuda_bindings/examples/3_CUDA_Features/global_to_shmem_async_copy.py index 18f5c88e9d5..d3ab1c53f19 100644 --- a/cuda_bindings/examples/3_CUDA_Features/global_to_shmem_async_copy.py +++ b/cuda_bindings/examples/3_CUDA_Features/global_to_shmem_async_copy.py @@ -1142,7 +1142,7 @@ def matrix_multiply(dims_a, dims_b, kernel_number): def main(): check_compute_capability_too_low(find_cuda_device(), (7, 0)) - if platform.machine() == "qnx": + if platform.system() == "QNX": requirement_not_met("globalToShmemAsyncCopy is not supported on QNX") version = check_cuda_errors(cuda.cuDriverGetVersion()) diff --git a/cuda_bindings/examples/4_CUDA_Libraries/conjugate_gradient_multi_block_cg.py b/cuda_bindings/examples/4_CUDA_Libraries/conjugate_gradient_multi_block_cg.py index 44ff57c6d72..94c9519100f 100644 --- a/cuda_bindings/examples/4_CUDA_Libraries/conjugate_gradient_multi_block_cg.py +++ b/cuda_bindings/examples/4_CUDA_Libraries/conjugate_gradient_multi_block_cg.py @@ -26,6 +26,7 @@ KernelHelper, check_cuda_errors, find_cuda_device, + requirement_not_met, ) conjugate_gradient_multi_block_cg = """\ @@ -177,71 +178,68 @@ """ -def gen_tridiag(i, j, val, n, nz): - i[0] = 0 - j[0] = 0 - j[1] = 0 +def gen_tridiag(row_offsets, col_indices, values, n, nz): + row_offsets[0] = 0 + col_indices[0] = 0 + col_indices[1] = 0 - val[0] = float(random()) + 10.0 - val[1] = float(random()) + values[0] = float(random()) + 10.0 + values[1] = float(random()) - for i in range(1, n): - if i > 1: - i[i] = i[i - 1] + 3 + for row_idx in range(1, n): + if row_idx > 1: + row_offsets[row_idx] = row_offsets[row_idx - 1] + 3 else: - i[1] = 2 + row_offsets[1] = 2 - start = (i - 1) * 3 + 2 - j[start] = i - 1 - j[start + 1] = i + start = (row_idx - 1) * 3 + 2 + col_indices[start] = row_idx - 1 + col_indices[start + 1] = row_idx - if i < n - 1: - j[start + 2] = i + 1 + if row_idx < n - 1: + col_indices[start + 2] = row_idx + 1 - val[start] = val[start - 1] - val[start + 1] = float(random()) + 10.0 + values[start] = values[start - 1] + values[start + 1] = float(random()) + 10.0 - if i < n - 1: - val[start + 2] = float(random()) - i[n] = nz + if row_idx < n - 1: + values[start + 2] = float(random()) + row_offsets[n] = nz THREADS_PER_BLOCK = 512 s_sd_kname = "conjugateGradientMultiBlockCG" +UNSUPPORTED_SYSTEMS = {"Darwin", "QNX"} +UNSUPPORTED_MACHINES = {"armv7l"} def main(): tol = 1e-5 - import pytest + system_name = platform.system() + if system_name in UNSUPPORTED_SYSTEMS: + requirement_not_met(f"{s_sd_kname} is not supported on {system_name}") - # WAIVE: Due to bug in NVRTC - return - - if platform.system() == "Darwin": - pytest.skip("conjugateGradientMultiBlockCG is not supported on Mac OSX") - - if platform.machine() == "armv7l": - pytest.skip("conjugateGradientMultiBlockCG is not supported on ARMv7") - - if platform.machine() == "qnx": - pytest.skip("conjugateGradientMultiBlockCG is not supported on QNX") + machine_name = platform.machine() + if machine_name in UNSUPPORTED_MACHINES: + requirement_not_met(f"{s_sd_kname} is not supported on {machine_name}") # This will pick the best possible CUDA capable device dev_id = find_cuda_device() device_prop = check_cuda_errors(cudart.cudaGetDeviceProperties(dev_id)) if not device_prop.managedMemory: - pytest.skip("Unified Memory not supported on this device") + requirement_not_met("Unified Memory not supported on this device") # This sample requires being run on a device that supports Cooperative Kernel # Launch if not device_prop.cooperativeLaunch: - pytest.skip(f"Selected GPU {dev_id} does not support Cooperative Kernel Launch") + requirement_not_met(f"Selected GPU {dev_id} does not support Cooperative Kernel Launch") # Statistics about the GPU device print( - f"> GPU device has {device_prop.multiProcessorCount:%d} Multi-Processors, SM {device_prop.major:%d}.{device_prop.minor:%d} compute capabilities\n" + f"> GPU device has {device_prop.multiProcessorCount} Multi-Processors, " + f"SM {device_prop.major}.{device_prop.minor} compute capabilities\n" ) # Get kernel @@ -267,7 +265,7 @@ def main(): x_local = (ctypes.c_float * n).from_address(x) rhs_local = (ctypes.c_float * n).from_address(rhs) dot_result_local = (ctypes.c_double).from_address(dot_result) - dot_result_local = 0 + dot_result_local.value = 0.0 # temp memory for CG r = check_cuda_errors(cudart.cudaMallocManaged(np.dtype(np.float32).itemsize * n, cudart.cudaMemAttachGlobal)) @@ -280,9 +278,9 @@ def main(): start = check_cuda_errors(cudart.cudaEventCreate()) stop = check_cuda_errors(cudart.cudaEventCreate()) - for i in range(n): - r_local[i] = rhs_local[i] = 1.0 - x_local[i] = 0.0 + for idx in range(n): + r_local[idx] = rhs_local[idx] = 1.0 + x_local[idx] = 0.0 kernel_args_value = (i, j, val, x, ax, p, r, dot_result, nz, n, tol) kernel_args_types = ( @@ -300,7 +298,7 @@ def main(): ) kernel_args = (kernel_args_value, kernel_args_types) - s_mem_size = np.dtype(np.float64).itemsize * ((THREADS_PER_BLOCK / 32) + 1) + s_mem_size = np.dtype(np.float64).itemsize * ((THREADS_PER_BLOCK // 32) + 1) num_threads = THREADS_PER_BLOCK num_blocks_per_sm = check_cuda_errors( cuda.cuOccupancyMaxActiveBlocksPerMultiprocessor(_gpu_conjugate_gradient, num_threads, s_mem_size) @@ -325,7 +323,7 @@ def main(): dim_block.x, dim_block.y, dim_block.z, - 0, + s_mem_size, 0, kernel_args, ) @@ -334,16 +332,17 @@ def main(): check_cuda_errors(cudart.cudaDeviceSynchronize()) time = check_cuda_errors(cudart.cudaEventElapsedTime(start, stop)) - print(f"GPU Final, residual = {math.sqrt(dot_result_local):e}, kernel execution time = {time:f} ms") + residual = math.sqrt(dot_result_local.value) + print(f"GPU Final, residual = {residual:e}, kernel execution time = {time:f} ms") err = 0.0 - for i in range(n): + for row_idx in range(n): rsum = 0.0 - for j in range(i_local[i], i_local[i + 1]): - rsum += val_local[j] * x_local[j_local[j]] + for elem_idx in range(i_local[row_idx], i_local[row_idx + 1]): + rsum += val_local[elem_idx] * x_local[j_local[elem_idx]] - diff = math.fabs(rsum - rhs_local[i]) + diff = math.fabs(rsum - rhs_local[row_idx]) if diff > err: err = diff @@ -361,7 +360,7 @@ def main(): check_cuda_errors(cudart.cudaEventDestroy(stop)) print(f"Test Summary: Error amount = {err:f}") - if math.sqrt(dot_result_local) >= tol: + if residual >= tol: print("conjugateGradientMultiBlockCG FAILED", file=sys.stderr) sys.exit(1) From c85391ad6b3968a46da2a425ec1a0bad3552da68 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Thu, 16 Apr 2026 17:11:48 -0400 Subject: [PATCH 005/244] Sync CI: switch to rapidsai sccache fork and bump CUDA to 13.2.1 (#1914) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync CI: switch to rapidsai sccache fork and bump CUDA to 13.2.1 - Replace mozilla-actions/sccache-action with manual rapidsai/sccache fork installation (the action hasn't been updated since Oct 2025) - Add SCCACHE_GHA_USE_PREPROCESSOR_CACHE_MODE to cibuildwheel env - Use --show-adv-stats for richer sccache output - Bump CUDA test version 13.2.0 → 13.2.1 in test-matrix.yml and versions.yml * Move guess_latest.sh to ci/tools/ Consolidate CI scripts under ci/tools/ instead of scattering them across .github/workflows/. --- .github/workflows/build-wheel.yml | 29 +++++++----- ci/test-matrix.yml | 44 +++++++++---------- ci/tools/env-vars | 2 +- .../workflows => ci/tools}/guess_latest.sh | 0 ci/versions.yml | 2 +- 5 files changed, 42 insertions(+), 35 deletions(-) rename {.github/workflows => ci/tools}/guess_latest.sh (100%) diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index 5bf5a598146..fbb8d092b06 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -44,20 +44,24 @@ jobs: with: fetch-depth: 0 - # The env vars ACTIONS_CACHE_SERVICE_V2, ACTIONS_RESULTS_URL, and ACTIONS_RUNTIME_TOKEN - # are exposed by this action. - - name: Enable sccache - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # 0.0.9 - with: - disable_annotations: 'true' + - name: Install latest rapidsai/sccache + if: ${{ startsWith(inputs.host-platform, 'linux') }} + run: | + curl -fsSL "https://github.com/rapidsai/sccache/releases/latest/download/sccache-$(uname -m)-unknown-linux-musl.tar.gz" \ + | sudo tar -C /usr/local/bin -xvzf - --wildcards --strip-components=1 -x '*/sccache' + echo "SCCACHE_PATH=/usr/local/bin/sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_USE_PREPROCESSOR_CACHE_MODE=true" >> "$GITHUB_ENV" # xref: https://github.com/orgs/community/discussions/42856#discussioncomment-7678867 - name: Adding addtional GHA cache-related env vars uses: actions/github-script@v8 with: script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL']) - core.exportVariable('ACTIONS_RUNTIME_URL', process.env['ACTIONS_RUNTIME_URL']) + core.exportVariable('ACTIONS_CACHE_SERVICE_V2', 'on'); + core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL'] || ''); + core.exportVariable('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL'] || ''); + core.exportVariable('ACTIONS_RUNTIME_URL', process.env['ACTIONS_RUNTIME_URL'] || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN'] || ''); - name: Setup proxy cache uses: nv-gha-runners/setup-proxy-cache@main @@ -176,6 +180,7 @@ jobs: ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} + SCCACHE_GHA_USE_PREPROCESSOR_CACHE_MODE=${{ env.SCCACHE_GHA_USE_PREPROCESSOR_CACHE_MODE }} SCCACHE_DIR=/host/${{ env.SCCACHE_DIR }} SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} CIBW_ENVIRONMENT_WINDOWS: > @@ -183,7 +188,7 @@ jobs: CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > - "/host/${{ env.SCCACHE_PATH }}" --show-stats && + "/host/${{ env.SCCACHE_PATH }}" --show-adv-stats && "/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_bindings.json # force the test stage to be run (so that before-test is not skipped) # TODO: we might want to think twice on adding this, it does a lot of @@ -241,6 +246,7 @@ jobs: ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} + SCCACHE_GHA_USE_PREPROCESSOR_CACHE_MODE=${{ env.SCCACHE_GHA_USE_PREPROCESSOR_CACHE_MODE }} SCCACHE_DIR=/host/${{ env.SCCACHE_DIR }} SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} CIBW_ENVIRONMENT_WINDOWS: > @@ -250,7 +256,7 @@ jobs: PIP_FIND_LINKS="$(cygpath -w ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }})" # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > - "/host${{ env.SCCACHE_PATH }}" --show-stats && + "/host${{ env.SCCACHE_PATH }}" --show-adv-stats && "/host${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_core.json # force the test stage to be run (so that before-test is not skipped) # TODO: we might want to think twice on adding this, it does a lot of @@ -429,6 +435,7 @@ jobs: ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} + SCCACHE_GHA_USE_PREPROCESSOR_CACHE_MODE=${{ env.SCCACHE_GHA_USE_PREPROCESSOR_CACHE_MODE }} SCCACHE_DIR=/host/${{ env.SCCACHE_DIR }} SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} CIBW_ENVIRONMENT_WINDOWS: > @@ -438,7 +445,7 @@ jobs: PIP_FIND_LINKS="$(cygpath -w ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }})" # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > - "/host${{ env.SCCACHE_PATH }}" --show-stats && + "/host${{ env.SCCACHE_PATH }}" --show-adv-stats && "/host${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_core_prev.json # force the test stage to be run (so that before-test is not skipped) # TODO: we might want to think twice on adding this, it does a lot of diff --git a/ci/test-matrix.yml b/ci/test-matrix.yml index c4682606073..4bcfcf4b232 100644 --- a/ci/test-matrix.yml +++ b/ci/test-matrix.yml @@ -20,48 +20,48 @@ linux: # linux-64 - { ARCH: 'amd64', PY_VER: '3.10', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'v100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.10', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.10', CUDA_VER: '13.2.0', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.10', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '13.2.0', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'v100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 't4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 't4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } # linux-aarch64 - { ARCH: 'arm64', PY_VER: '3.10', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.10', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.10', CUDA_VER: '13.2.0', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.10', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.12', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.12', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.12', CUDA_VER: '13.2.0', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.12', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.14', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.14', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.14', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.14', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } # special runners - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'h100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'h100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 't4', GPU_COUNT: '2', DRIVER: 'latest' } - - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'h100', GPU_COUNT: '2', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'h100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 't4', GPU_COUNT: '2', DRIVER: 'latest' } + - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'h100', GPU_COUNT: '2', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 't4', GPU_COUNT: '1', DRIVER: 'latest', FLAVOR: 'wsl' } - - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '13.2.0', LOCAL_CTK: '0', GPU: 'rtx4090', GPU_COUNT: '1', DRIVER: 'latest', FLAVOR: 'wsl' } + - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'rtx4090', GPU_COUNT: '1', DRIVER: 'latest', FLAVOR: 'wsl' } nightly: [] windows: @@ -69,20 +69,20 @@ windows: # win-64 - { ARCH: 'amd64', PY_VER: '3.10', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'rtx2080', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'WDDM' } - { ARCH: 'amd64', PY_VER: '3.10', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } - - { ARCH: 'amd64', PY_VER: '3.10', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } + - { ARCH: 'amd64', PY_VER: '3.10', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'v100', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'rtx4090', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'WDDM' } - - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '13.2.0', LOCAL_CTK: '0', GPU: 'rtx4090', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'WDDM' } + - { ARCH: 'amd64', PY_VER: '3.11', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'rtx4090', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'WDDM' } - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } - - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } + - { ARCH: 'amd64', PY_VER: '3.12', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } - - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.2.0', LOCAL_CTK: '0', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } + - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'rtxpro6000', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'v100', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } - - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '13.2.0', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } + - { ARCH: 'amd64', PY_VER: '3.14', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'TCC' } - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } - - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.2.0', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } + - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest', DRIVER_MODE: 'MCDM' } nightly: [] diff --git a/ci/tools/env-vars b/ci/tools/env-vars index bbc3ed66c50..793928babc0 100755 --- a/ci/tools/env-vars +++ b/ci/tools/env-vars @@ -69,7 +69,7 @@ elif [[ "${1}" == "test" ]]; then # We only test compute-sanitizer on python 3.12 arbitrarily; we don't need to use sanitizer on the entire matrix # Only local ctk installs have compute-sanitizer; there is no wheel for it if [[ "${PY_VER}" == "3.12" && "${CUDA_VER}" != "11.8.0" && "${LOCAL_CTK}" == 1 && "${HOST_PLATFORM}" == linux* ]]; then - echo "LATEST_CUDA_VERSION=$(bash .github/workflows/guess_latest.sh $TEST_CUDA_MAJOR)" >> $GITHUB_ENV + echo "LATEST_CUDA_VERSION=$(bash ci/tools/guess_latest.sh $TEST_CUDA_MAJOR)" >> $GITHUB_ENV SETUP_SANITIZER=1 else SETUP_SANITIZER=0 diff --git a/.github/workflows/guess_latest.sh b/ci/tools/guess_latest.sh similarity index 100% rename from .github/workflows/guess_latest.sh rename to ci/tools/guess_latest.sh diff --git a/ci/versions.yml b/ci/versions.yml index 153cc708919..fb749c0718d 100644 --- a/ci/versions.yml +++ b/ci/versions.yml @@ -5,6 +5,6 @@ backport_branch: "12.9.x" # keep in sync with target-branch in .github/dependab cuda: build: - version: "13.2.0" + version: "13.2.1" prev_build: version: "12.9.1" From 67be918f1001556a90e421e71abae4c1bd905557 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Thu, 16 Apr 2026 17:19:43 -0400 Subject: [PATCH 006/244] cuda.bindings: Update generated code (#1927) --- cuda_bindings/cuda/bindings/cufile.pxd | 4 +- cuda_bindings/cuda/bindings/cufile.pyx | 25 +- cuda_bindings/cuda/bindings/cycufile.pxd | 3 +- cuda_bindings/cuda/bindings/cynvml.pxd | 69 +++- cuda_bindings/cuda/bindings/driver.pyx.in | 152 ++++----- cuda_bindings/cuda/bindings/nvml.pxd | 16 +- cuda_bindings/cuda/bindings/nvml.pyx | 353 +++++++++++---------- cuda_bindings/cuda/bindings/runtime.pyx.in | 110 +++---- 8 files changed, 399 insertions(+), 333 deletions(-) diff --git a/cuda_bindings/cuda/bindings/cufile.pxd b/cuda_bindings/cuda/bindings/cufile.pxd index 4af1265c784..7a4a83cd796 100644 --- a/cuda_bindings/cuda/bindings/cufile.pxd +++ b/cuda_bindings/cuda/bindings/cufile.pxd @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE # -# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1568+g289771de9.d20260413. Do not modify it directly. from libc.stdint cimport intptr_t @@ -80,6 +80,6 @@ cpdef stats_reset() cpdef get_stats_l1(intptr_t stats) cpdef get_stats_l2(intptr_t stats) cpdef get_stats_l3(intptr_t stats) -cpdef size_t get_bar_size_in_kb(int gpu_ind_ex) except? 0 +cpdef size_t get_bar_size_in_kb(int gpu_index) except? 0 cpdef set_parameter_posix_pool_slab_array(intptr_t size_values, intptr_t count_values, int len) cpdef get_parameter_posix_pool_slab_array(intptr_t size_values, intptr_t count_values, int len) diff --git a/cuda_bindings/cuda/bindings/cufile.pyx b/cuda_bindings/cuda/bindings/cufile.pyx index 1045abef954..bc68ecca4ac 100644 --- a/cuda_bindings/cuda/bindings/cufile.pyx +++ b/cuda_bindings/cuda/bindings/cufile.pyx @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE # -# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1568+g289771de9.d20260413. Do not modify it directly. cimport cython # NOQA from libc cimport errno @@ -75,7 +75,6 @@ _py_anon_pod1_dtype = _numpy.dtype(( } )) - cdef class _py_anon_pod1: """Empty-initialize an instance of `cuda_bindings_cufile__anon_pod1`. @@ -389,21 +388,17 @@ io_events_dtype = _get_io_events_dtype_offsets() cdef class IOEvents: """Empty-initialize an array of `CUfileIOEvents_t`. - The resulting object is of length `size` and of dtype `io_events_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `CUfileIOEvents_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=io_events_dtype) self._data = arr.view(_numpy.recarray) @@ -728,21 +723,17 @@ per_gpu_stats_dtype = _get_per_gpu_stats_dtype_offsets() cdef class PerGpuStats: """Empty-initialize an array of `CUfilePerGpuStats_t`. - The resulting object is of length `size` and of dtype `per_gpu_stats_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `CUfilePerGpuStats_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=per_gpu_stats_dtype) self._data = arr.view(_numpy.recarray) @@ -1192,21 +1183,17 @@ descr_dtype = _get_descr_dtype_offsets() cdef class Descr: """Empty-initialize an array of `CUfileDescr_t`. - The resulting object is of length `size` and of dtype `descr_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `CUfileDescr_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=descr_dtype) self._data = arr.view(_numpy.recarray) @@ -1349,7 +1336,6 @@ _py_anon_pod2_dtype = _numpy.dtype(( } )) - cdef class _py_anon_pod2: """Empty-initialize an instance of `cuda_bindings_cufile__anon_pod2`. @@ -2144,21 +2130,17 @@ io_params_dtype = _get_io_params_dtype_offsets() cdef class IOParams: """Empty-initialize an array of `CUfileIOParams_t`. - The resulting object is of length `size` and of dtype `io_params_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `CUfileIOParams_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=io_params_dtype) self._data = arr.view(_numpy.recarray) @@ -2643,7 +2625,6 @@ cdef class StatsLevel3: return obj - ############################################################################### # Enum ############################################################################### @@ -3252,10 +3233,10 @@ cpdef get_stats_l3(intptr_t stats): check_status(__status__) -cpdef size_t get_bar_size_in_kb(int gpu_ind_ex) except? 0: +cpdef size_t get_bar_size_in_kb(int gpu_index) except? 0: cdef size_t bar_size with nogil: - __status__ = cuFileGetBARSizeInKB(gpu_ind_ex, &bar_size) + __status__ = cuFileGetBARSizeInKB(gpu_index, &bar_size) check_status(__status__) return bar_size diff --git a/cuda_bindings/cuda/bindings/cycufile.pxd b/cuda_bindings/cuda/bindings/cycufile.pxd index bcc2e7596d8..bdbfc8e9a46 100644 --- a/cuda_bindings/cuda/bindings/cycufile.pxd +++ b/cuda_bindings/cuda/bindings/cycufile.pxd @@ -197,9 +197,11 @@ cdef extern from '': cdef extern from '': ctypedef void* CUfileHandle_t 'CUfileHandle_t' + cdef extern from '': ctypedef void* CUfileBatchHandle_t 'CUfileBatchHandle_t' + cdef extern from '': ctypedef struct CUfileError_t 'CUfileError_t': CUfileOpError err @@ -367,7 +369,6 @@ cdef extern from '': CUfilePerGpuStats_t per_gpu_stats[16] - cdef extern from *: """ // This is the missing piece we need to supply to help Cython & C++ compilers. diff --git a/cuda_bindings/cuda/bindings/cynvml.pxd b/cuda_bindings/cuda/bindings/cynvml.pxd index db04f04d760..1f59e6d522a 100644 --- a/cuda_bindings/cuda/bindings/cynvml.pxd +++ b/cuda_bindings/cuda/bindings/cynvml.pxd @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE # -# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1568+g289771de9.d20260413. Do not modify it directly. from libc.stdint cimport int64_t @@ -817,12 +817,19 @@ ctypedef struct nvmlPlatformInfo_v2_t 'nvmlPlatformInfo_v2_t': unsigned char moduleId ctypedef unsigned int nvmlDeviceArchitecture_t 'nvmlDeviceArchitecture_t' + ctypedef unsigned int nvmlBusType_t 'nvmlBusType_t' + ctypedef unsigned int nvmlFanControlPolicy_t 'nvmlFanControlPolicy_t' + ctypedef unsigned int nvmlPowerSource_t 'nvmlPowerSource_t' + ctypedef unsigned char nvmlPowerScopeType_t 'nvmlPowerScopeType_t' + ctypedef unsigned int nvmlVgpuTypeId_t 'nvmlVgpuTypeId_t' + ctypedef unsigned int nvmlVgpuInstance_t 'nvmlVgpuInstance_t' + ctypedef struct nvmlVgpuHeterogeneousMode_v1_t 'nvmlVgpuHeterogeneousMode_v1_t': unsigned int version unsigned int mode @@ -862,11 +869,13 @@ ctypedef struct nvmlConfComputeGetKeyRotationThresholdInfo_v1_t 'nvmlConfCompute unsigned long long attackerAdvantage ctypedef unsigned char nvmlGpuFabricState_t 'nvmlGpuFabricState_t' + ctypedef struct nvmlSystemDriverBranchInfo_v1_t 'nvmlSystemDriverBranchInfo_v1_t': unsigned int version char branch[80] ctypedef unsigned int nvmlAffinityScope_t 'nvmlAffinityScope_t' + ctypedef struct nvmlTemperature_v1_t 'nvmlTemperature_v1_t': unsigned int version nvmlTemperatureSensors_t sensorType @@ -915,12 +924,19 @@ ctypedef struct nvmlPdi_v1_t 'nvmlPdi_v1_t': unsigned long long value ctypedef void* nvmlDevice_t 'nvmlDevice_t' + ctypedef void* nvmlGpuInstance_t 'nvmlGpuInstance_t' + ctypedef void* nvmlUnit_t 'nvmlUnit_t' + ctypedef void* nvmlEventSet_t 'nvmlEventSet_t' + ctypedef void* nvmlSystemEventSet_t 'nvmlSystemEventSet_t' + ctypedef void* nvmlComputeInstance_t 'nvmlComputeInstance_t' + ctypedef void* nvmlGpmSample_t 'nvmlGpmSample_t' + ctypedef struct nvmlPciInfo_t 'nvmlPciInfo_t': char busIdLegacy[16] unsigned int domain @@ -1383,15 +1399,25 @@ ctypedef struct nvmlVgpuSchedulerState_v2_t 'nvmlVgpuSchedulerState_v2_t': unsigned int frequency ctypedef nvmlPciInfoExt_v1_t nvmlPciInfoExt_t 'nvmlPciInfoExt_t' + ctypedef nvmlCoolerInfo_v1_t nvmlCoolerInfo_t 'nvmlCoolerInfo_t' + ctypedef nvmlDramEncryptionInfo_v1_t nvmlDramEncryptionInfo_t 'nvmlDramEncryptionInfo_t' + ctypedef nvmlMarginTemperature_v1_t nvmlMarginTemperature_t 'nvmlMarginTemperature_t' + ctypedef nvmlClockOffset_v1_t nvmlClockOffset_t 'nvmlClockOffset_t' + ctypedef nvmlFanSpeedInfo_v1_t nvmlFanSpeedInfo_t 'nvmlFanSpeedInfo_t' + ctypedef nvmlDevicePerfModes_v1_t nvmlDevicePerfModes_t 'nvmlDevicePerfModes_t' + ctypedef nvmlDeviceCurrentClockFreqs_v1_t nvmlDeviceCurrentClockFreqs_t 'nvmlDeviceCurrentClockFreqs_t' + ctypedef nvmlEccSramErrorStatus_v1_t nvmlEccSramErrorStatus_t 'nvmlEccSramErrorStatus_t' + ctypedef nvmlPlatformInfo_v2_t nvmlPlatformInfo_t 'nvmlPlatformInfo_t' + ctypedef struct nvmlPowerValue_v2_t 'nvmlPowerValue_v2_t': unsigned int version nvmlPowerScopeType_t powerScope @@ -1466,13 +1492,21 @@ ctypedef struct nvmlFBCSessionInfo_t 'nvmlFBCSessionInfo_t': unsigned int averageLatency ctypedef nvmlVgpuHeterogeneousMode_v1_t nvmlVgpuHeterogeneousMode_t 'nvmlVgpuHeterogeneousMode_t' + ctypedef nvmlVgpuPlacementId_v1_t nvmlVgpuPlacementId_t 'nvmlVgpuPlacementId_t' + ctypedef nvmlVgpuPlacementList_v2_t nvmlVgpuPlacementList_t 'nvmlVgpuPlacementList_t' + ctypedef nvmlVgpuTypeBar1Info_v1_t nvmlVgpuTypeBar1Info_t 'nvmlVgpuTypeBar1Info_t' + ctypedef nvmlVgpuRuntimeState_v1_t nvmlVgpuRuntimeState_t 'nvmlVgpuRuntimeState_t' + ctypedef nvmlSystemConfComputeSettings_v1_t nvmlSystemConfComputeSettings_t 'nvmlSystemConfComputeSettings_t' + ctypedef nvmlConfComputeSetKeyRotationThresholdInfo_v1_t nvmlConfComputeSetKeyRotationThresholdInfo_t 'nvmlConfComputeSetKeyRotationThresholdInfo_t' + ctypedef nvmlConfComputeGetKeyRotationThresholdInfo_v1_t nvmlConfComputeGetKeyRotationThresholdInfo_t 'nvmlConfComputeGetKeyRotationThresholdInfo_t' + ctypedef struct nvmlGpuFabricInfo_t 'nvmlGpuFabricInfo_t': unsigned char clusterUuid[16] nvmlReturn_t status @@ -1497,16 +1531,27 @@ ctypedef struct nvmlGpuFabricInfo_v3_t 'nvmlGpuFabricInfo_v3_t': unsigned char healthSummary ctypedef nvmlSystemDriverBranchInfo_v1_t nvmlSystemDriverBranchInfo_t 'nvmlSystemDriverBranchInfo_t' + ctypedef nvmlTemperature_v1_t nvmlTemperature_t 'nvmlTemperature_t' + ctypedef nvmlNvlinkSupportedBwModes_v1_t nvmlNvlinkSupportedBwModes_t 'nvmlNvlinkSupportedBwModes_t' + ctypedef nvmlNvlinkGetBwMode_v1_t nvmlNvlinkGetBwMode_t 'nvmlNvlinkGetBwMode_t' + ctypedef nvmlNvlinkSetBwMode_v1_t nvmlNvlinkSetBwMode_t 'nvmlNvlinkSetBwMode_t' + ctypedef nvmlDeviceCapabilities_v1_t nvmlDeviceCapabilities_t 'nvmlDeviceCapabilities_t' + ctypedef nvmlPowerSmoothingProfile_v1_t nvmlPowerSmoothingProfile_t 'nvmlPowerSmoothingProfile_t' + ctypedef nvmlPowerSmoothingState_v1_t nvmlPowerSmoothingState_t 'nvmlPowerSmoothingState_t' + ctypedef nvmlDeviceAddressingMode_v1_t nvmlDeviceAddressingMode_t 'nvmlDeviceAddressingMode_t' + ctypedef nvmlRepairStatus_v1_t nvmlRepairStatus_t 'nvmlRepairStatus_t' + ctypedef nvmlPdi_v1_t nvmlPdi_t 'nvmlPdi_t' + ctypedef struct nvmlEventData_t 'nvmlEventData_t': nvmlDevice_t device unsigned long long eventType @@ -1706,8 +1751,11 @@ ctypedef struct nvmlVgpuSchedulerLogInfo_v2_t 'nvmlVgpuSchedulerLogInfo_v2_t': nvmlVgpuSchedulerLogEntry_v2_t logEntries[200] ctypedef nvmlVgpuTypeIdInfo_v1_t nvmlVgpuTypeIdInfo_t 'nvmlVgpuTypeIdInfo_t' + ctypedef nvmlVgpuTypeMaxInstance_v1_t nvmlVgpuTypeMaxInstance_t 'nvmlVgpuTypeMaxInstance_t' + ctypedef nvmlVgpuCreatablePlacementInfo_v1_t nvmlVgpuCreatablePlacementInfo_t 'nvmlVgpuCreatablePlacementInfo_t' + ctypedef struct nvmlVgpuProcessesUtilizationInfo_v1_t 'nvmlVgpuProcessesUtilizationInfo_v1_t': unsigned int version unsigned int vgpuProcessCount @@ -1715,11 +1763,17 @@ ctypedef struct nvmlVgpuProcessesUtilizationInfo_v1_t 'nvmlVgpuProcessesUtilizat nvmlVgpuProcessUtilizationInfo_v1_t* vgpuProcUtilArray ctypedef nvmlActiveVgpuInstanceInfo_v1_t nvmlActiveVgpuInstanceInfo_t 'nvmlActiveVgpuInstanceInfo_t' + ctypedef nvmlGpuFabricInfo_v3_t nvmlGpuFabricInfoV_t 'nvmlGpuFabricInfoV_t' + ctypedef nvmlSystemEventSetCreateRequest_v1_t nvmlSystemEventSetCreateRequest_t 'nvmlSystemEventSetCreateRequest_t' + ctypedef nvmlSystemEventSetFreeRequest_v1_t nvmlSystemEventSetFreeRequest_t 'nvmlSystemEventSetFreeRequest_t' + ctypedef nvmlSystemRegisterEventRequest_v1_t nvmlSystemRegisterEventRequest_t 'nvmlSystemRegisterEventRequest_t' + ctypedef nvmlProcessDetailList_v1_t nvmlProcessDetailList_t 'nvmlProcessDetailList_t' + ctypedef struct nvmlVgpuInstancesUtilizationInfo_v1_t 'nvmlVgpuInstancesUtilizationInfo_v1_t': unsigned int version nvmlValueType_t sampleValType @@ -1733,7 +1787,9 @@ ctypedef struct nvmlPRMCounter_v1_t 'nvmlPRMCounter_v1_t': nvmlPRMCounterValue_v1_t counterValue ctypedef nvmlUUID_v1_t nvmlUUID_t 'nvmlUUID_t' + ctypedef nvmlProcessesUtilizationInfo_v1_t nvmlProcessesUtilizationInfo_t 'nvmlProcessesUtilizationInfo_t' + ctypedef struct nvmlVgpuSchedulerLog_t 'nvmlVgpuSchedulerLog_t': unsigned int engineId unsigned int schedulerPolicy @@ -1781,6 +1837,7 @@ ctypedef struct nvmlGridLicensableFeatures_t 'nvmlGridLicensableFeatures_t': nvmlGridLicensableFeature_t gridLicensableFeatures[3] ctypedef nvmlSystemEventSetWaitRequest_v1_t nvmlSystemEventSetWaitRequest_t 'nvmlSystemEventSetWaitRequest_t' + ctypedef struct nvmlGpmMetricsGet_t 'nvmlGpmMetricsGet_t': unsigned int version unsigned int numMetrics @@ -1789,29 +1846,39 @@ ctypedef struct nvmlGpmMetricsGet_t 'nvmlGpmMetricsGet_t': nvmlGpmMetric_t metrics[333] ctypedef nvmlWorkloadPowerProfileInfo_v1_t nvmlWorkloadPowerProfileInfo_t 'nvmlWorkloadPowerProfileInfo_t' + ctypedef nvmlWorkloadPowerProfileCurrentProfiles_v1_t nvmlWorkloadPowerProfileCurrentProfiles_t 'nvmlWorkloadPowerProfileCurrentProfiles_t' + ctypedef nvmlWorkloadPowerProfileRequestedProfiles_v1_t nvmlWorkloadPowerProfileRequestedProfiles_t 'nvmlWorkloadPowerProfileRequestedProfiles_t' + ctypedef nvmlEccSramUniqueUncorrectedErrorCounts_v1_t nvmlEccSramUniqueUncorrectedErrorCounts_t 'nvmlEccSramUniqueUncorrectedErrorCounts_t' + ctypedef struct nvmlNvLinkInfo_v2_t 'nvmlNvLinkInfo_v2_t': unsigned int version unsigned int isNvleEnabled nvmlNvlinkFirmwareInfo_t firmwareInfo ctypedef nvmlVgpuProcessesUtilizationInfo_v1_t nvmlVgpuProcessesUtilizationInfo_t 'nvmlVgpuProcessesUtilizationInfo_t' + ctypedef nvmlVgpuInstancesUtilizationInfo_v1_t nvmlVgpuInstancesUtilizationInfo_t 'nvmlVgpuInstancesUtilizationInfo_t' + ctypedef struct nvmlPRMCounterList_v1_t 'nvmlPRMCounterList_v1_t': unsigned int numCounters nvmlPRMCounter_v1_t* counters ctypedef nvmlVgpuSchedulerStateInfo_v1_t nvmlVgpuSchedulerStateInfo_t 'nvmlVgpuSchedulerStateInfo_t' + ctypedef nvmlVgpuSchedulerLogInfo_v1_t nvmlVgpuSchedulerLogInfo_t 'nvmlVgpuSchedulerLogInfo_t' + ctypedef nvmlVgpuSchedulerState_v1_t nvmlVgpuSchedulerState_t 'nvmlVgpuSchedulerState_t' + ctypedef struct nvmlWorkloadPowerProfileProfilesInfo_v1_t 'nvmlWorkloadPowerProfileProfilesInfo_v1_t': unsigned int version nvmlMask255_t perfProfilesMask nvmlWorkloadPowerProfileInfo_t perfProfile[255] ctypedef nvmlNvLinkInfo_v2_t nvmlNvLinkInfo_t 'nvmlNvLinkInfo_t' + ctypedef nvmlWorkloadPowerProfileProfilesInfo_v1_t nvmlWorkloadPowerProfileProfilesInfo_t 'nvmlWorkloadPowerProfileProfilesInfo_t' diff --git a/cuda_bindings/cuda/bindings/driver.pyx.in b/cuda_bindings/cuda/bindings/driver.pyx.in index 2c02494148b..d97fba967a6 100644 --- a/cuda_bindings/cuda/bindings/driver.pyx.in +++ b/cuda_bindings/cuda/bindings/driver.pyx.in @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE -# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1568+g289771de9.d20260413. Do not modify it directly. from typing import Any, Optional import cython import ctypes @@ -27562,7 +27562,7 @@ def cuCtxCreate(ctxCreateParams : Optional[CUctxCreateParams], unsigned int flag pdev = int(CUdevice(dev)) cydev = pdev cdef CUcontext pctx = CUcontext() - cdef cydriver.CUctxCreateParams* cyctxCreateParams_ptr = ctxCreateParams._pvt_ptr if ctxCreateParams is not None else NULL + cdef cydriver.CUctxCreateParams* cyctxCreateParams_ptr = ctxCreateParams._pvt_ptr if ctxCreateParams is not None else NULL with nogil: err = cydriver.cuCtxCreate(pctx._pvt_ptr, cyctxCreateParams_ptr, flags, cydev) if err != cydriver.CUDA_SUCCESS: @@ -32636,7 +32636,7 @@ def cuMemcpy2D(pCopy : Optional[CUDA_MEMCPY2D]): -------- :py:obj:`~.cuArray3DCreate`, :py:obj:`~.cuArray3DGetDescriptor`, :py:obj:`~.cuArrayCreate`, :py:obj:`~.cuArrayDestroy`, :py:obj:`~.cuArrayGetDescriptor`, :py:obj:`~.cuMemAlloc`, :py:obj:`~.cuMemAllocHost`, :py:obj:`~.cuMemAllocPitch`, :py:obj:`~.cuMemcpy2DAsync`, :py:obj:`~.cuMemcpy2DUnaligned`, :py:obj:`~.cuMemcpy3D`, :py:obj:`~.cuMemcpy3DAsync`, :py:obj:`~.cuMemcpyAtoA`, :py:obj:`~.cuMemcpyAtoD`, :py:obj:`~.cuMemcpyAtoH`, :py:obj:`~.cuMemcpyAtoHAsync`, :py:obj:`~.cuMemcpyDtoA`, :py:obj:`~.cuMemcpyDtoD`, :py:obj:`~.cuMemcpyDtoDAsync`, :py:obj:`~.cuMemcpyDtoH`, :py:obj:`~.cuMemcpyDtoHAsync`, :py:obj:`~.cuMemcpyHtoA`, :py:obj:`~.cuMemcpyHtoAAsync`, :py:obj:`~.cuMemcpyHtoD`, :py:obj:`~.cuMemcpyHtoDAsync`, :py:obj:`~.cuMemFree`, :py:obj:`~.cuMemFreeHost`, :py:obj:`~.cuMemGetAddressRange`, :py:obj:`~.cuMemGetInfo`, :py:obj:`~.cuMemHostAlloc`, :py:obj:`~.cuMemHostGetDevicePointer`, :py:obj:`~.cuMemsetD2D8`, :py:obj:`~.cuMemsetD2D16`, :py:obj:`~.cuMemsetD2D32`, :py:obj:`~.cuMemsetD8`, :py:obj:`~.cuMemsetD16`, :py:obj:`~.cuMemsetD32`, :py:obj:`~.cudaMemcpy2D`, :py:obj:`~.cudaMemcpy2DToArray`, :py:obj:`~.cudaMemcpy2DFromArray` """ - cdef cydriver.CUDA_MEMCPY2D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL + cdef cydriver.CUDA_MEMCPY2D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL with nogil: err = cydriver.cuMemcpy2D(cypCopy_ptr) return (_CUresult(err),) @@ -32763,7 +32763,7 @@ def cuMemcpy2DUnaligned(pCopy : Optional[CUDA_MEMCPY2D]): -------- :py:obj:`~.cuArray3DCreate`, :py:obj:`~.cuArray3DGetDescriptor`, :py:obj:`~.cuArrayCreate`, :py:obj:`~.cuArrayDestroy`, :py:obj:`~.cuArrayGetDescriptor`, :py:obj:`~.cuMemAlloc`, :py:obj:`~.cuMemAllocHost`, :py:obj:`~.cuMemAllocPitch`, :py:obj:`~.cuMemcpy2D`, :py:obj:`~.cuMemcpy2DAsync`, :py:obj:`~.cuMemcpy3D`, :py:obj:`~.cuMemcpy3DAsync`, :py:obj:`~.cuMemcpyAtoA`, :py:obj:`~.cuMemcpyAtoD`, :py:obj:`~.cuMemcpyAtoH`, :py:obj:`~.cuMemcpyAtoHAsync`, :py:obj:`~.cuMemcpyDtoA`, :py:obj:`~.cuMemcpyDtoD`, :py:obj:`~.cuMemcpyDtoDAsync`, :py:obj:`~.cuMemcpyDtoH`, :py:obj:`~.cuMemcpyDtoHAsync`, :py:obj:`~.cuMemcpyHtoA`, :py:obj:`~.cuMemcpyHtoAAsync`, :py:obj:`~.cuMemcpyHtoD`, :py:obj:`~.cuMemcpyHtoDAsync`, :py:obj:`~.cuMemFree`, :py:obj:`~.cuMemFreeHost`, :py:obj:`~.cuMemGetAddressRange`, :py:obj:`~.cuMemGetInfo`, :py:obj:`~.cuMemHostAlloc`, :py:obj:`~.cuMemHostGetDevicePointer`, :py:obj:`~.cuMemsetD2D8`, :py:obj:`~.cuMemsetD2D16`, :py:obj:`~.cuMemsetD2D32`, :py:obj:`~.cuMemsetD8`, :py:obj:`~.cuMemsetD16`, :py:obj:`~.cuMemsetD32`, :py:obj:`~.cudaMemcpy2D`, :py:obj:`~.cudaMemcpy2DToArray`, :py:obj:`~.cudaMemcpy2DFromArray` """ - cdef cydriver.CUDA_MEMCPY2D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL + cdef cydriver.CUDA_MEMCPY2D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL with nogil: err = cydriver.cuMemcpy2DUnaligned(cypCopy_ptr) return (_CUresult(err),) @@ -32893,7 +32893,7 @@ def cuMemcpy3D(pCopy : Optional[CUDA_MEMCPY3D]): -------- :py:obj:`~.cuArray3DCreate`, :py:obj:`~.cuArray3DGetDescriptor`, :py:obj:`~.cuArrayCreate`, :py:obj:`~.cuArrayDestroy`, :py:obj:`~.cuArrayGetDescriptor`, :py:obj:`~.cuMemAlloc`, :py:obj:`~.cuMemAllocHost`, :py:obj:`~.cuMemAllocPitch`, :py:obj:`~.cuMemcpy2D`, :py:obj:`~.cuMemcpy2DAsync`, :py:obj:`~.cuMemcpy2DUnaligned`, :py:obj:`~.cuMemcpy3DAsync`, :py:obj:`~.cuMemcpyAtoA`, :py:obj:`~.cuMemcpyAtoD`, :py:obj:`~.cuMemcpyAtoH`, :py:obj:`~.cuMemcpyAtoHAsync`, :py:obj:`~.cuMemcpyDtoA`, :py:obj:`~.cuMemcpyDtoD`, :py:obj:`~.cuMemcpyDtoDAsync`, :py:obj:`~.cuMemcpyDtoH`, :py:obj:`~.cuMemcpyDtoHAsync`, :py:obj:`~.cuMemcpyHtoA`, :py:obj:`~.cuMemcpyHtoAAsync`, :py:obj:`~.cuMemcpyHtoD`, :py:obj:`~.cuMemcpyHtoDAsync`, :py:obj:`~.cuMemFree`, :py:obj:`~.cuMemFreeHost`, :py:obj:`~.cuMemGetAddressRange`, :py:obj:`~.cuMemGetInfo`, :py:obj:`~.cuMemHostAlloc`, :py:obj:`~.cuMemHostGetDevicePointer`, :py:obj:`~.cuMemsetD2D8`, :py:obj:`~.cuMemsetD2D16`, :py:obj:`~.cuMemsetD2D32`, :py:obj:`~.cuMemsetD8`, :py:obj:`~.cuMemsetD16`, :py:obj:`~.cuMemsetD32`, :py:obj:`~.cudaMemcpy3D` """ - cdef cydriver.CUDA_MEMCPY3D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL + cdef cydriver.CUDA_MEMCPY3D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL with nogil: err = cydriver.cuMemcpy3D(cypCopy_ptr) return (_CUresult(err),) @@ -32923,7 +32923,7 @@ def cuMemcpy3DPeer(pCopy : Optional[CUDA_MEMCPY3D_PEER]): -------- :py:obj:`~.cuMemcpyDtoD`, :py:obj:`~.cuMemcpyPeer`, :py:obj:`~.cuMemcpyDtoDAsync`, :py:obj:`~.cuMemcpyPeerAsync`, :py:obj:`~.cuMemcpy3DPeerAsync`, :py:obj:`~.cudaMemcpy3DPeer` """ - cdef cydriver.CUDA_MEMCPY3D_PEER* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL + cdef cydriver.CUDA_MEMCPY3D_PEER* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL with nogil: err = cydriver.cuMemcpy3DPeer(cypCopy_ptr) return (_CUresult(err),) @@ -33491,7 +33491,7 @@ def cuMemcpy2DAsync(pCopy : Optional[CUDA_MEMCPY2D], hStream): else: phStream = int(CUstream(hStream)) cyhStream = phStream - cdef cydriver.CUDA_MEMCPY2D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL + cdef cydriver.CUDA_MEMCPY2D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL with nogil: err = cydriver.cuMemcpy2DAsync(cypCopy_ptr, cyhStream) return (_CUresult(err),) @@ -33631,7 +33631,7 @@ def cuMemcpy3DAsync(pCopy : Optional[CUDA_MEMCPY3D], hStream): else: phStream = int(CUstream(hStream)) cyhStream = phStream - cdef cydriver.CUDA_MEMCPY3D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL + cdef cydriver.CUDA_MEMCPY3D* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL with nogil: err = cydriver.cuMemcpy3DAsync(cypCopy_ptr, cyhStream) return (_CUresult(err),) @@ -33671,7 +33671,7 @@ def cuMemcpy3DPeerAsync(pCopy : Optional[CUDA_MEMCPY3D_PEER], hStream): else: phStream = int(CUstream(hStream)) cyhStream = phStream - cdef cydriver.CUDA_MEMCPY3D_PEER* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL + cdef cydriver.CUDA_MEMCPY3D_PEER* cypCopy_ptr = pCopy._pvt_ptr if pCopy is not None else NULL with nogil: err = cydriver.cuMemcpy3DPeerAsync(cypCopy_ptr, cyhStream) return (_CUresult(err),) @@ -34044,7 +34044,7 @@ def cuMemcpyWithAttributesAsync(dst, src, size_t size, attr : Optional[CUmemcpyA else: pdst = int(CUdeviceptr(dst)) cydst = pdst - cdef cydriver.CUmemcpyAttributes* cyattr_ptr = attr._pvt_ptr if attr is not None else NULL + cdef cydriver.CUmemcpyAttributes* cyattr_ptr = attr._pvt_ptr if attr is not None else NULL with nogil: err = cydriver.cuMemcpyWithAttributesAsync(cydst, cysrc, size, cyattr_ptr, cyhStream) return (_CUresult(err),) @@ -34092,7 +34092,7 @@ def cuMemcpy3DWithAttributesAsync(op : Optional[CUDA_MEMCPY3D_BATCH_OP], unsigne else: phStream = int(CUstream(hStream)) cyhStream = phStream - cdef cydriver.CUDA_MEMCPY3D_BATCH_OP* cyop_ptr = op._pvt_ptr if op is not None else NULL + cdef cydriver.CUDA_MEMCPY3D_BATCH_OP* cyop_ptr = op._pvt_ptr if op is not None else NULL with nogil: err = cydriver.cuMemcpy3DWithAttributesAsync(cyop_ptr, flags, cyhStream) return (_CUresult(err),) @@ -34745,7 +34745,7 @@ def cuArrayCreate(pAllocateArray : Optional[CUDA_ARRAY_DESCRIPTOR]): :py:obj:`~.cuArray3DCreate`, :py:obj:`~.cuArray3DGetDescriptor`, :py:obj:`~.cuArrayDestroy`, :py:obj:`~.cuArrayGetDescriptor`, :py:obj:`~.cuMemAlloc`, :py:obj:`~.cuMemAllocHost`, :py:obj:`~.cuMemAllocPitch`, :py:obj:`~.cuMemcpy2D`, :py:obj:`~.cuMemcpy2DAsync`, :py:obj:`~.cuMemcpy2DUnaligned`, :py:obj:`~.cuMemcpy3D`, :py:obj:`~.cuMemcpy3DAsync`, :py:obj:`~.cuMemcpyAtoA`, :py:obj:`~.cuMemcpyAtoD`, :py:obj:`~.cuMemcpyAtoH`, :py:obj:`~.cuMemcpyAtoHAsync`, :py:obj:`~.cuMemcpyDtoA`, :py:obj:`~.cuMemcpyDtoD`, :py:obj:`~.cuMemcpyDtoDAsync`, :py:obj:`~.cuMemcpyDtoH`, :py:obj:`~.cuMemcpyDtoHAsync`, :py:obj:`~.cuMemcpyHtoA`, :py:obj:`~.cuMemcpyHtoAAsync`, :py:obj:`~.cuMemcpyHtoD`, :py:obj:`~.cuMemcpyHtoDAsync`, :py:obj:`~.cuMemFree`, :py:obj:`~.cuMemFreeHost`, :py:obj:`~.cuMemGetAddressRange`, :py:obj:`~.cuMemGetInfo`, :py:obj:`~.cuMemHostAlloc`, :py:obj:`~.cuMemHostGetDevicePointer`, :py:obj:`~.cuMemsetD2D8`, :py:obj:`~.cuMemsetD2D16`, :py:obj:`~.cuMemsetD2D32`, :py:obj:`~.cuMemsetD8`, :py:obj:`~.cuMemsetD16`, :py:obj:`~.cuMemsetD32`, :py:obj:`~.cudaMallocArray` """ cdef CUarray pHandle = CUarray() - cdef cydriver.CUDA_ARRAY_DESCRIPTOR* cypAllocateArray_ptr = pAllocateArray._pvt_ptr if pAllocateArray is not None else NULL + cdef cydriver.CUDA_ARRAY_DESCRIPTOR* cypAllocateArray_ptr = pAllocateArray._pvt_ptr if pAllocateArray is not None else NULL with nogil: err = cydriver.cuArrayCreate(pHandle._pvt_ptr, cypAllocateArray_ptr) if err != cydriver.CUDA_SUCCESS: @@ -35245,7 +35245,7 @@ def cuArray3DCreate(pAllocateArray : Optional[CUDA_ARRAY3D_DESCRIPTOR]): :py:obj:`~.cuArray3DGetDescriptor`, :py:obj:`~.cuArrayCreate`, :py:obj:`~.cuArrayDestroy`, :py:obj:`~.cuArrayGetDescriptor`, :py:obj:`~.cuMemAlloc`, :py:obj:`~.cuMemAllocHost`, :py:obj:`~.cuMemAllocPitch`, :py:obj:`~.cuMemcpy2D`, :py:obj:`~.cuMemcpy2DAsync`, :py:obj:`~.cuMemcpy2DUnaligned`, :py:obj:`~.cuMemcpy3D`, :py:obj:`~.cuMemcpy3DAsync`, :py:obj:`~.cuMemcpyAtoA`, :py:obj:`~.cuMemcpyAtoD`, :py:obj:`~.cuMemcpyAtoH`, :py:obj:`~.cuMemcpyAtoHAsync`, :py:obj:`~.cuMemcpyDtoA`, :py:obj:`~.cuMemcpyDtoD`, :py:obj:`~.cuMemcpyDtoDAsync`, :py:obj:`~.cuMemcpyDtoH`, :py:obj:`~.cuMemcpyDtoHAsync`, :py:obj:`~.cuMemcpyHtoA`, :py:obj:`~.cuMemcpyHtoAAsync`, :py:obj:`~.cuMemcpyHtoD`, :py:obj:`~.cuMemcpyHtoDAsync`, :py:obj:`~.cuMemFree`, :py:obj:`~.cuMemFreeHost`, :py:obj:`~.cuMemGetAddressRange`, :py:obj:`~.cuMemGetInfo`, :py:obj:`~.cuMemHostAlloc`, :py:obj:`~.cuMemHostGetDevicePointer`, :py:obj:`~.cuMemsetD2D8`, :py:obj:`~.cuMemsetD2D16`, :py:obj:`~.cuMemsetD2D32`, :py:obj:`~.cuMemsetD8`, :py:obj:`~.cuMemsetD16`, :py:obj:`~.cuMemsetD32`, :py:obj:`~.cudaMalloc3DArray` """ cdef CUarray pHandle = CUarray() - cdef cydriver.CUDA_ARRAY3D_DESCRIPTOR* cypAllocateArray_ptr = pAllocateArray._pvt_ptr if pAllocateArray is not None else NULL + cdef cydriver.CUDA_ARRAY3D_DESCRIPTOR* cypAllocateArray_ptr = pAllocateArray._pvt_ptr if pAllocateArray is not None else NULL with nogil: err = cydriver.cuArray3DCreate(pHandle._pvt_ptr, cypAllocateArray_ptr) if err != cydriver.CUDA_SUCCESS: @@ -35415,7 +35415,7 @@ def cuMipmappedArrayCreate(pMipmappedArrayDesc : Optional[CUDA_ARRAY3D_DESCRIPTO :py:obj:`~.cuMipmappedArrayDestroy`, :py:obj:`~.cuMipmappedArrayGetLevel`, :py:obj:`~.cuArrayCreate`, :py:obj:`~.cudaMallocMipmappedArray` """ cdef CUmipmappedArray pHandle = CUmipmappedArray() - cdef cydriver.CUDA_ARRAY3D_DESCRIPTOR* cypMipmappedArrayDesc_ptr = pMipmappedArrayDesc._pvt_ptr if pMipmappedArrayDesc is not None else NULL + cdef cydriver.CUDA_ARRAY3D_DESCRIPTOR* cypMipmappedArrayDesc_ptr = pMipmappedArrayDesc._pvt_ptr if pMipmappedArrayDesc is not None else NULL with nogil: err = cydriver.cuMipmappedArrayCreate(pHandle._pvt_ptr, cypMipmappedArrayDesc_ptr, numMipmapLevels) if err != cydriver.CUDA_SUCCESS: @@ -35662,7 +35662,7 @@ def cuMemBatchDecompressAsync(paramsArray : Optional[CUmemDecompressParams], siz else: pstream = int(CUstream(stream)) cystream = pstream - cdef cydriver.CUmemDecompressParams* cyparamsArray_ptr = paramsArray._pvt_ptr if paramsArray is not None else NULL + cdef cydriver.CUmemDecompressParams* cyparamsArray_ptr = paramsArray._pvt_ptr if paramsArray is not None else NULL cdef size_t errorIndex = 0 with nogil: err = cydriver.cuMemBatchDecompressAsync(cyparamsArray_ptr, count, flags, &errorIndex, cystream) @@ -35848,7 +35848,7 @@ def cuMemCreate(size_t size, prop : Optional[CUmemAllocationProp], unsigned long :py:obj:`~.cuMemRelease`, :py:obj:`~.cuMemExportToShareableHandle`, :py:obj:`~.cuMemImportFromShareableHandle` """ cdef CUmemGenericAllocationHandle handle = CUmemGenericAllocationHandle() - cdef cydriver.CUmemAllocationProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL + cdef cydriver.CUmemAllocationProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL with nogil: err = cydriver.cuMemCreate(handle._pvt_ptr, size, cyprop_ptr, flags) if err != cydriver.CUDA_SUCCESS: @@ -36310,7 +36310,7 @@ def cuMemGetAccess(location : Optional[CUmemLocation], ptr): pptr = int(CUdeviceptr(ptr)) cyptr = pptr cdef unsigned long long flags = 0 - cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL with nogil: err = cydriver.cuMemGetAccess(&flags, cylocation_ptr, cyptr) if err != cydriver.CUDA_SUCCESS: @@ -36459,7 +36459,7 @@ def cuMemGetAllocationGranularity(prop : Optional[CUmemAllocationProp], option n :py:obj:`~.cuMemCreate`, :py:obj:`~.cuMemMap` """ cdef size_t granularity = 0 - cdef cydriver.CUmemAllocationProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL + cdef cydriver.CUmemAllocationProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL cdef cydriver.CUmemAllocationGranularity_flags cyoption = int(option) with nogil: err = cydriver.cuMemGetAllocationGranularity(&granularity, cyprop_ptr, cyoption) @@ -36976,7 +36976,7 @@ def cuMemPoolGetAccess(memPool, location : Optional[CUmemLocation]): pmemPool = int(CUmemoryPool(memPool)) cymemPool = pmemPool cdef cydriver.CUmemAccess_flags flags - cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL with nogil: err = cydriver.cuMemPoolGetAccess(&flags, cymemPool, cylocation_ptr) if err != cydriver.CUDA_SUCCESS: @@ -37075,7 +37075,7 @@ def cuMemPoolCreate(poolProps : Optional[CUmemPoolProps]): Specifying CU_MEM_HANDLE_TYPE_NONE creates a memory pool that will not support IPC. """ cdef CUmemoryPool pool = CUmemoryPool() - cdef cydriver.CUmemPoolProps* cypoolProps_ptr = poolProps._pvt_ptr if poolProps is not None else NULL + cdef cydriver.CUmemPoolProps* cypoolProps_ptr = poolProps._pvt_ptr if poolProps is not None else NULL with nogil: err = cydriver.cuMemPoolCreate(pool._pvt_ptr, cypoolProps_ptr) if err != cydriver.CUDA_SUCCESS: @@ -37165,7 +37165,7 @@ def cuMemGetDefaultMemPool(location : Optional[CUmemLocation], typename not None :py:obj:`~.cuMemAllocAsync`, :py:obj:`~.cuMemPoolTrimTo`, :py:obj:`~.cuMemPoolGetAttribute`, :py:obj:`~.cuMemPoolSetAttribute`, :py:obj:`~.cuMemPoolSetAccess`, :py:obj:`~.cuMemGetMemPool`, :py:obj:`~.cuMemPoolCreate` """ cdef CUmemoryPool pool_out = CUmemoryPool() - cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL cdef cydriver.CUmemAllocationType cytypename = int(typename) with nogil: err = cydriver.cuMemGetDefaultMemPool(pool_out._pvt_ptr, cylocation_ptr, cytypename) @@ -37218,7 +37218,7 @@ def cuMemGetMemPool(location : Optional[CUmemLocation], typename not None : CUme :py:obj:`~.cuDeviceGetDefaultMemPool`, :py:obj:`~.cuMemPoolCreate`, :py:obj:`~.cuDeviceSetMemPool`, :py:obj:`~.cuMemSetMemPool` """ cdef CUmemoryPool pool = CUmemoryPool() - cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL cdef cydriver.CUmemAllocationType cytypename = int(typename) with nogil: err = cydriver.cuMemGetMemPool(pool._pvt_ptr, cylocation_ptr, cytypename) @@ -37286,7 +37286,7 @@ def cuMemSetMemPool(location : Optional[CUmemLocation], typename not None : CUme else: ppool = int(CUmemoryPool(pool)) cypool = ppool - cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cydriver.CUmemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL cdef cydriver.CUmemAllocationType cytypename = int(typename) with nogil: err = cydriver.cuMemSetMemPool(cylocation_ptr, cytypename, cypool) @@ -37547,7 +37547,7 @@ def cuMemPoolImportPointer(pool, shareData : Optional[CUmemPoolPtrExportData]): ppool = int(CUmemoryPool(pool)) cypool = ppool cdef CUdeviceptr ptr_out = CUdeviceptr() - cdef cydriver.CUmemPoolPtrExportData* cyshareData_ptr = shareData._pvt_ptr if shareData is not None else NULL + cdef cydriver.CUmemPoolPtrExportData* cyshareData_ptr = shareData._pvt_ptr if shareData is not None else NULL with nogil: err = cydriver.cuMemPoolImportPointer(ptr_out._pvt_ptr, cypool, cyshareData_ptr) if err != cydriver.CUDA_SUCCESS: @@ -37610,7 +37610,7 @@ def cuMulticastCreate(prop : Optional[CUmulticastObjectProp]): :py:obj:`~.cuMulticastBindAddr_v2`, :py:obj:`~.cuMulticastBindMem_v2` """ cdef CUmemGenericAllocationHandle mcHandle = CUmemGenericAllocationHandle() - cdef cydriver.CUmulticastObjectProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL + cdef cydriver.CUmulticastObjectProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL with nogil: err = cydriver.cuMulticastCreate(mcHandle._pvt_ptr, cyprop_ptr) if err != cydriver.CUDA_SUCCESS: @@ -38132,7 +38132,7 @@ def cuMulticastGetGranularity(prop : Optional[CUmulticastObjectProp], option not :py:obj:`~.cuMulticastBindMem_v2`, :py:obj:`~.cuMulticastBindAddr_v2` """ cdef size_t granularity = 0 - cdef cydriver.CUmulticastObjectProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL + cdef cydriver.CUmulticastObjectProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL cdef cydriver.CUmulticastGranularity_flags cyoption = int(option) with nogil: err = cydriver.cuMulticastGetGranularity(&granularity, cyprop_ptr, cyoption) @@ -39589,7 +39589,7 @@ def cuStreamBeginCaptureToCig(hStream, streamCigCaptureParams : Optional[CUstrea else: phStream = int(CUstream(hStream)) cyhStream = phStream - cdef cydriver.CUstreamCigCaptureParams* cystreamCigCaptureParams_ptr = streamCigCaptureParams._pvt_ptr if streamCigCaptureParams is not None else NULL + cdef cydriver.CUstreamCigCaptureParams* cystreamCigCaptureParams_ptr = streamCigCaptureParams._pvt_ptr if streamCigCaptureParams is not None else NULL with nogil: err = cydriver.cuStreamBeginCaptureToCig(cyhStream, cystreamCigCaptureParams_ptr) return (_CUresult(err),) @@ -41046,7 +41046,7 @@ def cuStreamSetAttribute(hStream, attr not None : CUstreamAttrID, value : Option phStream = int(CUstream(hStream)) cyhStream = phStream cdef cydriver.CUstreamAttrID cyattr = int(attr) - cdef cydriver.CUstreamAttrValue* cyvalue_ptr = value._pvt_ptr if value is not None else NULL + cdef cydriver.CUstreamAttrValue* cyvalue_ptr = value._pvt_ptr if value is not None else NULL with nogil: err = cydriver.cuStreamSetAttribute(cyhStream, cyattr, cyvalue_ptr) return (_CUresult(err),) @@ -41589,7 +41589,7 @@ def cuImportExternalMemory(memHandleDesc : Optional[CUDA_EXTERNAL_MEMORY_HANDLE_ and Cache Control" chapter from Vulkan specification. """ cdef CUexternalMemory extMem_out = CUexternalMemory() - cdef cydriver.CUDA_EXTERNAL_MEMORY_HANDLE_DESC* cymemHandleDesc_ptr = memHandleDesc._pvt_ptr if memHandleDesc is not None else NULL + cdef cydriver.CUDA_EXTERNAL_MEMORY_HANDLE_DESC* cymemHandleDesc_ptr = memHandleDesc._pvt_ptr if memHandleDesc is not None else NULL with nogil: err = cydriver.cuImportExternalMemory(extMem_out._pvt_ptr, cymemHandleDesc_ptr) if err != cydriver.CUDA_SUCCESS: @@ -41659,7 +41659,7 @@ def cuExternalMemoryGetMappedBuffer(extMem, bufferDesc : Optional[CUDA_EXTERNAL_ pextMem = int(CUexternalMemory(extMem)) cyextMem = pextMem cdef CUdeviceptr devPtr = CUdeviceptr() - cdef cydriver.CUDA_EXTERNAL_MEMORY_BUFFER_DESC* cybufferDesc_ptr = bufferDesc._pvt_ptr if bufferDesc is not None else NULL + cdef cydriver.CUDA_EXTERNAL_MEMORY_BUFFER_DESC* cybufferDesc_ptr = bufferDesc._pvt_ptr if bufferDesc is not None else NULL with nogil: err = cydriver.cuExternalMemoryGetMappedBuffer(devPtr._pvt_ptr, cyextMem, cybufferDesc_ptr) if err != cydriver.CUDA_SUCCESS: @@ -41735,7 +41735,7 @@ def cuExternalMemoryGetMappedMipmappedArray(extMem, mipmapDesc : Optional[CUDA_E pextMem = int(CUexternalMemory(extMem)) cyextMem = pextMem cdef CUmipmappedArray mipmap = CUmipmappedArray() - cdef cydriver.CUDA_EXTERNAL_MEMORY_MIPMAPPED_ARRAY_DESC* cymipmapDesc_ptr = mipmapDesc._pvt_ptr if mipmapDesc is not None else NULL + cdef cydriver.CUDA_EXTERNAL_MEMORY_MIPMAPPED_ARRAY_DESC* cymipmapDesc_ptr = mipmapDesc._pvt_ptr if mipmapDesc is not None else NULL with nogil: err = cydriver.cuExternalMemoryGetMappedMipmappedArray(mipmap._pvt_ptr, cyextMem, cymipmapDesc_ptr) if err != cydriver.CUDA_SUCCESS: @@ -41921,7 +41921,7 @@ def cuImportExternalSemaphore(semHandleDesc : Optional[CUDA_EXTERNAL_SEMAPHORE_H :py:obj:`~.cuDestroyExternalSemaphore`, :py:obj:`~.cuSignalExternalSemaphoresAsync`, :py:obj:`~.cuWaitExternalSemaphoresAsync` """ cdef CUexternalSemaphore extSem_out = CUexternalSemaphore() - cdef cydriver.CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC* cysemHandleDesc_ptr = semHandleDesc._pvt_ptr if semHandleDesc is not None else NULL + cdef cydriver.CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC* cysemHandleDesc_ptr = semHandleDesc._pvt_ptr if semHandleDesc is not None else NULL with nogil: err = cydriver.cuImportExternalSemaphore(extSem_out._pvt_ptr, cysemHandleDesc_ptr) if err != cydriver.CUDA_SUCCESS: @@ -43514,7 +43514,7 @@ def cuLaunchKernelEx(config : Optional[CUlaunchConfig], f, kernelParams, void_pt else: pf = int(CUfunction(f)) cyf = pf - cdef cydriver.CUlaunchConfig* cyconfig_ptr = config._pvt_ptr if config is not None else NULL + cdef cydriver.CUlaunchConfig* cyconfig_ptr = config._pvt_ptr if config is not None else NULL cykernelParams = _HelperKernelParams(kernelParams) cdef void** cykernelParams_ptr = cykernelParams.ckernelParams with nogil: @@ -44712,7 +44712,7 @@ def cuGraphAddKernelNode(hGraph, dependencies : Optional[tuple[CUgraphNode] | li elif len(dependencies) == 1: cydependencies = (dependencies[0])._pvt_ptr if numDependencies > len(dependencies): raise RuntimeError("List is too small: " + str(len(dependencies)) + " < " + str(numDependencies)) - cdef cydriver.CUDA_KERNEL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_KERNEL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphAddKernelNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, numDependencies, cynodeParams_ptr) if len(dependencies) > 1 and cydependencies is not NULL: @@ -44803,7 +44803,7 @@ def cuGraphKernelNodeSetParams(hNode, nodeParams : Optional[CUDA_KERNEL_NODE_PAR else: phNode = int(CUgraphNode(hNode)) cyhNode = phNode - cdef cydriver.CUDA_KERNEL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_KERNEL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphKernelNodeSetParams(cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -44890,7 +44890,7 @@ def cuGraphAddMemcpyNode(hGraph, dependencies : Optional[tuple[CUgraphNode] | li elif len(dependencies) == 1: cydependencies = (dependencies[0])._pvt_ptr if numDependencies > len(dependencies): raise RuntimeError("List is too small: " + str(len(dependencies)) + " < " + str(numDependencies)) - cdef cydriver.CUDA_MEMCPY3D* cycopyParams_ptr = copyParams._pvt_ptr if copyParams is not None else NULL + cdef cydriver.CUDA_MEMCPY3D* cycopyParams_ptr = copyParams._pvt_ptr if copyParams is not None else NULL with nogil: err = cydriver.cuGraphAddMemcpyNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, numDependencies, cycopyParams_ptr, cyctx) if len(dependencies) > 1 and cydependencies is not NULL: @@ -44972,7 +44972,7 @@ def cuGraphMemcpyNodeSetParams(hNode, nodeParams : Optional[CUDA_MEMCPY3D]): else: phNode = int(CUgraphNode(hNode)) cyhNode = phNode - cdef cydriver.CUDA_MEMCPY3D* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_MEMCPY3D* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphMemcpyNodeSetParams(cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -45049,7 +45049,7 @@ def cuGraphAddMemsetNode(hGraph, dependencies : Optional[tuple[CUgraphNode] | li elif len(dependencies) == 1: cydependencies = (dependencies[0])._pvt_ptr if numDependencies > len(dependencies): raise RuntimeError("List is too small: " + str(len(dependencies)) + " < " + str(numDependencies)) - cdef cydriver.CUDA_MEMSET_NODE_PARAMS* cymemsetParams_ptr = memsetParams._pvt_ptr if memsetParams is not None else NULL + cdef cydriver.CUDA_MEMSET_NODE_PARAMS* cymemsetParams_ptr = memsetParams._pvt_ptr if memsetParams is not None else NULL with nogil: err = cydriver.cuGraphAddMemsetNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, numDependencies, cymemsetParams_ptr, cyctx) if len(dependencies) > 1 and cydependencies is not NULL: @@ -45131,7 +45131,7 @@ def cuGraphMemsetNodeSetParams(hNode, nodeParams : Optional[CUDA_MEMSET_NODE_PAR else: phNode = int(CUgraphNode(hNode)) cyhNode = phNode - cdef cydriver.CUDA_MEMSET_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_MEMSET_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphMemsetNodeSetParams(cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -45198,7 +45198,7 @@ def cuGraphAddHostNode(hGraph, dependencies : Optional[tuple[CUgraphNode] | list elif len(dependencies) == 1: cydependencies = (dependencies[0])._pvt_ptr if numDependencies > len(dependencies): raise RuntimeError("List is too small: " + str(len(dependencies)) + " < " + str(numDependencies)) - cdef cydriver.CUDA_HOST_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_HOST_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphAddHostNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, numDependencies, cynodeParams_ptr) if len(dependencies) > 1 and cydependencies is not NULL: @@ -45280,7 +45280,7 @@ def cuGraphHostNodeSetParams(hNode, nodeParams : Optional[CUDA_HOST_NODE_PARAMS] else: phNode = int(CUgraphNode(hNode)) cyhNode = phNode - cdef cydriver.CUDA_HOST_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_HOST_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphHostNodeSetParams(cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -45873,7 +45873,7 @@ def cuGraphAddExternalSemaphoresSignalNode(hGraph, dependencies : Optional[tuple elif len(dependencies) == 1: cydependencies = (dependencies[0])._pvt_ptr if numDependencies > len(dependencies): raise RuntimeError("List is too small: " + str(len(dependencies)) + " < " + str(numDependencies)) - cdef cydriver.CUDA_EXT_SEM_SIGNAL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_EXT_SEM_SIGNAL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphAddExternalSemaphoresSignalNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, numDependencies, cynodeParams_ptr) if len(dependencies) > 1 and cydependencies is not NULL: @@ -45962,7 +45962,7 @@ def cuGraphExternalSemaphoresSignalNodeSetParams(hNode, nodeParams : Optional[CU else: phNode = int(CUgraphNode(hNode)) cyhNode = phNode - cdef cydriver.CUDA_EXT_SEM_SIGNAL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_EXT_SEM_SIGNAL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphExternalSemaphoresSignalNodeSetParams(cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -46030,7 +46030,7 @@ def cuGraphAddExternalSemaphoresWaitNode(hGraph, dependencies : Optional[tuple[C elif len(dependencies) == 1: cydependencies = (dependencies[0])._pvt_ptr if numDependencies > len(dependencies): raise RuntimeError("List is too small: " + str(len(dependencies)) + " < " + str(numDependencies)) - cdef cydriver.CUDA_EXT_SEM_WAIT_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_EXT_SEM_WAIT_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphAddExternalSemaphoresWaitNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, numDependencies, cynodeParams_ptr) if len(dependencies) > 1 and cydependencies is not NULL: @@ -46119,7 +46119,7 @@ def cuGraphExternalSemaphoresWaitNodeSetParams(hNode, nodeParams : Optional[CUDA else: phNode = int(CUgraphNode(hNode)) cyhNode = phNode - cdef cydriver.CUDA_EXT_SEM_WAIT_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_EXT_SEM_WAIT_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphExternalSemaphoresWaitNodeSetParams(cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -46190,7 +46190,7 @@ def cuGraphAddBatchMemOpNode(hGraph, dependencies : Optional[tuple[CUgraphNode] elif len(dependencies) == 1: cydependencies = (dependencies[0])._pvt_ptr if numDependencies > len(dependencies): raise RuntimeError("List is too small: " + str(len(dependencies)) + " < " + str(numDependencies)) - cdef cydriver.CUDA_BATCH_MEM_OP_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_BATCH_MEM_OP_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphAddBatchMemOpNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, numDependencies, cynodeParams_ptr) if len(dependencies) > 1 and cydependencies is not NULL: @@ -46280,7 +46280,7 @@ def cuGraphBatchMemOpNodeSetParams(hNode, nodeParams : Optional[CUDA_BATCH_MEM_O else: phNode = int(CUgraphNode(hNode)) cyhNode = phNode - cdef cydriver.CUDA_BATCH_MEM_OP_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_BATCH_MEM_OP_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphBatchMemOpNodeSetParams(cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -46351,7 +46351,7 @@ def cuGraphExecBatchMemOpNodeSetParams(hGraphExec, hNode, nodeParams : Optional[ else: phGraphExec = int(CUgraphExec(hGraphExec)) cyhGraphExec = phGraphExec - cdef cydriver.CUDA_BATCH_MEM_OP_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_BATCH_MEM_OP_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphExecBatchMemOpNodeSetParams(cyhGraphExec, cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -46459,7 +46459,7 @@ def cuGraphAddMemAllocNode(hGraph, dependencies : Optional[tuple[CUgraphNode] | elif len(dependencies) == 1: cydependencies = (dependencies[0])._pvt_ptr if numDependencies > len(dependencies): raise RuntimeError("List is too small: " + str(len(dependencies)) + " < " + str(numDependencies)) - cdef cydriver.CUDA_MEM_ALLOC_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_MEM_ALLOC_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphAddMemAllocNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, numDependencies, cynodeParams_ptr) if len(dependencies) > 1 and cydependencies is not NULL: @@ -47980,7 +47980,7 @@ def cuGraphInstantiateWithParams(hGraph, instantiateParams : Optional[CUDA_GRAPH phGraph = int(CUgraph(hGraph)) cyhGraph = phGraph cdef CUgraphExec phGraphExec = CUgraphExec() - cdef cydriver.CUDA_GRAPH_INSTANTIATE_PARAMS* cyinstantiateParams_ptr = instantiateParams._pvt_ptr if instantiateParams is not None else NULL + cdef cydriver.CUDA_GRAPH_INSTANTIATE_PARAMS* cyinstantiateParams_ptr = instantiateParams._pvt_ptr if instantiateParams is not None else NULL with nogil: err = cydriver.cuGraphInstantiateWithParams(phGraphExec._pvt_ptr, cyhGraph, cyinstantiateParams_ptr) if err != cydriver.CUDA_SUCCESS: @@ -48106,7 +48106,7 @@ def cuGraphExecKernelNodeSetParams(hGraphExec, hNode, nodeParams : Optional[CUDA else: phGraphExec = int(CUgraphExec(hGraphExec)) cyhGraphExec = phGraphExec - cdef cydriver.CUDA_KERNEL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_KERNEL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphExecKernelNodeSetParams(cyhGraphExec, cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -48181,7 +48181,7 @@ def cuGraphExecMemcpyNodeSetParams(hGraphExec, hNode, copyParams : Optional[CUDA else: phGraphExec = int(CUgraphExec(hGraphExec)) cyhGraphExec = phGraphExec - cdef cydriver.CUDA_MEMCPY3D* cycopyParams_ptr = copyParams._pvt_ptr if copyParams is not None else NULL + cdef cydriver.CUDA_MEMCPY3D* cycopyParams_ptr = copyParams._pvt_ptr if copyParams is not None else NULL with nogil: err = cydriver.cuGraphExecMemcpyNodeSetParams(cyhGraphExec, cyhNode, cycopyParams_ptr, cyctx) return (_CUresult(err),) @@ -48261,7 +48261,7 @@ def cuGraphExecMemsetNodeSetParams(hGraphExec, hNode, memsetParams : Optional[CU else: phGraphExec = int(CUgraphExec(hGraphExec)) cyhGraphExec = phGraphExec - cdef cydriver.CUDA_MEMSET_NODE_PARAMS* cymemsetParams_ptr = memsetParams._pvt_ptr if memsetParams is not None else NULL + cdef cydriver.CUDA_MEMSET_NODE_PARAMS* cymemsetParams_ptr = memsetParams._pvt_ptr if memsetParams is not None else NULL with nogil: err = cydriver.cuGraphExecMemsetNodeSetParams(cyhGraphExec, cyhNode, cymemsetParams_ptr, cyctx) return (_CUresult(err),) @@ -48316,7 +48316,7 @@ def cuGraphExecHostNodeSetParams(hGraphExec, hNode, nodeParams : Optional[CUDA_H else: phGraphExec = int(CUgraphExec(hGraphExec)) cyhGraphExec = phGraphExec - cdef cydriver.CUDA_HOST_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_HOST_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphExecHostNodeSetParams(cyhGraphExec, cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -48572,7 +48572,7 @@ def cuGraphExecExternalSemaphoresSignalNodeSetParams(hGraphExec, hNode, nodePara else: phGraphExec = int(CUgraphExec(hGraphExec)) cyhGraphExec = phGraphExec - cdef cydriver.CUDA_EXT_SEM_SIGNAL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_EXT_SEM_SIGNAL_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphExecExternalSemaphoresSignalNodeSetParams(cyhGraphExec, cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -48632,7 +48632,7 @@ def cuGraphExecExternalSemaphoresWaitNodeSetParams(hGraphExec, hNode, nodeParams else: phGraphExec = int(CUgraphExec(hGraphExec)) cyhGraphExec = phGraphExec - cdef cydriver.CUDA_EXT_SEM_WAIT_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUDA_EXT_SEM_WAIT_NODE_PARAMS* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphExecExternalSemaphoresWaitNodeSetParams(cyhGraphExec, cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -49250,7 +49250,7 @@ def cuGraphKernelNodeSetAttribute(hNode, attr not None : CUkernelNodeAttrID, val phNode = int(CUgraphNode(hNode)) cyhNode = phNode cdef cydriver.CUkernelNodeAttrID cyattr = int(attr) - cdef cydriver.CUkernelNodeAttrValue* cyvalue_ptr = value._pvt_ptr if value is not None else NULL + cdef cydriver.CUkernelNodeAttrValue* cyvalue_ptr = value._pvt_ptr if value is not None else NULL with nogil: err = cydriver.cuGraphKernelNodeSetAttribute(cyhNode, cyattr, cyvalue_ptr) return (_CUresult(err),) @@ -49637,7 +49637,7 @@ def cuGraphAddNode(hGraph, dependencies : Optional[tuple[CUgraphNode] | list[CUg string.memcpy(&cydependencyData[idx], (dependencyData[idx])._pvt_ptr, sizeof(cydriver.CUgraphEdgeData)) elif len(dependencyData) == 1: cydependencyData = (dependencyData[0])._pvt_ptr - cdef cydriver.CUgraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUgraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphAddNode(phGraphNode._pvt_ptr, cyhGraph, cydependencies, cydependencyData, numDependencies, cynodeParams_ptr) if len(dependencies) > 1 and cydependencies is not NULL: @@ -49687,7 +49687,7 @@ def cuGraphNodeSetParams(hNode, nodeParams : Optional[CUgraphNodeParams]): else: phNode = int(CUgraphNode(hNode)) cyhNode = phNode - cdef cydriver.CUgraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUgraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphNodeSetParams(cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -49800,7 +49800,7 @@ def cuGraphExecNodeSetParams(hGraphExec, hNode, nodeParams : Optional[CUgraphNod else: phGraphExec = int(CUgraphExec(hGraphExec)) cyhGraphExec = phGraphExec - cdef cydriver.CUgraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cydriver.CUgraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cydriver.cuGraphExecNodeSetParams(cyhGraphExec, cyhNode, cynodeParams_ptr) return (_CUresult(err),) @@ -50263,7 +50263,7 @@ def cuOccupancyMaxPotentialClusterSize(func, config : Optional[CUlaunchConfig]): pfunc = int(CUfunction(func)) cyfunc = pfunc cdef int clusterSize = 0 - cdef cydriver.CUlaunchConfig* cyconfig_ptr = config._pvt_ptr if config is not None else NULL + cdef cydriver.CUlaunchConfig* cyconfig_ptr = config._pvt_ptr if config is not None else NULL with nogil: err = cydriver.cuOccupancyMaxPotentialClusterSize(&clusterSize, cyfunc, cyconfig_ptr) if err != cydriver.CUDA_SUCCESS: @@ -50323,7 +50323,7 @@ def cuOccupancyMaxActiveClusters(func, config : Optional[CUlaunchConfig]): pfunc = int(CUfunction(func)) cyfunc = pfunc cdef int numClusters = 0 - cdef cydriver.CUlaunchConfig* cyconfig_ptr = config._pvt_ptr if config is not None else NULL + cdef cydriver.CUlaunchConfig* cyconfig_ptr = config._pvt_ptr if config is not None else NULL with nogil: err = cydriver.cuOccupancyMaxActiveClusters(&numClusters, cyfunc, cyconfig_ptr) if err != cydriver.CUDA_SUCCESS: @@ -50587,7 +50587,7 @@ def cuTexRefSetAddress2D(hTexRef, desc : Optional[CUDA_ARRAY_DESCRIPTOR], dptr, else: phTexRef = int(CUtexref(hTexRef)) cyhTexRef = phTexRef - cdef cydriver.CUDA_ARRAY_DESCRIPTOR* cydesc_ptr = desc._pvt_ptr if desc is not None else NULL + cdef cydriver.CUDA_ARRAY_DESCRIPTOR* cydesc_ptr = desc._pvt_ptr if desc is not None else NULL with nogil: err = cydriver.cuTexRefSetAddress2D(cyhTexRef, cydesc_ptr, cydptr, Pitch) return (_CUresult(err),) @@ -51957,9 +51957,9 @@ def cuTexObjectCreate(pResDesc : Optional[CUDA_RESOURCE_DESC], pTexDesc : Option :py:obj:`~.cuTexObjectDestroy`, :py:obj:`~.cudaCreateTextureObject` """ cdef CUtexObject pTexObject = CUtexObject() - cdef cydriver.CUDA_RESOURCE_DESC* cypResDesc_ptr = pResDesc._pvt_ptr if pResDesc is not None else NULL - cdef cydriver.CUDA_TEXTURE_DESC* cypTexDesc_ptr = pTexDesc._pvt_ptr if pTexDesc is not None else NULL - cdef cydriver.CUDA_RESOURCE_VIEW_DESC* cypResViewDesc_ptr = pResViewDesc._pvt_ptr if pResViewDesc is not None else NULL + cdef cydriver.CUDA_RESOURCE_DESC* cypResDesc_ptr = pResDesc._pvt_ptr if pResDesc is not None else NULL + cdef cydriver.CUDA_TEXTURE_DESC* cypTexDesc_ptr = pTexDesc._pvt_ptr if pTexDesc is not None else NULL + cdef cydriver.CUDA_RESOURCE_VIEW_DESC* cypResViewDesc_ptr = pResViewDesc._pvt_ptr if pResViewDesc is not None else NULL with nogil: err = cydriver.cuTexObjectCreate(pTexObject._pvt_ptr, cypResDesc_ptr, cypTexDesc_ptr, cypResViewDesc_ptr) if err != cydriver.CUDA_SUCCESS: @@ -52161,7 +52161,7 @@ def cuSurfObjectCreate(pResDesc : Optional[CUDA_RESOURCE_DESC]): :py:obj:`~.cuSurfObjectDestroy`, :py:obj:`~.cudaCreateSurfaceObject` """ cdef CUsurfObject pSurfObject = CUsurfObject() - cdef cydriver.CUDA_RESOURCE_DESC* cypResDesc_ptr = pResDesc._pvt_ptr if pResDesc is not None else NULL + cdef cydriver.CUDA_RESOURCE_DESC* cypResDesc_ptr = pResDesc._pvt_ptr if pResDesc is not None else NULL with nogil: err = cydriver.cuSurfObjectCreate(pSurfObject._pvt_ptr, cypResDesc_ptr) if err != cydriver.CUDA_SUCCESS: @@ -53258,7 +53258,7 @@ def cuTensorMapReplaceAddress(tensorMap : Optional[CUtensorMap], globalAddress): -------- :py:obj:`~.cuTensorMapEncodeTiled`, :py:obj:`~.cuTensorMapEncodeIm2col`, :py:obj:`~.cuTensorMapEncodeIm2colWide` """ - cdef cydriver.CUtensorMap* cytensorMap_ptr = tensorMap._pvt_ptr if tensorMap is not None else NULL + cdef cydriver.CUtensorMap* cytensorMap_ptr = tensorMap._pvt_ptr if tensorMap is not None else NULL cdef _HelperInputVoidPtrStruct cyglobalAddressHelper cdef void* cyglobalAddress = _helper_input_void_ptr(globalAddress, &cyglobalAddressHelper) with nogil: @@ -54765,7 +54765,7 @@ def cuGetExportTable(pExportTableId : Optional[CUuuid]): None """ cdef void_ptr ppExportTable = 0 - cdef cydriver.CUuuid* cypExportTableId_ptr = pExportTableId._pvt_ptr if pExportTableId is not None else NULL + cdef cydriver.CUuuid* cypExportTableId_ptr = pExportTableId._pvt_ptr if pExportTableId is not None else NULL with nogil: err = cydriver.cuGetExportTable(&ppExportTable, cypExportTableId_ptr) if err != cydriver.CUDA_SUCCESS: @@ -55193,7 +55193,7 @@ def cuDevSmResourceSplitByCount(unsigned int nbGroups, input_ : Optional[CUdevRe if cyresult is NULL: raise MemoryError('Failed to allocate length x size memory: ' + str(nbGroups) + 'x' + str(sizeof(cydriver.CUdevResource))) cdef unsigned int cynbGroups = nbGroups - cdef cydriver.CUdevResource* cyinput__ptr = input_._pvt_ptr if input_ is not None else NULL + cdef cydriver.CUdevResource* cyinput__ptr = input_._pvt_ptr if input_ is not None else NULL cdef CUdevResource remainder = CUdevResource() with nogil: err = cydriver.cuDevSmResourceSplitByCount(cyresult, &cynbGroups, cyinput__ptr, remainder._pvt_ptr, flags, minCount) @@ -55356,7 +55356,7 @@ def cuDevSmResourceSplit(unsigned int nbGroups, input_ : Optional[CUdevResource] cyresult = calloc(nbGroups, sizeof(cydriver.CUdevResource)) if cyresult is NULL: raise MemoryError('Failed to allocate length x size memory: ' + str(nbGroups) + 'x' + str(sizeof(cydriver.CUdevResource))) - cdef cydriver.CUdevResource* cyinput__ptr = input_._pvt_ptr if input_ is not None else NULL + cdef cydriver.CUdevResource* cyinput__ptr = input_._pvt_ptr if input_ is not None else NULL cdef CUdevResource remainder = CUdevResource() cdef cydriver.CU_DEV_SM_RESOURCE_GROUP_PARAMS* cygroupParams = NULL if len(groupParams) > 1: @@ -56095,7 +56095,7 @@ def cuCheckpointProcessLock(int pid, args : Optional[CUcheckpointLockArgs]): CUresult :py:obj:`~.CUDA_SUCCESS` :py:obj:`~.CUDA_ERROR_INVALID_VALUE` :py:obj:`~.CUDA_ERROR_NOT_INITIALIZED` :py:obj:`~.CUDA_ERROR_ILLEGAL_STATE` :py:obj:`~.CUDA_ERROR_NOT_SUPPORTED` :py:obj:`~.CUDA_ERROR_NOT_READY` """ - cdef cydriver.CUcheckpointLockArgs* cyargs_ptr = args._pvt_ptr if args is not None else NULL + cdef cydriver.CUcheckpointLockArgs* cyargs_ptr = args._pvt_ptr if args is not None else NULL with nogil: err = cydriver.cuCheckpointProcessLock(pid, cyargs_ptr) return (_CUresult(err),) @@ -56126,7 +56126,7 @@ def cuCheckpointProcessCheckpoint(int pid, args : Optional[CUcheckpointCheckpoin CUresult :py:obj:`~.CUDA_SUCCESS` :py:obj:`~.CUDA_ERROR_INVALID_VALUE` :py:obj:`~.CUDA_ERROR_NOT_INITIALIZED` :py:obj:`~.CUDA_ERROR_ILLEGAL_STATE` :py:obj:`~.CUDA_ERROR_NOT_SUPPORTED` """ - cdef cydriver.CUcheckpointCheckpointArgs* cyargs_ptr = args._pvt_ptr if args is not None else NULL + cdef cydriver.CUcheckpointCheckpointArgs* cyargs_ptr = args._pvt_ptr if args is not None else NULL with nogil: err = cydriver.cuCheckpointProcessCheckpoint(pid, cyargs_ptr) return (_CUresult(err),) @@ -56167,7 +56167,7 @@ def cuCheckpointProcessRestore(int pid, args : Optional[CUcheckpointRestoreArgs] -------- :py:obj:`~.cuInit` """ - cdef cydriver.CUcheckpointRestoreArgs* cyargs_ptr = args._pvt_ptr if args is not None else NULL + cdef cydriver.CUcheckpointRestoreArgs* cyargs_ptr = args._pvt_ptr if args is not None else NULL with nogil: err = cydriver.cuCheckpointProcessRestore(pid, cyargs_ptr) return (_CUresult(err),) @@ -56196,7 +56196,7 @@ def cuCheckpointProcessUnlock(int pid, args : Optional[CUcheckpointUnlockArgs]): CUresult :py:obj:`~.CUDA_SUCCESS` :py:obj:`~.CUDA_ERROR_INVALID_VALUE` :py:obj:`~.CUDA_ERROR_NOT_INITIALIZED` :py:obj:`~.CUDA_ERROR_ILLEGAL_STATE` :py:obj:`~.CUDA_ERROR_NOT_SUPPORTED` """ - cdef cydriver.CUcheckpointUnlockArgs* cyargs_ptr = args._pvt_ptr if args is not None else NULL + cdef cydriver.CUcheckpointUnlockArgs* cyargs_ptr = args._pvt_ptr if args is not None else NULL with nogil: err = cydriver.cuCheckpointProcessUnlock(pid, cyargs_ptr) return (_CUresult(err),) @@ -56815,7 +56815,7 @@ def cuEGLStreamProducerReturnFrame(conn, eglframe : Optional[CUeglFrame], pStrea cyconn = conn else: raise TypeError("Argument 'conn' is not instance of type (expected , found " + str(type(conn))) - cdef cydriver.CUeglFrame* cyeglframe_ptr = eglframe._pvt_ptr if eglframe is not None else NULL + cdef cydriver.CUeglFrame* cyeglframe_ptr = eglframe._pvt_ptr if eglframe is not None else NULL with nogil: err = cydriver.cuEGLStreamProducerReturnFrame(cyconn, cyeglframe_ptr, cypStream) return (_CUresult(err),) diff --git a/cuda_bindings/cuda/bindings/nvml.pxd b/cuda_bindings/cuda/bindings/nvml.pxd index 2176561bc43..1822e272f39 100644 --- a/cuda_bindings/cuda/bindings/nvml.pxd +++ b/cuda_bindings/cuda/bindings/nvml.pxd @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE # -# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1568+g289771de9.d20260413. Do not modify it directly. from libc.stdint cimport intptr_t @@ -154,7 +154,7 @@ cpdef int system_get_cuda_driver_version_v2() except 0 cpdef str system_get_process_name(unsigned int pid) cpdef object system_get_hic_version() cpdef unsigned int unit_get_count() except? 0 -cpdef intptr_t unit_get_handle_by_index(unsigned int ind_ex) except? 0 +cpdef intptr_t unit_get_handle_by_index(unsigned int index) except? 0 cpdef object unit_get_unit_info(intptr_t unit) cpdef object unit_get_led_state(intptr_t unit) cpdef object unit_get_psu_info(intptr_t unit) @@ -162,7 +162,7 @@ cpdef unsigned int unit_get_temperature(intptr_t unit, unsigned int type) except cpdef object unit_get_fan_speed_info(intptr_t unit) cpdef unsigned int device_get_count_v2() except? 0 cpdef object device_get_attributes_v2(intptr_t device) -cpdef intptr_t device_get_handle_by_index_v2(unsigned int ind_ex) except? 0 +cpdef intptr_t device_get_handle_by_index_v2(unsigned int index) except? 0 cpdef intptr_t device_get_handle_by_serial(serial) except? 0 cpdef intptr_t device_get_handle_by_uuid(uuid) except? 0 cpdef intptr_t device_get_handle_by_pci_bus_id_v2(pci_bus_id) except? 0 @@ -179,7 +179,7 @@ cpdef device_set_cpu_affinity(intptr_t device) cpdef device_clear_cpu_affinity(intptr_t device) cpdef unsigned int device_get_numa_node_id(intptr_t device) except? 0 cpdef int device_get_topology_common_ancestor(intptr_t device1, intptr_t device2) except? -1 -cpdef int device_get_p2p_status(intptr_t device1, intptr_t device2, int p2p_ind_ex) except? -1 +cpdef int device_get_p2p_status(intptr_t device1, intptr_t device2, int p2p_index) except? -1 cpdef str device_get_uuid(intptr_t device) cpdef unsigned int device_get_minor_number(intptr_t device) except? 0 cpdef str device_get_board_part_number(intptr_t device) @@ -216,7 +216,7 @@ cpdef unsigned int device_get_fan_control_policy_v2(intptr_t device, unsigned in cpdef unsigned int device_get_num_fans(intptr_t device) except? 0 cpdef object device_get_cooler_info(intptr_t device) cpdef unsigned int device_get_temperature_threshold(intptr_t device, int threshold_type) except? 0 -cpdef object device_get_thermal_settings(intptr_t device, unsigned int sensor_ind_ex) +cpdef object device_get_thermal_settings(intptr_t device, unsigned int sensor_index) cpdef int device_get_performance_state(intptr_t device) except? -1 cpdef unsigned long long device_get_current_clocks_event_reasons(intptr_t device) except? 0 cpdef unsigned long long device_get_supported_clocks_event_reasons(intptr_t device) except? 0 @@ -347,7 +347,7 @@ cpdef unsigned int vgpu_type_get_gpu_instance_profile_id(unsigned int vgpu_type_ cpdef tuple vgpu_type_get_device_id(unsigned int vgpu_type_id) cpdef unsigned long long vgpu_type_get_framebuffer_size(unsigned int vgpu_type_id) except? 0 cpdef unsigned int vgpu_type_get_num_display_heads(unsigned int vgpu_type_id) except? 0 -cpdef tuple vgpu_type_get_resolution(unsigned int vgpu_type_id, unsigned int display_ind_ex) +cpdef tuple vgpu_type_get_resolution(unsigned int vgpu_type_id, unsigned int display_index) cpdef str vgpu_type_get_license(unsigned int vgpu_type_id) cpdef unsigned int vgpu_type_get_frame_rate_limit(unsigned int vgpu_type_id) except? 0 cpdef unsigned int vgpu_type_get_max_instances(intptr_t device, unsigned int vgpu_type_id) except? 0 @@ -386,7 +386,7 @@ cpdef object vgpu_instance_get_accounting_stats(unsigned int vgpu_instance, unsi cpdef vgpu_instance_clear_accounting_pids(unsigned int vgpu_instance) cpdef object vgpu_instance_get_license_info_v2(unsigned int vgpu_instance) cpdef unsigned int get_excluded_device_count() except? 0 -cpdef object get_excluded_device_info_by_index(unsigned int ind_ex) +cpdef object get_excluded_device_info_by_index(unsigned int index) cpdef int device_set_mig_mode(intptr_t device, unsigned int mode) except? -1 cpdef tuple device_get_mig_mode(intptr_t device) cpdef object device_get_gpu_instance_possible_placements_v2(intptr_t device, unsigned int profile_id) @@ -408,7 +408,7 @@ cpdef unsigned int device_is_mig_device_handle(intptr_t device) except? 0 cpdef unsigned int device_get_gpu_instance_id(intptr_t device) except? 0 cpdef unsigned int device_get_compute_instance_id(intptr_t device) except? 0 cpdef unsigned int device_get_max_mig_device_count(intptr_t device) except? 0 -cpdef intptr_t device_get_mig_device_handle_by_index(intptr_t device, unsigned int ind_ex) except? 0 +cpdef intptr_t device_get_mig_device_handle_by_index(intptr_t device, unsigned int index) except? 0 cpdef intptr_t device_get_device_handle_from_mig_device_handle(intptr_t mig_device) except? 0 cpdef device_power_smoothing_activate_preset_profile(intptr_t device, intptr_t profile) cpdef device_power_smoothing_update_preset_profile_param(intptr_t device, intptr_t profile) diff --git a/cuda_bindings/cuda/bindings/nvml.pyx b/cuda_bindings/cuda/bindings/nvml.pyx index 77f50415e17..0e2493a764b 100644 --- a/cuda_bindings/cuda/bindings/nvml.pyx +++ b/cuda_bindings/cuda/bindings/nvml.pyx @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE # -# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated across versions from 12.9.1 to 13.2.0, generator version 0.3.1.dev1568+g289771de9.d20260413. Do not modify it directly. cimport cython # NOQA @@ -2061,7 +2061,6 @@ cdef object _nvml_error_factory(int status): return NvmlError(status) - @cython.profile(False) cpdef int check_status(int status) except 1 nogil: if status != 0: @@ -3174,21 +3173,17 @@ process_info_dtype = _get_process_info_dtype_offsets() cdef class ProcessInfo: """Empty-initialize an array of `nvmlProcessInfo_t`. - The resulting object is of length `size` and of dtype `process_info_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlProcessInfo_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=process_info_dtype) self._data = arr.view(_numpy.recarray) @@ -3356,21 +3351,17 @@ process_detail_v1_dtype = _get_process_detail_v1_dtype_offsets() cdef class ProcessDetail_v1: """Empty-initialize an array of `nvmlProcessDetail_v1_t`. - The resulting object is of length `size` and of dtype `process_detail_v1_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlProcessDetail_v1_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=process_detail_v1_dtype) self._data = arr.view(_numpy.recarray) @@ -4083,21 +4074,17 @@ bridge_chip_info_dtype = _get_bridge_chip_info_dtype_offsets() cdef class BridgeChipInfo: """Empty-initialize an array of `nvmlBridgeChipInfo_t`. - The resulting object is of length `size` and of dtype `bridge_chip_info_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlBridgeChipInfo_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=bridge_chip_info_dtype) self._data = arr.view(_numpy.recarray) @@ -4237,7 +4224,6 @@ value_dtype = _numpy.dtype(( } )) - cdef class Value: """Empty-initialize an instance of `nvmlValue_t`. @@ -4604,7 +4590,7 @@ cdef class _py_anon_pod0: cdef _get_cooler_info_v1_dtype_offsets(): cdef nvmlCoolerInfo_v1_t pod = nvmlCoolerInfo_v1_t() return _numpy.dtype({ - 'names': ['version', 'ind_ex', 'signal_type', 'target'], + 'names': ['version', 'index', 'signal_type', 'target'], 'formats': [_numpy.uint32, _numpy.uint32, _numpy.int32, _numpy.int32], 'offsets': [ (&(pod.version)) - (&pod), @@ -4695,12 +4681,12 @@ cdef class CoolerInfo_v1: self._ptr[0].version = val @property - def ind_ex(self): + def index(self): """int: the cooler index""" return self._ptr[0].index - @ind_ex.setter - def ind_ex(self, val): + @index.setter + def index(self, val): if self._readonly: raise ValueError("This CoolerInfo_v1 instance is read-only") self._ptr[0].index = val @@ -4784,21 +4770,17 @@ clk_mon_fault_info_dtype = _get_clk_mon_fault_info_dtype_offsets() cdef class ClkMonFaultInfo: """Empty-initialize an array of `nvmlClkMonFaultInfo_t`. - The resulting object is of length `size` and of dtype `clk_mon_fault_info_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlClkMonFaultInfo_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=clk_mon_fault_info_dtype) self._data = arr.view(_numpy.recarray) @@ -5136,21 +5118,17 @@ process_utilization_sample_dtype = _get_process_utilization_sample_dtype_offsets cdef class ProcessUtilizationSample: """Empty-initialize an array of `nvmlProcessUtilizationSample_t`. - The resulting object is of length `size` and of dtype `process_utilization_sample_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlProcessUtilizationSample_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=process_utilization_sample_dtype) self._data = arr.view(_numpy.recarray) @@ -5343,21 +5321,17 @@ process_utilization_info_v1_dtype = _get_process_utilization_info_v1_dtype_offse cdef class ProcessUtilizationInfo_v1: """Empty-initialize an array of `nvmlProcessUtilizationInfo_v1_t`. - The resulting object is of length `size` and of dtype `process_utilization_info_v1_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlProcessUtilizationInfo_v1_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=process_utilization_info_v1_dtype) self._data = arr.view(_numpy.recarray) @@ -5828,7 +5802,7 @@ cdef class EccSramErrorStatus_v1: cdef _get_platform_info_v1_dtype_offsets(): cdef nvmlPlatformInfo_v1_t pod = nvmlPlatformInfo_v1_t() return _numpy.dtype({ - 'names': ['version', 'ib_guid', 'rack_guid', 'chassis_physical_slot_number', 'compute_slot_ind_ex', 'node_ind_ex', 'peer_type', 'module_id'], + 'names': ['version', 'ib_guid', 'rack_guid', 'chassis_physical_slot_number', 'compute_slot_index', 'node_index', 'peer_type', 'module_id'], 'formats': [_numpy.uint32, (_numpy.uint8, 16), (_numpy.uint8, 16), _numpy.uint8, _numpy.uint8, _numpy.uint8, _numpy.uint8, _numpy.uint8], 'offsets': [ (&(pod.version)) - (&pod), @@ -5968,23 +5942,23 @@ cdef class PlatformInfo_v1: self._ptr[0].chassisPhysicalSlotNumber = val @property - def compute_slot_ind_ex(self): + def compute_slot_index(self): """int: The index within the compute slots in the rack containing this GPU (does not include switches)""" return self._ptr[0].computeSlotIndex - @compute_slot_ind_ex.setter - def compute_slot_ind_ex(self, val): + @compute_slot_index.setter + def compute_slot_index(self, val): if self._readonly: raise ValueError("This PlatformInfo_v1 instance is read-only") self._ptr[0].computeSlotIndex = val @property - def node_ind_ex(self): + def node_index(self): """int: Index of the node within the slot containing this GPU.""" return self._ptr[0].nodeIndex - @node_ind_ex.setter - def node_ind_ex(self, val): + @node_index.setter + def node_index(self, val): if self._readonly: raise ValueError("This PlatformInfo_v1 instance is read-only") self._ptr[0].nodeIndex = val @@ -6055,7 +6029,7 @@ cdef class PlatformInfo_v1: cdef _get_platform_info_v2_dtype_offsets(): cdef nvmlPlatformInfo_v2_t pod = nvmlPlatformInfo_v2_t() return _numpy.dtype({ - 'names': ['version', 'ib_guid', 'chassis_serial_number', 'slot_number', 'tray_ind_ex', 'host_id', 'peer_type', 'module_id'], + 'names': ['version', 'ib_guid', 'chassis_serial_number', 'slot_number', 'tray_index', 'host_id', 'peer_type', 'module_id'], 'formats': [_numpy.uint32, (_numpy.uint8, 16), (_numpy.uint8, 16), _numpy.uint8, _numpy.uint8, _numpy.uint8, _numpy.uint8, _numpy.uint8], 'offsets': [ (&(pod.version)) - (&pod), @@ -6195,12 +6169,12 @@ cdef class PlatformInfo_v2: self._ptr[0].slotNumber = val @property - def tray_ind_ex(self): + def tray_index(self): """int: The tray index within the compute slots in the chassis containing this GPU (does not include switches)""" return self._ptr[0].trayIndex - @tray_ind_ex.setter - def tray_ind_ex(self, val): + @tray_index.setter + def tray_index(self, val): if self._readonly: raise ValueError("This PlatformInfo_v2 instance is read-only") self._ptr[0].trayIndex = val @@ -6792,21 +6766,17 @@ vgpu_process_utilization_info_v1_dtype = _get_vgpu_process_utilization_info_v1_d cdef class VgpuProcessUtilizationInfo_v1: """Empty-initialize an array of `nvmlVgpuProcessUtilizationInfo_v1_t`. - The resulting object is of length `size` and of dtype `vgpu_process_utilization_info_v1_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlVgpuProcessUtilizationInfo_v1_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=vgpu_process_utilization_info_v1_dtype) self._data = arr.view(_numpy.recarray) @@ -7313,21 +7283,17 @@ vgpu_scheduler_log_entry_dtype = _get_vgpu_scheduler_log_entry_dtype_offsets() cdef class VgpuSchedulerLogEntry: """Empty-initialize an array of `nvmlVgpuSchedulerLogEntry_t`. - The resulting object is of length `size` and of dtype `vgpu_scheduler_log_entry_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlVgpuSchedulerLogEntry_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=vgpu_scheduler_log_entry_dtype) self._data = arr.view(_numpy.recarray) @@ -8425,6 +8391,7 @@ cdef class VgpuTypeIdInfo_v1: object _owner bint _owned bint _readonly + dict _refs def __init__(self): self._ptr = calloc(1, sizeof(nvmlVgpuTypeIdInfo_v1_t)) @@ -8433,6 +8400,7 @@ cdef class VgpuTypeIdInfo_v1: self._owner = None self._owned = True self._readonly = False + self._refs = {} def __dealloc__(self): cdef nvmlVgpuTypeIdInfo_v1_t *ptr @@ -8548,6 +8516,7 @@ cdef class VgpuTypeIdInfo_v1: obj._owner = owner obj._owned = False obj._readonly = readonly + obj._refs = {} return obj @@ -8577,6 +8546,7 @@ cdef class ActiveVgpuInstanceInfo_v1: object _owner bint _owned bint _readonly + dict _refs def __init__(self): self._ptr = calloc(1, sizeof(nvmlActiveVgpuInstanceInfo_v1_t)) @@ -8585,6 +8555,7 @@ cdef class ActiveVgpuInstanceInfo_v1: self._owner = None self._owned = True self._readonly = False + self._refs = {} def __dealloc__(self): cdef nvmlActiveVgpuInstanceInfo_v1_t *ptr @@ -8700,6 +8671,7 @@ cdef class ActiveVgpuInstanceInfo_v1: obj._owner = owner obj._owned = False obj._readonly = readonly + obj._refs = {} return obj @@ -8898,21 +8870,17 @@ hwbc_entry_dtype = _get_hwbc_entry_dtype_offsets() cdef class HwbcEntry: """Empty-initialize an array of `nvmlHwbcEntry_t`. - The resulting object is of length `size` and of dtype `hwbc_entry_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlHwbcEntry_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=hwbc_entry_dtype) self._data = arr.view(_numpy.recarray) @@ -9554,21 +9522,17 @@ unit_fan_info_dtype = _get_unit_fan_info_dtype_offsets() cdef class UnitFanInfo: """Empty-initialize an array of `nvmlUnitFanInfo_t`. - The resulting object is of length `size` and of dtype `unit_fan_info_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlUnitFanInfo_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=unit_fan_info_dtype) self._data = arr.view(_numpy.recarray) @@ -9890,21 +9854,17 @@ system_event_data_v1_dtype = _get_system_event_data_v1_dtype_offsets() cdef class SystemEventData_v1: """Empty-initialize an array of `nvmlSystemEventData_v1_t`. - The resulting object is of length `size` and of dtype `system_event_data_v1_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlSystemEventData_v1_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=system_event_data_v1_dtype) self._data = arr.view(_numpy.recarray) @@ -10245,21 +10205,17 @@ encoder_session_info_dtype = _get_encoder_session_info_dtype_offsets() cdef class EncoderSessionInfo: """Empty-initialize an array of `nvmlEncoderSessionInfo_t`. - The resulting object is of length `size` and of dtype `encoder_session_info_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlEncoderSessionInfo_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=encoder_session_info_dtype) self._data = arr.view(_numpy.recarray) @@ -10633,21 +10589,17 @@ fbc_session_info_dtype = _get_fbc_session_info_dtype_offsets() cdef class FBCSessionInfo: """Empty-initialize an array of `nvmlFBCSessionInfo_t`. - The resulting object is of length `size` and of dtype `fbc_session_info_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlFBCSessionInfo_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=fbc_session_info_dtype) self._data = arr.view(_numpy.recarray) @@ -13072,21 +13024,17 @@ gpu_instance_placement_dtype = _get_gpu_instance_placement_dtype_offsets() cdef class GpuInstancePlacement: """Empty-initialize an array of `nvmlGpuInstancePlacement_t`. - The resulting object is of length `size` and of dtype `gpu_instance_placement_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlGpuInstancePlacement_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=gpu_instance_placement_dtype) self._data = arr.view(_numpy.recarray) @@ -13508,21 +13456,17 @@ compute_instance_placement_dtype = _get_compute_instance_placement_dtype_offsets cdef class ComputeInstancePlacement: """Empty-initialize an array of `nvmlComputeInstancePlacement_t`. - The resulting object is of length `size` and of dtype `compute_instance_placement_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlComputeInstancePlacement_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=compute_instance_placement_dtype) self._data = arr.view(_numpy.recarray) @@ -14645,21 +14589,17 @@ ecc_sram_unique_uncorrected_error_entry_v1_dtype = _get_ecc_sram_unique_uncorrec cdef class EccSramUniqueUncorrectedErrorEntry_v1: """Empty-initialize an array of `nvmlEccSramUniqueUncorrectedErrorEntry_v1_t`. - The resulting object is of length `size` and of dtype `ecc_sram_unique_uncorrected_error_entry_v1_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlEccSramUniqueUncorrectedErrorEntry_v1_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=ecc_sram_unique_uncorrected_error_entry_v1_dtype) self._data = arr.view(_numpy.recarray) @@ -15679,21 +15619,17 @@ vgpu_scheduler_log_entry_v2_dtype = _get_vgpu_scheduler_log_entry_v2_dtype_offse cdef class VgpuSchedulerLogEntry_v2: """Empty-initialize an array of `nvmlVgpuSchedulerLogEntry_v2_t`. - The resulting object is of length `size` and of dtype `vgpu_scheduler_log_entry_v2_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlVgpuSchedulerLogEntry_v2_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=vgpu_scheduler_log_entry_v2_dtype) self._data = arr.view(_numpy.recarray) @@ -16508,21 +16444,17 @@ sample_dtype = _get_sample_dtype_offsets() cdef class Sample: """Empty-initialize an array of `nvmlSample_t`. - The resulting object is of length `size` and of dtype `sample_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlSample_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=sample_dtype) self._data = arr.view(_numpy.recarray) @@ -16667,21 +16599,17 @@ vgpu_instance_utilization_sample_dtype = _get_vgpu_instance_utilization_sample_d cdef class VgpuInstanceUtilizationSample: """Empty-initialize an array of `nvmlVgpuInstanceUtilizationSample_t`. - The resulting object is of length `size` and of dtype `vgpu_instance_utilization_sample_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlVgpuInstanceUtilizationSample_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=vgpu_instance_utilization_sample_dtype) self._data = arr.view(_numpy.recarray) @@ -16866,21 +16794,17 @@ vgpu_instance_utilization_info_v1_dtype = _get_vgpu_instance_utilization_info_v1 cdef class VgpuInstanceUtilizationInfo_v1: """Empty-initialize an array of `nvmlVgpuInstanceUtilizationInfo_v1_t`. - The resulting object is of length `size` and of dtype `vgpu_instance_utilization_info_v1_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlVgpuInstanceUtilizationInfo_v1_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=vgpu_instance_utilization_info_v1_dtype) self._data = arr.view(_numpy.recarray) @@ -17082,21 +17006,17 @@ field_value_dtype = _get_field_value_dtype_offsets() cdef class FieldValue: """Empty-initialize an array of `nvmlFieldValue_t`. - The resulting object is of length `size` and of dtype `field_value_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlFieldValue_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=field_value_dtype) self._data = arr.view(_numpy.recarray) @@ -18210,7 +18130,6 @@ vgpu_scheduler_params_dtype = _numpy.dtype(( } )) - cdef class VgpuSchedulerParams: """Empty-initialize an instance of `nvmlVgpuSchedulerParams_t`. @@ -18350,7 +18269,6 @@ vgpu_scheduler_set_params_dtype = _numpy.dtype(( } )) - cdef class VgpuSchedulerSetParams: """Empty-initialize an instance of `nvmlVgpuSchedulerSetParams_t`. @@ -18658,21 +18576,17 @@ grid_licensable_feature_dtype = _get_grid_licensable_feature_dtype_offsets() cdef class GridLicensableFeature: """Empty-initialize an array of `nvmlGridLicensableFeature_t`. - The resulting object is of length `size` and of dtype `grid_licensable_feature_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlGridLicensableFeature_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=grid_licensable_feature_dtype) self._data = arr.view(_numpy.recarray) @@ -20229,21 +20143,17 @@ prm_counter_v1_dtype = _get_prm_counter_v1_dtype_offsets() cdef class PRMCounter_v1: """Empty-initialize an array of `nvmlPRMCounter_v1_t`. - The resulting object is of length `size` and of dtype `prm_counter_v1_dtype`. If default-constructed, the instance represents a single struct. Args: size (int): number of structs, default=1. - .. seealso:: `nvmlPRMCounter_v1_t` """ cdef: readonly object _data - - def __init__(self, size=1): arr = _numpy.empty(size, dtype=prm_counter_v1_dtype) self._data = arr.view(_numpy.recarray) @@ -21601,7 +21511,6 @@ cdef class NvLinkInfo_v2: return obj - cpdef init_v2(): """Initialize NVML, but don't initialize any GPUs yet. @@ -21651,6 +21560,9 @@ cpdef str error_string(int result): cpdef str system_get_driver_version(): """Retrieves the version of the system's graphics driver. + Returns: + char: Reference in which to return the version identifier. + .. seealso:: `nvmlSystemGetDriverVersion` """ cdef unsigned int length = 80 @@ -21664,6 +21576,9 @@ cpdef str system_get_driver_version(): cpdef str system_get_nvml_version(): """Retrieves the version of the NVML library. + Returns: + char: Reference in which to return the version identifier. + .. seealso:: `nvmlSystemGetNVMLVersion` """ cdef unsigned int length = 80 @@ -21710,6 +21625,9 @@ cpdef str system_get_process_name(unsigned int pid): Args: pid (unsigned int): The identifier of the process. + Returns: + char: Reference in which to return the process name. + .. seealso:: `nvmlSystemGetProcessName` """ cdef unsigned int length = 1024 @@ -21723,6 +21641,9 @@ cpdef str system_get_process_name(unsigned int pid): cpdef object system_get_hic_version(): """Retrieves the IDs and firmware versions for any Host Interface Cards (HICs) in the system. + Returns: + nvmlHwbcEntry_t: Array holding information about hwbc. + .. seealso:: `nvmlSystemGetHicVersion` """ cdef unsigned int[1] hwbc_count = [0] @@ -21754,11 +21675,11 @@ cpdef unsigned int unit_get_count() except? 0: return unit_count -cpdef intptr_t unit_get_handle_by_index(unsigned int ind_ex) except? 0: - """Acquire the handle for a particular unit, based on its ind_ex. +cpdef intptr_t unit_get_handle_by_index(unsigned int index) except? 0: + """Acquire the handle for a particular unit, based on its index. Args: - ind_ex (unsigned int): The ind_ex of the target unit, >= 0 and < ``unitCount``. + index (unsigned int): The index of the target unit, >= 0 and < ``unitCount``. Returns: intptr_t: Reference in which to return the unit handle. @@ -21767,7 +21688,7 @@ cpdef intptr_t unit_get_handle_by_index(unsigned int ind_ex) except? 0: """ cdef Unit unit with nogil: - __status__ = nvmlUnitGetHandleByIndex(ind_ex, &unit) + __status__ = nvmlUnitGetHandleByIndex(index, &unit) check_status(__status__) return unit @@ -21901,11 +21822,11 @@ cpdef object device_get_attributes_v2(intptr_t device): return attributes_py -cpdef intptr_t device_get_handle_by_index_v2(unsigned int ind_ex) except? 0: - """Acquire the handle for a particular device, based on its ind_ex. +cpdef intptr_t device_get_handle_by_index_v2(unsigned int index) except? 0: + """Acquire the handle for a particular device, based on its index. Args: - ind_ex (unsigned int): The ind_ex of the target GPU, >= 0 and < ``accessibleDevices``. + index (unsigned int): The index of the target GPU, >= 0 and < ``accessibleDevices``. Returns: intptr_t: Reference in which to return the device handle. @@ -21914,7 +21835,7 @@ cpdef intptr_t device_get_handle_by_index_v2(unsigned int ind_ex) except? 0: """ cdef Device device with nogil: - __status__ = nvmlDeviceGetHandleByIndex_v2(ind_ex, &device) + __status__ = nvmlDeviceGetHandleByIndex_v2(index, &device) check_status(__status__) return device @@ -21991,6 +21912,9 @@ cpdef str device_get_name(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + char: Reference in which to return the product name. + .. seealso:: `nvmlDeviceGetName` """ cdef unsigned int length = 96 @@ -22030,11 +21954,11 @@ cpdef unsigned int device_get_index(intptr_t device) except? 0: .. seealso:: `nvmlDeviceGetIndex` """ - cdef unsigned int ind_ex + cdef unsigned int index with nogil: - __status__ = nvmlDeviceGetIndex(device, &ind_ex) + __status__ = nvmlDeviceGetIndex(device, &index) check_status(__status__) - return ind_ex + return index cpdef str device_get_serial(intptr_t device): @@ -22043,6 +21967,9 @@ cpdef str device_get_serial(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + char: Reference in which to return the board/module serial number. + .. seealso:: `nvmlDeviceGetSerial` """ cdef unsigned int length = 30 @@ -22095,8 +22022,11 @@ cpdef object device_get_memory_affinity(intptr_t device, unsigned int node_set_s Args: device (intptr_t): The identifier of the target device. - node_set_size (unsigned int): The size of the nodeSet array that is safe to access. - scope (unsigned int): Array reference in which to return a bitmask of NODEs, 64 NODEs per unsigned long on 64-bit machines, 32 on 32-bit machines. + node_set_size (unsigned int): The size of the node_set array that is safe to access. + scope (unsigned int): Scope that change the default behavior. + + Returns: + unsigned long: Array reference in which to return a bitmask of NODEs, 64 NODEs per unsigned long on 64-bit machines, 32 on 32-bit machines. .. seealso:: `nvmlDeviceGetMemoryAffinity` """ @@ -22115,8 +22045,11 @@ cpdef object device_get_cpu_affinity_within_scope(intptr_t device, unsigned int Args: device (intptr_t): The identifier of the target device. - cpu_set_size (unsigned int): The size of the cpuSet array that is safe to access. - scope (unsigned int): Array reference in which to return a bitmask of CPUs, 64 CPUs per unsigned long on 64-bit machines, 32 on 32-bit machines. + cpu_set_size (unsigned int): The size of the cpu_set array that is safe to access. + scope (unsigned int): Scope that change the default behavior. + + Returns: + unsigned long: Array reference in which to return a bitmask of CPUs, 64 CPUs per unsigned long on 64-bit machines, 32 on 32-bit machines. .. seealso:: `nvmlDeviceGetCpuAffinityWithinScope` """ @@ -22135,7 +22068,10 @@ cpdef object device_get_cpu_affinity(intptr_t device, unsigned int cpu_set_size) Args: device (intptr_t): The identifier of the target device. - cpu_set_size (unsigned int): The size of the cpuSet array that is safe to access. + cpu_set_size (unsigned int): The size of the cpu_set array that is safe to access. + + Returns: + unsigned long: Array reference in which to return a bitmask of CPUs, 64 CPUs per unsigned long on 64-bit machines, 32 on 32-bit machines. .. seealso:: `nvmlDeviceGetCpuAffinity` """ @@ -22212,22 +22148,22 @@ cpdef int device_get_topology_common_ancestor(intptr_t device1, intptr_t device2 return path_info -cpdef int device_get_p2p_status(intptr_t device1, intptr_t device2, int p2p_ind_ex) except? -1: +cpdef int device_get_p2p_status(intptr_t device1, intptr_t device2, int p2p_index) except? -1: """Retrieve the status for a given p2p capability index between a given pair of GPU. Args: device1 (intptr_t): The first device. device2 (intptr_t): The second device. - p2p_ind_ex (GpuP2PCapsIndex): p2p Capability Index being looked for between ``device1`` and ``device2``. + p2p_index (GpuP2PCapsIndex): p2p Capability Index being looked for between ``device1`` and ``device2``. Returns: - int: Reference in which to return the status of the ``p2p_ind_ex`` between ``device1`` and ``device2``. + int: Reference in which to return the status of the ``p2p_index`` between ``device1`` and ``device2``. .. seealso:: `nvmlDeviceGetP2PStatus` """ cdef _GpuP2PStatus p2p_status with nogil: - __status__ = nvmlDeviceGetP2PStatus(device1, device2, <_GpuP2PCapsIndex>p2p_ind_ex, &p2p_status) + __status__ = nvmlDeviceGetP2PStatus(device1, device2, <_GpuP2PCapsIndex>p2p_index, &p2p_status) check_status(__status__) return p2p_status @@ -22238,6 +22174,9 @@ cpdef str device_get_uuid(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + char: Reference in which to return the GPU UUID. + .. seealso:: `nvmlDeviceGetUUID` """ cdef unsigned int length = 96 @@ -22272,6 +22211,9 @@ cpdef str device_get_board_part_number(intptr_t device): Args: device (intptr_t): Identifier of the target device. + Returns: + char: Reference to the buffer to return. + .. seealso:: `nvmlDeviceGetBoardPartNumber` """ cdef unsigned int length = 80 @@ -22289,6 +22231,9 @@ cpdef str device_get_inforom_version(intptr_t device, int object): device (intptr_t): The identifier of the target device. object (InforomObject): The target infoROM object. + Returns: + char: Reference in which to return the infoROM version. + .. seealso:: `nvmlDeviceGetInforomVersion` """ cdef unsigned int length = 16 @@ -22305,6 +22250,9 @@ cpdef str device_get_inforom_image_version(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + char: Reference in which to return the infoROM image version. + .. seealso:: `nvmlDeviceGetInforomImageVersion` """ cdef unsigned int length = 16 @@ -22689,6 +22637,9 @@ cpdef object device_get_supported_memory_clocks(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + unsigned int: Reference in which to return the clock in MHz. + .. seealso:: `nvmlDeviceGetSupportedMemoryClocks` """ cdef unsigned int[1] count = [0] @@ -22712,6 +22663,9 @@ cpdef object device_get_supported_graphics_clocks(intptr_t device, unsigned int device (intptr_t): The identifier of the target device. memory_clock_m_hz (unsigned int): Memory clock for which to return possible graphics clocks. + Returns: + unsigned int: Reference in which to return the clocks in MHz. + .. seealso:: `nvmlDeviceGetSupportedGraphicsClocks` """ cdef unsigned int[1] count = [0] @@ -22729,7 +22683,7 @@ cpdef object device_get_supported_graphics_clocks(intptr_t device, unsigned int cpdef tuple device_get_auto_boosted_clocks_enabled(intptr_t device): - """Retrieve the current state of Auto Boosted clocks on a device and store it in ``isEnabled``. + """Retrieve the current state of Auto Boosted clocks on a device and store it in ``is_enabled``. Args: device (intptr_t): The identifier of the target device. @@ -22904,12 +22858,12 @@ cpdef unsigned int device_get_temperature_threshold(intptr_t device, int thresho return temp -cpdef object device_get_thermal_settings(intptr_t device, unsigned int sensor_ind_ex): +cpdef object device_get_thermal_settings(intptr_t device, unsigned int sensor_index): """Used to execute a list of thermal system instructions. Args: device (intptr_t): The identifier of the target device. - sensor_ind_ex (unsigned int): The index of the thermal sensor. + sensor_index (unsigned int): The index of the thermal sensor. Returns: nvmlGpuThermalSettings_t: Reference in which to return the thermal sensor information. @@ -22919,7 +22873,7 @@ cpdef object device_get_thermal_settings(intptr_t device, unsigned int sensor_in cdef GpuThermalSettings p_thermal_settings_py = GpuThermalSettings() cdef nvmlGpuThermalSettings_t *p_thermal_settings = (p_thermal_settings_py._get_ptr()) with nogil: - __status__ = nvmlDeviceGetThermalSettings(device, sensor_ind_ex, p_thermal_settings) + __status__ = nvmlDeviceGetThermalSettings(device, sensor_index, p_thermal_settings) check_status(__status__) return p_thermal_settings_py @@ -23350,7 +23304,7 @@ cpdef int device_get_default_ecc_mode(intptr_t device) except? -1: cpdef unsigned int device_get_board_id(intptr_t device) except? 0: - """Retrieves the device boardId from 0-N. Devices with the same boardId indicate GPUs connected to the same PLX. Use in conjunction with :func:`device_get_multi_gpu_board` to decide if they are on the same board as well. The boardId returned is a unique ID for the current configuration. Uniqueness and ordering across reboots and system configurations is not guaranteed (i.e. if a Tesla K40c returns 0x100 and the two GPUs on a Tesla K10 in the same system returns 0x200 it is not guaranteed they will always return those values but they will always be different from each other). + """Retrieves the device board_id from 0-N. Devices with the same board_id indicate GPUs connected to the same PLX. Use in conjunction with :func:`device_get_multi_gpu_board` to decide if they are on the same board as well. The board_id returned is a unique ID for the current configuration. Uniqueness and ordering across reboots and system configurations is not guaranteed (i.e. if a Tesla K40c returns 0x100 and the two GPUs on a Tesla K10 in the same system returns 0x200 it is not guaranteed they will always return those values but they will always be different from each other). Args: device (intptr_t): The identifier of the target device. @@ -23368,7 +23322,7 @@ cpdef unsigned int device_get_board_id(intptr_t device) except? 0: cpdef unsigned int device_get_multi_gpu_board(intptr_t device) except? 0: - """Retrieves whether the device is on a Multi-GPU Board Devices that are on multi-GPU boards will set ``multiGpuBool`` to a non-zero value. + """Retrieves whether the device is on a Multi-GPU Board Devices that are on multi-GPU boards will set ``multi_gpu_bool`` to a non-zero value. Args: device (intptr_t): The identifier of the target device. @@ -23516,6 +23470,9 @@ cpdef object device_get_encoder_sessions(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + nvmlEncoderSessionInfo_t: Reference in which to return the session information. + .. seealso:: `nvmlDeviceGetEncoderSessions` """ cdef unsigned int[1] session_count = [0] @@ -23623,6 +23580,9 @@ cpdef object device_get_fbc_sessions(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + nvmlFBCSessionInfo_t: Reference in which to return the session information. + .. seealso:: `nvmlDeviceGetFBCSessions` """ cdef unsigned int[1] session_count = [0] @@ -23667,6 +23627,9 @@ cpdef str device_get_vbios_version(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + char: Reference to which to return the VBIOS version. + .. seealso:: `nvmlDeviceGetVbiosVersion` """ cdef unsigned int length = 32 @@ -23702,6 +23665,9 @@ cpdef object device_get_compute_running_processes_v3(intptr_t device): Args: device (intptr_t): The device handle or MIG device handle. + Returns: + nvmlProcessInfo_t: Reference in which to return the process information. + .. seealso:: `nvmlDeviceGetComputeRunningProcesses_v3` """ cdef unsigned int[1] info_count = [0] @@ -23724,6 +23690,9 @@ cpdef object device_get_graphics_running_processes_v3(intptr_t device): Args: device (intptr_t): The device handle or MIG device handle. + Returns: + nvmlProcessInfo_t: Reference in which to return the process information. + .. seealso:: `nvmlDeviceGetGraphicsRunningProcesses_v3` """ cdef unsigned int[1] info_count = [0] @@ -23746,6 +23715,9 @@ cpdef object device_get_mps_compute_running_processes_v3(intptr_t device): Args: device (intptr_t): The device handle or MIG device handle. + Returns: + nvmlProcessInfo_t: Reference in which to return the process information. + .. seealso:: `nvmlDeviceGetMPSComputeRunningProcesses_v3` """ cdef unsigned int[1] info_count = [0] @@ -24215,6 +24187,9 @@ cpdef object device_get_accounting_pids(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + unsigned int: Reference in which to return list of process ids. + .. seealso:: `nvmlDeviceGetAccountingPids` """ cdef unsigned int[1] count = [0] @@ -24256,6 +24231,9 @@ cpdef object device_get_retired_pages(intptr_t device, int cause): device (intptr_t): The identifier of the target device. cause (PageRetirementCause): Filter page addresses by cause of retirement. + Returns: + unsigned long long: Buffer to write the page addresses into. + .. seealso:: `nvmlDeviceGetRetiredPages` """ cdef unsigned int[1] page_count = [0] @@ -24291,7 +24269,7 @@ cpdef int device_get_retired_pages_pending_status(intptr_t device) except? -1: cpdef tuple device_get_remapped_rows(intptr_t device): - """Get number of remapped rows. The number of rows reported will be based on the cause of the remapping. isPending indicates whether or not there are pending remappings. A reset will be required to actually remap the row. failureOccurred will be set if a row remapping ever failed in the past. A pending remapping won't affect future work on the GPU since error-containment and dynamic page blacklisting will take care of that. + """Get number of remapped rows. The number of rows reported will be based on the cause of the remapping. is_pending indicates whether or not there are pending remappings. A reset will be required to actually remap the row. failure_occurred will be set if a row remapping ever failed in the past. A pending remapping won't affect future work on the GPU since error-containment and dynamic page blacklisting will take care of that. Args: device (intptr_t): The identifier of the target device. @@ -24377,7 +24355,10 @@ cpdef object device_get_process_utilization(intptr_t device, unsigned long long Args: device (intptr_t): The identifier of the target device. - last_seen_time_stamp (unsigned long long): Pointer to caller-supplied buffer in which guest process utilization samples are returned. + last_seen_time_stamp (unsigned long long): Return only samples with timestamp greater than last_seen_time_stamp. + + Returns: + nvmlProcessUtilizationSample_t: Pointer to caller-supplied buffer in which guest process utilization samples are returned. .. seealso:: `nvmlDeviceGetProcessUtilization` """ @@ -25183,6 +25164,9 @@ cpdef str vgpu_type_get_class(unsigned int vgpu_type_id): Args: vgpu_type_id (unsigned int): Handle to vGPU type. + Returns: + char: Pointer to string array to return class in. + .. seealso:: `nvmlVgpuTypeGetClass` """ cdef unsigned int[1] size = [0] @@ -25275,12 +25259,12 @@ cpdef unsigned int vgpu_type_get_num_display_heads(unsigned int vgpu_type_id) ex return num_display_heads -cpdef tuple vgpu_type_get_resolution(unsigned int vgpu_type_id, unsigned int display_ind_ex): +cpdef tuple vgpu_type_get_resolution(unsigned int vgpu_type_id, unsigned int display_index): """Retrieve vGPU display head's maximum supported resolution. Args: vgpu_type_id (unsigned int): Handle to vGPU type. - display_ind_ex (unsigned int): Zero-based index of display head. + display_index (unsigned int): Zero-based index of display head. Returns: A 2-tuple containing: @@ -25293,7 +25277,7 @@ cpdef tuple vgpu_type_get_resolution(unsigned int vgpu_type_id, unsigned int dis cdef unsigned int xdim cdef unsigned int ydim with nogil: - __status__ = nvmlVgpuTypeGetResolution(vgpu_type_id, display_ind_ex, &xdim, &ydim) + __status__ = nvmlVgpuTypeGetResolution(vgpu_type_id, display_index, &xdim, &ydim) check_status(__status__) return (xdim, ydim) @@ -25304,6 +25288,9 @@ cpdef str vgpu_type_get_license(unsigned int vgpu_type_id): Args: vgpu_type_id (unsigned int): Handle to vGPU type. + Returns: + char: Pointer to buffer to return license info. + .. seealso:: `nvmlVgpuTypeGetLicense` """ cdef unsigned int size = 128 @@ -25395,6 +25382,9 @@ cpdef str vgpu_instance_get_uuid(unsigned int vgpu_instance): Args: vgpu_instance (unsigned int): Identifier of the target vGPU instance. + Returns: + char: Pointer to caller-supplied buffer to hold vGPU UUID. + .. seealso:: `nvmlVgpuInstanceGetUUID` """ cdef unsigned int size = 80 @@ -25411,6 +25401,9 @@ cpdef str vgpu_instance_get_vm_driver_version(unsigned int vgpu_instance): Args: vgpu_instance (unsigned int): Identifier of the target vGPU instance. + Returns: + char: Caller-supplied buffer to return driver version string. + .. seealso:: `nvmlVgpuInstanceGetVmDriverVersion` """ cdef unsigned int length = 80 @@ -25464,7 +25457,7 @@ cpdef unsigned int vgpu_instance_get_type(unsigned int vgpu_instance) except? 0: vgpu_instance (unsigned int): Identifier of the target vGPU instance. Returns: - unsigned int: Reference to return the vgpuTypeId. + unsigned int: Reference to return the vgpu_type_id. .. seealso:: `nvmlVgpuInstanceGetType` """ @@ -25573,6 +25566,9 @@ cpdef object vgpu_instance_get_encoder_sessions(unsigned int vgpu_instance): Args: vgpu_instance (unsigned int): Identifier of the target vGPU instance. + Returns: + nvmlEncoderSessionInfo_t: Reference to caller supplied array in which the list of session information us returned. + .. seealso:: `nvmlVgpuInstanceGetEncoderSessions` """ cdef unsigned int[1] session_count = [0] @@ -25614,6 +25610,9 @@ cpdef object vgpu_instance_get_fbc_sessions(unsigned int vgpu_instance): Args: vgpu_instance (unsigned int): Identifier of the target vGPU instance. + Returns: + nvmlFBCSessionInfo_t: Reference in which to return the session information. + .. seealso:: `nvmlVgpuInstanceGetFBCSessions` """ cdef unsigned int[1] session_count = [0] @@ -25654,6 +25653,9 @@ cpdef str vgpu_instance_get_gpu_pci_id(unsigned int vgpu_instance): Args: vgpu_instance (unsigned int): Identifier of the target vGPU instance. + Returns: + char: Caller-supplied buffer to return vGPU PCI Id string. + .. seealso:: `nvmlVgpuInstanceGetGpuPciId` """ cdef unsigned int[1] length = [0] @@ -25671,7 +25673,7 @@ cpdef str vgpu_instance_get_gpu_pci_id(unsigned int vgpu_instance): cpdef unsigned int vgpu_type_get_capabilities(unsigned int vgpu_type_id, int capability) except? 0: - """Retrieve the requested capability for a given vGPU type. Refer to the ``nvmlVgpuCapability_t`` structure for the specific capabilities that can be queried. The return value in ``capResult`` should be treated as a boolean, with a non-zero value indicating that the capability is supported. + """Retrieve the requested capability for a given vGPU type. Refer to the ``nvmlVgpuCapability_t`` structure for the specific capabilities that can be queried. The return value in ``cap_result`` should be treated as a boolean, with a non-zero value indicating that the capability is supported. Args: vgpu_type_id (unsigned int): Handle to vGPU type. @@ -25695,6 +25697,9 @@ cpdef str vgpu_instance_get_mdev_uuid(unsigned int vgpu_instance): Args: vgpu_instance (unsigned int): Identifier of the target vGPU instance. + Returns: + char: Pointer to caller-supplied buffer to hold MDEV UUID. + .. seealso:: `nvmlVgpuInstanceGetMdevUUID` """ cdef unsigned int size = 80 @@ -25727,7 +25732,7 @@ cpdef object gpu_instance_get_vgpu_scheduler_state(intptr_t gpu_instance): gpu_instance (intptr_t): The GPU instance handle. Returns: - nvmlVgpuSchedulerStateInfo_v1_t: Reference in which ``pSchedulerStateInfo`` is returned. + nvmlVgpuSchedulerStateInfo_v1_t: Reference in which ``p_scheduler_state_info`` is returned. .. seealso:: `nvmlGpuInstanceGetVgpuSchedulerState` """ @@ -25741,13 +25746,13 @@ cpdef object gpu_instance_get_vgpu_scheduler_state(intptr_t gpu_instance): cpdef object gpu_instance_get_vgpu_scheduler_log(intptr_t gpu_instance): - """Returns the vGPU scheduler logs for the given GPU instance. ``pSchedulerLogInfo`` points to a caller-allocated structure to contain the logs. The number of elements returned will never exceed ``NVML_SCHEDULER_SW_MAX_LOG_ENTRIES``. + """Returns the vGPU scheduler logs for the given GPU instance. ``p_scheduler_log_info`` points to a caller-allocated structure to contain the logs. The number of elements returned will never exceed ``NVML_SCHEDULER_SW_MAX_LOG_ENTRIES``. Args: gpu_instance (intptr_t): The GPU instance handle. Returns: - nvmlVgpuSchedulerLogInfo_v1_t: Reference in which ``pSchedulerLogInfo`` is written. + nvmlVgpuSchedulerLogInfo_v1_t: Reference in which ``p_scheduler_log_info`` is written. .. seealso:: `nvmlGpuInstanceGetVgpuSchedulerLog` """ @@ -25766,6 +25771,9 @@ cpdef str device_get_pgpu_metadata_string(intptr_t device): Args: device (intptr_t): The identifier of the target device. + Returns: + char: Pointer to caller-supplied buffer into which ``pgpu_metadata`` is written. + .. seealso:: `nvmlDeviceGetPgpuMetadataString` """ cdef unsigned int[1] buffer_size = [0] @@ -25783,13 +25791,13 @@ cpdef str device_get_pgpu_metadata_string(intptr_t device): cpdef object device_get_vgpu_scheduler_log(intptr_t device): - """Returns the vGPU Software scheduler logs. ``pSchedulerLog`` points to a caller-allocated structure to contain the logs. The number of elements returned will never exceed ``NVML_SCHEDULER_SW_MAX_LOG_ENTRIES``. + """Returns the vGPU Software scheduler logs. ``p_scheduler_log`` points to a caller-allocated structure to contain the logs. The number of elements returned will never exceed ``NVML_SCHEDULER_SW_MAX_LOG_ENTRIES``. Args: device (intptr_t): The identifier of the target ``device``. Returns: - nvmlVgpuSchedulerLog_t: Reference in which ``pSchedulerLog`` is written. + nvmlVgpuSchedulerLog_t: Reference in which ``p_scheduler_log`` is written. .. seealso:: `nvmlDeviceGetVgpuSchedulerLog` """ @@ -25808,7 +25816,7 @@ cpdef object device_get_vgpu_scheduler_state(intptr_t device): device (intptr_t): The identifier of the target ``device``. Returns: - nvmlVgpuSchedulerGetState_t: Reference in which ``pSchedulerState`` is returned. + nvmlVgpuSchedulerGetState_t: Reference in which ``p_scheduler_state`` is returned. .. seealso:: `nvmlDeviceGetVgpuSchedulerState` """ @@ -25827,7 +25835,7 @@ cpdef object device_get_vgpu_scheduler_capabilities(intptr_t device): device (intptr_t): The identifier of the target ``device``. Returns: - nvmlVgpuSchedulerCapabilities_t: Reference in which ``pCapabilities`` is written. + nvmlVgpuSchedulerCapabilities_t: Reference in which ``p_capabilities`` is written. .. seealso:: `nvmlDeviceGetVgpuSchedulerCapabilities` """ @@ -25913,6 +25921,9 @@ cpdef object vgpu_instance_get_accounting_pids(unsigned int vgpu_instance): Args: vgpu_instance (unsigned int): The identifier of the target vGPU instance. + Returns: + unsigned int: Reference in which to return list of process ids. + .. seealso:: `nvmlVgpuInstanceGetAccountingPids` """ cdef unsigned int[1] count = [0] @@ -25996,11 +26007,11 @@ cpdef unsigned int get_excluded_device_count() except? 0: return device_count -cpdef object get_excluded_device_info_by_index(unsigned int ind_ex): - """Acquire the device information for an excluded GPU device, based on its ind_ex. +cpdef object get_excluded_device_info_by_index(unsigned int index): + """Acquire the device information for an excluded GPU device, based on its index. Args: - ind_ex (unsigned int): The ind_ex of the target GPU, >= 0 and < ``deviceCount``. + index (unsigned int): The index of the target GPU, >= 0 and < ``deviceCount``. Returns: nvmlExcludedDeviceInfo_t: Reference in which to return the device information. @@ -26010,7 +26021,7 @@ cpdef object get_excluded_device_info_by_index(unsigned int ind_ex): cdef ExcludedDeviceInfo info_py = ExcludedDeviceInfo() cdef nvmlExcludedDeviceInfo_t *info = (info_py._get_ptr()) with nogil: - __status__ = nvmlGetExcludedDeviceInfoByIndex(ind_ex, info) + __status__ = nvmlGetExcludedDeviceInfoByIndex(index, info) check_status(__status__) return info_py @@ -26023,7 +26034,7 @@ cpdef int device_set_mig_mode(intptr_t device, unsigned int mode) except? -1: mode (unsigned int): The mode to be set, ``NVML_DEVICE_MIG_DISABLE`` or ``NVML_DEVICE_MIG_ENABLE``. Returns: - int: The activationStatus status. + int: The activation_status status. .. seealso:: `nvmlDeviceSetMigMode` """ @@ -26063,6 +26074,9 @@ cpdef object device_get_gpu_instance_possible_placements_v2(intptr_t device, uns device (intptr_t): The identifier of the target device. profile_id (unsigned int): The GPU instance profile ID. See ``nvmlDeviceGetGpuInstanceProfileInfo``. + Returns: + nvmlGpuInstancePlacement_t: Returns placements allowed for the profile. Can be NULL to discover number of allowed placements for this profile. If non-NULL must be large enough to accommodate the placements supported by the profile. + .. seealso:: `nvmlDeviceGetGpuInstancePossiblePlacements_v2` """ cdef unsigned int[1] count = [0] @@ -26236,6 +26250,9 @@ cpdef object gpu_instance_get_compute_instance_possible_placements(intptr_t gpu_ gpu_instance (intptr_t): The identifier of the target GPU instance. profile_id (unsigned int): The compute instance profile ID. See ``nvmlGpuInstanceGetComputeInstanceProfileInfo``. + Returns: + nvmlComputeInstancePlacement_t: Returns placements allowed for the profile. Can be NULL to discover number of allowed placements for this profile. If non-NULL must be large enough to accommodate the placements supported by the profile. + .. seealso:: `nvmlGpuInstanceGetComputeInstancePossiblePlacements` """ cdef unsigned int[1] count = [0] @@ -26414,12 +26431,12 @@ cpdef unsigned int device_get_max_mig_device_count(intptr_t device) except? 0: return count -cpdef intptr_t device_get_mig_device_handle_by_index(intptr_t device, unsigned int ind_ex) except? 0: - """Get MIG device handle for the given ind_ex under its parent NVML device. +cpdef intptr_t device_get_mig_device_handle_by_index(intptr_t device, unsigned int index) except? 0: + """Get MIG device handle for the given index under its parent NVML device. Args: device (intptr_t): Reference to the parent GPU device handle. - ind_ex (unsigned int): Index of the MIG device. + index (unsigned int): Index of the MIG device. Returns: intptr_t: Reference to the MIG device handle. @@ -26428,7 +26445,7 @@ cpdef intptr_t device_get_mig_device_handle_by_index(intptr_t device, unsigned i """ cdef Device mig_device with nogil: - __status__ = nvmlDeviceGetMigDeviceHandleByIndex(device, ind_ex, &mig_device) + __status__ = nvmlDeviceGetMigDeviceHandleByIndex(device, index, &mig_device) check_status(__status__) return mig_device @@ -26586,7 +26603,7 @@ cpdef object device_get_vgpu_scheduler_state_v2(intptr_t device): device (intptr_t): The identifier of the target ``device``. Returns: - nvmlVgpuSchedulerStateInfo_v2_t: Reference in which ``pSchedulerStateInfo`` is returned. + nvmlVgpuSchedulerStateInfo_v2_t: Reference in which ``p_scheduler_state_info`` is returned. .. seealso:: `nvmlDeviceGetVgpuSchedulerState_v2` """ @@ -26605,7 +26622,7 @@ cpdef object gpu_instance_get_vgpu_scheduler_state_v2(intptr_t gpu_instance): gpu_instance (intptr_t): The GPU instance handle. Returns: - nvmlVgpuSchedulerStateInfo_v2_t: Reference in which ``pSchedulerStateInfo`` is returned. + nvmlVgpuSchedulerStateInfo_v2_t: Reference in which ``p_scheduler_state_info`` is returned. .. seealso:: `nvmlGpuInstanceGetVgpuSchedulerState_v2` """ @@ -26618,13 +26635,13 @@ cpdef object gpu_instance_get_vgpu_scheduler_state_v2(intptr_t gpu_instance): cpdef object device_get_vgpu_scheduler_log_v2(intptr_t device): - """Returns the vGPU Software scheduler logs for the device. ``pSchedulerLogInfo`` points to a caller-allocated structure to contain the logs. The number of elements returned will never exceed ``NVML_SCHEDULER_SW_MAX_LOG_ENTRIES``. + """Returns the vGPU Software scheduler logs for the device. ``p_scheduler_log_info`` points to a caller-allocated structure to contain the logs. The number of elements returned will never exceed ``NVML_SCHEDULER_SW_MAX_LOG_ENTRIES``. Args: device (intptr_t): The identifier of the target ``device``. Returns: - nvmlVgpuSchedulerLogInfo_v2_t: Reference in which ``pSchedulerLogInfo`` is written. + nvmlVgpuSchedulerLogInfo_v2_t: Reference in which ``p_scheduler_log_info`` is written. .. seealso:: `nvmlDeviceGetVgpuSchedulerLog_v2` """ @@ -26637,13 +26654,13 @@ cpdef object device_get_vgpu_scheduler_log_v2(intptr_t device): cpdef object gpu_instance_get_vgpu_scheduler_log_v2(intptr_t gpu_instance): - """Returns the vGPU scheduler logs for the given GPU instance. ``pSchedulerLogInfo`` points to a caller-allocated structure to contain the logs. The number of elements returned will never exceed ``NVML_SCHEDULER_SW_MAX_LOG_ENTRIES``. + """Returns the vGPU scheduler logs for the given GPU instance. ``p_scheduler_log_info`` points to a caller-allocated structure to contain the logs. The number of elements returned will never exceed ``NVML_SCHEDULER_SW_MAX_LOG_ENTRIES``. Args: gpu_instance (intptr_t): The GPU instance handle. Returns: - nvmlVgpuSchedulerLogInfo_v2_t: Reference in which ``pSchedulerLogInfo`` is written. + nvmlVgpuSchedulerLogInfo_v2_t: Reference in which ``p_scheduler_log_info`` is written. .. seealso:: `nvmlGpuInstanceGetVgpuSchedulerLog_v2` """ diff --git a/cuda_bindings/cuda/bindings/runtime.pyx.in b/cuda_bindings/cuda/bindings/runtime.pyx.in index 279a8c17595..c8f94c378b8 100644 --- a/cuda_bindings/cuda/bindings/runtime.pyx.in +++ b/cuda_bindings/cuda/bindings/runtime.pyx.in @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE -# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1568+g289771de9.d20260413. Do not modify it directly. from typing import Any, Optional import cython import ctypes @@ -21245,7 +21245,7 @@ def cudaDeviceGetLimit(limit not None : cudaLimit): def cudaDeviceGetTexture1DLinearMaxWidth(fmtDesc : Optional[cudaChannelFormatDesc], int device): """""" cdef size_t maxWidthInElements = 0 - cdef cyruntime.cudaChannelFormatDesc* cyfmtDesc_ptr = fmtDesc._pvt_ptr if fmtDesc is not None else NULL + cdef cyruntime.cudaChannelFormatDesc* cyfmtDesc_ptr = fmtDesc._pvt_ptr if fmtDesc is not None else NULL with nogil: err = cyruntime.cudaDeviceGetTexture1DLinearMaxWidth(&maxWidthInElements, cyfmtDesc_ptr, device) if err != cyruntime.cudaSuccess: @@ -22672,7 +22672,7 @@ def cudaChooseDevice(prop : Optional[cudaDeviceProp]): :py:obj:`~.cudaGetDeviceCount`, :py:obj:`~.cudaGetDevice`, :py:obj:`~.cudaSetDevice`, :py:obj:`~.cudaGetDeviceProperties`, :py:obj:`~.cudaInitDevice` """ cdef int device = 0 - cdef cyruntime.cudaDeviceProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL + cdef cyruntime.cudaDeviceProp* cyprop_ptr = prop._pvt_ptr if prop is not None else NULL with nogil: err = cyruntime.cudaChooseDevice(&device, cyprop_ptr) if err != cyruntime.cudaSuccess: @@ -23413,7 +23413,7 @@ def cudaStreamSetAttribute(hStream, attr not None : cudaStreamAttrID, value : Op phStream = int(cudaStream_t(hStream)) cyhStream = phStream cdef cyruntime.cudaStreamAttrID cyattr = int(attr) - cdef cyruntime.cudaStreamAttrValue* cyvalue_ptr = value._pvt_ptr if value is not None else NULL + cdef cyruntime.cudaStreamAttrValue* cyvalue_ptr = value._pvt_ptr if value is not None else NULL with nogil: err = cyruntime.cudaStreamSetAttribute(cyhStream, cyattr, cyvalue_ptr) return (_cudaError_t(err),) @@ -24901,7 +24901,7 @@ def cudaImportExternalMemory(memHandleDesc : Optional[cudaExternalMemoryHandleDe and Cache Control" chapter from Vulkan specification. """ cdef cudaExternalMemory_t extMem_out = cudaExternalMemory_t() - cdef cyruntime.cudaExternalMemoryHandleDesc* cymemHandleDesc_ptr = memHandleDesc._pvt_ptr if memHandleDesc is not None else NULL + cdef cyruntime.cudaExternalMemoryHandleDesc* cymemHandleDesc_ptr = memHandleDesc._pvt_ptr if memHandleDesc is not None else NULL with nogil: err = cyruntime.cudaImportExternalMemory(extMem_out._pvt_ptr, cymemHandleDesc_ptr) if err != cyruntime.cudaSuccess: @@ -24969,7 +24969,7 @@ def cudaExternalMemoryGetMappedBuffer(extMem, bufferDesc : Optional[cudaExternal pextMem = int(cudaExternalMemory_t(extMem)) cyextMem = pextMem cdef void_ptr devPtr = 0 - cdef cyruntime.cudaExternalMemoryBufferDesc* cybufferDesc_ptr = bufferDesc._pvt_ptr if bufferDesc is not None else NULL + cdef cyruntime.cudaExternalMemoryBufferDesc* cybufferDesc_ptr = bufferDesc._pvt_ptr if bufferDesc is not None else NULL with nogil: err = cyruntime.cudaExternalMemoryGetMappedBuffer(&devPtr, cyextMem, cybufferDesc_ptr) if err != cyruntime.cudaSuccess: @@ -25041,7 +25041,7 @@ def cudaExternalMemoryGetMappedMipmappedArray(extMem, mipmapDesc : Optional[cuda pextMem = int(cudaExternalMemory_t(extMem)) cyextMem = pextMem cdef cudaMipmappedArray_t mipmap = cudaMipmappedArray_t() - cdef cyruntime.cudaExternalMemoryMipmappedArrayDesc* cymipmapDesc_ptr = mipmapDesc._pvt_ptr if mipmapDesc is not None else NULL + cdef cyruntime.cudaExternalMemoryMipmappedArrayDesc* cymipmapDesc_ptr = mipmapDesc._pvt_ptr if mipmapDesc is not None else NULL with nogil: err = cyruntime.cudaExternalMemoryGetMappedMipmappedArray(mipmap._pvt_ptr, cyextMem, cymipmapDesc_ptr) if err != cyruntime.cudaSuccess: @@ -25228,7 +25228,7 @@ def cudaImportExternalSemaphore(semHandleDesc : Optional[cudaExternalSemaphoreHa :py:obj:`~.cudaDestroyExternalSemaphore`, :py:obj:`~.cudaSignalExternalSemaphoresAsync`, :py:obj:`~.cudaWaitExternalSemaphoresAsync` """ cdef cudaExternalSemaphore_t extSem_out = cudaExternalSemaphore_t() - cdef cyruntime.cudaExternalSemaphoreHandleDesc* cysemHandleDesc_ptr = semHandleDesc._pvt_ptr if semHandleDesc is not None else NULL + cdef cyruntime.cudaExternalSemaphoreHandleDesc* cysemHandleDesc_ptr = semHandleDesc._pvt_ptr if semHandleDesc is not None else NULL with nogil: err = cyruntime.cudaImportExternalSemaphore(extSem_out._pvt_ptr, cysemHandleDesc_ptr) if err != cyruntime.cudaSuccess: @@ -26544,7 +26544,7 @@ def cudaMallocArray(desc : Optional[cudaChannelFormatDesc], size_t width, size_t :py:obj:`~.cudaMalloc`, :py:obj:`~.cudaMallocPitch`, :py:obj:`~.cudaFree`, :py:obj:`~.cudaFreeArray`, :py:obj:`~.cudaMallocHost (C API)`, :py:obj:`~.cudaFreeHost`, :py:obj:`~.cudaMalloc3D`, :py:obj:`~.cudaMalloc3DArray`, :py:obj:`~.cudaHostAlloc`, :py:obj:`~.cuArrayCreate` """ cdef cudaArray_t array = cudaArray_t() - cdef cyruntime.cudaChannelFormatDesc* cydesc_ptr = desc._pvt_ptr if desc is not None else NULL + cdef cyruntime.cudaChannelFormatDesc* cydesc_ptr = desc._pvt_ptr if desc is not None else NULL with nogil: err = cyruntime.cudaMallocArray(array._pvt_ptr, cydesc_ptr, width, height, flags) if err != cyruntime.cudaSuccess: @@ -27209,7 +27209,7 @@ def cudaMalloc3DArray(desc : Optional[cudaChannelFormatDesc], extent not None : :py:obj:`~.cudaMalloc3D`, :py:obj:`~.cudaMalloc`, :py:obj:`~.cudaMallocPitch`, :py:obj:`~.cudaFree`, :py:obj:`~.cudaFreeArray`, :py:obj:`~.cudaMallocHost (C API)`, :py:obj:`~.cudaFreeHost`, :py:obj:`~.cudaHostAlloc`, make_cudaExtent, :py:obj:`~.cuArray3DCreate` """ cdef cudaArray_t array = cudaArray_t() - cdef cyruntime.cudaChannelFormatDesc* cydesc_ptr = desc._pvt_ptr if desc is not None else NULL + cdef cyruntime.cudaChannelFormatDesc* cydesc_ptr = desc._pvt_ptr if desc is not None else NULL with nogil: err = cyruntime.cudaMalloc3DArray(array._pvt_ptr, cydesc_ptr, extent._pvt_ptr[0], flags) if err != cyruntime.cudaSuccess: @@ -27335,7 +27335,7 @@ def cudaMallocMipmappedArray(desc : Optional[cudaChannelFormatDesc], extent not :py:obj:`~.cudaMalloc3D`, :py:obj:`~.cudaMalloc`, :py:obj:`~.cudaMallocPitch`, :py:obj:`~.cudaFree`, :py:obj:`~.cudaFreeArray`, :py:obj:`~.cudaMallocHost (C API)`, :py:obj:`~.cudaFreeHost`, :py:obj:`~.cudaHostAlloc`, make_cudaExtent, :py:obj:`~.cuMipmappedArrayCreate` """ cdef cudaMipmappedArray_t mipmappedArray = cudaMipmappedArray_t() - cdef cyruntime.cudaChannelFormatDesc* cydesc_ptr = desc._pvt_ptr if desc is not None else NULL + cdef cyruntime.cudaChannelFormatDesc* cydesc_ptr = desc._pvt_ptr if desc is not None else NULL with nogil: err = cyruntime.cudaMallocMipmappedArray(mipmappedArray._pvt_ptr, cydesc_ptr, extent._pvt_ptr[0], numLevels, flags) if err != cyruntime.cudaSuccess: @@ -27470,7 +27470,7 @@ def cudaMemcpy3D(p : Optional[cudaMemcpy3DParms]): -------- :py:obj:`~.cudaMalloc3D`, :py:obj:`~.cudaMalloc3DArray`, :py:obj:`~.cudaMemset3D`, :py:obj:`~.cudaMemcpy3DAsync`, :py:obj:`~.cudaMemcpy`, :py:obj:`~.cudaMemcpy2D`, :py:obj:`~.cudaMemcpy2DToArray`, :py:obj:`~.cudaMemcpy2DFromArray`, :py:obj:`~.cudaMemcpy2DArrayToArray`, :py:obj:`~.cudaMemcpyToSymbol`, :py:obj:`~.cudaMemcpyFromSymbol`, :py:obj:`~.cudaMemcpyAsync`, :py:obj:`~.cudaMemcpy2DAsync`, :py:obj:`~.cudaMemcpy2DToArrayAsync`, :py:obj:`~.cudaMemcpy2DFromArrayAsync`, :py:obj:`~.cudaMemcpyToSymbolAsync`, :py:obj:`~.cudaMemcpyFromSymbolAsync`, make_cudaExtent, make_cudaPos, :py:obj:`~.cuMemcpy3D` """ - cdef cyruntime.cudaMemcpy3DParms* cyp_ptr = p._pvt_ptr if p is not None else NULL + cdef cyruntime.cudaMemcpy3DParms* cyp_ptr = p._pvt_ptr if p is not None else NULL with nogil: err = cyruntime.cudaMemcpy3D(cyp_ptr) return (_cudaError_t(err),) @@ -27507,7 +27507,7 @@ def cudaMemcpy3DPeer(p : Optional[cudaMemcpy3DPeerParms]): -------- :py:obj:`~.cudaMemcpy`, :py:obj:`~.cudaMemcpyPeer`, :py:obj:`~.cudaMemcpyAsync`, :py:obj:`~.cudaMemcpyPeerAsync`, :py:obj:`~.cudaMemcpy3DPeerAsync`, :py:obj:`~.cuMemcpy3DPeer` """ - cdef cyruntime.cudaMemcpy3DPeerParms* cyp_ptr = p._pvt_ptr if p is not None else NULL + cdef cyruntime.cudaMemcpy3DPeerParms* cyp_ptr = p._pvt_ptr if p is not None else NULL with nogil: err = cyruntime.cudaMemcpy3DPeer(cyp_ptr) return (_cudaError_t(err),) @@ -27612,7 +27612,7 @@ def cudaMemcpy3DAsync(p : Optional[cudaMemcpy3DParms], stream): else: pstream = int(cudaStream_t(stream)) cystream = pstream - cdef cyruntime.cudaMemcpy3DParms* cyp_ptr = p._pvt_ptr if p is not None else NULL + cdef cyruntime.cudaMemcpy3DParms* cyp_ptr = p._pvt_ptr if p is not None else NULL with nogil: err = cyruntime.cudaMemcpy3DAsync(cyp_ptr, cystream) return (_cudaError_t(err),) @@ -27652,7 +27652,7 @@ def cudaMemcpy3DPeerAsync(p : Optional[cudaMemcpy3DPeerParms], stream): else: pstream = int(cudaStream_t(stream)) cystream = pstream - cdef cyruntime.cudaMemcpy3DPeerParms* cyp_ptr = p._pvt_ptr if p is not None else NULL + cdef cyruntime.cudaMemcpy3DPeerParms* cyp_ptr = p._pvt_ptr if p is not None else NULL with nogil: err = cyruntime.cudaMemcpy3DPeerAsync(cyp_ptr, cystream) return (_cudaError_t(err),) @@ -28831,7 +28831,7 @@ def cudaMemcpyWithAttributesAsync(dst, src, size_t size, attr : Optional[cudaMem cdef void* cydst = _helper_input_void_ptr(dst, &cydstHelper) cdef _HelperInputVoidPtrStruct cysrcHelper cdef void* cysrc = _helper_input_void_ptr(src, &cysrcHelper) - cdef cyruntime.cudaMemcpyAttributes* cyattr_ptr = attr._pvt_ptr if attr is not None else NULL + cdef cyruntime.cudaMemcpyAttributes* cyattr_ptr = attr._pvt_ptr if attr is not None else NULL with nogil: err = cyruntime.cudaMemcpyWithAttributesAsync(cydst, cysrc, size, cyattr_ptr, cystream) _helper_input_void_ptr_free(&cydstHelper) @@ -28881,7 +28881,7 @@ def cudaMemcpy3DWithAttributesAsync(op : Optional[cudaMemcpy3DBatchOp], unsigned else: pstream = int(cudaStream_t(stream)) cystream = pstream - cdef cyruntime.cudaMemcpy3DBatchOp* cyop_ptr = op._pvt_ptr if op is not None else NULL + cdef cyruntime.cudaMemcpy3DBatchOp* cyop_ptr = op._pvt_ptr if op is not None else NULL with nogil: err = cyruntime.cudaMemcpy3DWithAttributesAsync(cyop_ptr, flags, cystream) return (_cudaError_t(err),) @@ -31095,7 +31095,7 @@ def cudaMemPoolGetAccess(memPool, location : Optional[cudaMemLocation]): pmemPool = int(cudaMemPool_t(memPool)) cymemPool = pmemPool cdef cyruntime.cudaMemAccessFlags flags - cdef cyruntime.cudaMemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cyruntime.cudaMemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL with nogil: err = cyruntime.cudaMemPoolGetAccess(&flags, cymemPool, cylocation_ptr) if err != cyruntime.cudaSuccess: @@ -31195,7 +31195,7 @@ def cudaMemPoolCreate(poolProps : Optional[cudaMemPoolProps]): Specifying :py:obj:`~.cudaMemHandleTypeNone` creates a memory pool that will not support IPC. """ cdef cudaMemPool_t memPool = cudaMemPool_t() - cdef cyruntime.cudaMemPoolProps* cypoolProps_ptr = poolProps._pvt_ptr if poolProps is not None else NULL + cdef cyruntime.cudaMemPoolProps* cypoolProps_ptr = poolProps._pvt_ptr if poolProps is not None else NULL with nogil: err = cyruntime.cudaMemPoolCreate(memPool._pvt_ptr, cypoolProps_ptr) if err != cyruntime.cudaSuccess: @@ -31285,7 +31285,7 @@ def cudaMemGetDefaultMemPool(location : Optional[cudaMemLocation], typename not :py:obj:`~.cuMemAllocAsync`, :py:obj:`~.cuMemPoolTrimTo`, :py:obj:`~.cuMemPoolGetAttribute`, :py:obj:`~.cuMemPoolSetAttribute`, cuMemPoolSetAccess, :py:obj:`~.cuMemGetMemPool`, :py:obj:`~.cuMemPoolCreate` """ cdef cudaMemPool_t memPool = cudaMemPool_t() - cdef cyruntime.cudaMemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cyruntime.cudaMemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL cdef cyruntime.cudaMemAllocationType cytypename = int(typename) with nogil: err = cyruntime.cudaMemGetDefaultMemPool(memPool._pvt_ptr, cylocation_ptr, cytypename) @@ -31339,7 +31339,7 @@ def cudaMemGetMemPool(location : Optional[cudaMemLocation], typename not None : :py:obj:`~.cuDeviceGetDefaultMemPool`, :py:obj:`~.cuMemPoolCreate`, :py:obj:`~.cuDeviceSetMemPool`, :py:obj:`~.cuMemSetMemPool` """ cdef cudaMemPool_t memPool = cudaMemPool_t() - cdef cyruntime.cudaMemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cyruntime.cudaMemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL cdef cyruntime.cudaMemAllocationType cytypename = int(typename) with nogil: err = cyruntime.cudaMemGetMemPool(memPool._pvt_ptr, cylocation_ptr, cytypename) @@ -31408,7 +31408,7 @@ def cudaMemSetMemPool(location : Optional[cudaMemLocation], typename not None : else: pmemPool = int(cudaMemPool_t(memPool)) cymemPool = pmemPool - cdef cyruntime.cudaMemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL + cdef cyruntime.cudaMemLocation* cylocation_ptr = location._pvt_ptr if location is not None else NULL cdef cyruntime.cudaMemAllocationType cytypename = int(typename) with nogil: err = cyruntime.cudaMemSetMemPool(cylocation_ptr, cytypename, cymemPool) @@ -31659,7 +31659,7 @@ def cudaMemPoolImportPointer(memPool, exportData : Optional[cudaMemPoolPtrExport pmemPool = int(cudaMemPool_t(memPool)) cymemPool = pmemPool cdef void_ptr ptr = 0 - cdef cyruntime.cudaMemPoolPtrExportData* cyexportData_ptr = exportData._pvt_ptr if exportData is not None else NULL + cdef cyruntime.cudaMemPoolPtrExportData* cyexportData_ptr = exportData._pvt_ptr if exportData is not None else NULL with nogil: err = cyruntime.cudaMemPoolImportPointer(&ptr, cymemPool, cyexportData_ptr) if err != cyruntime.cudaSuccess: @@ -32551,9 +32551,9 @@ def cudaCreateTextureObject(pResDesc : Optional[cudaResourceDesc], pTexDesc : Op :py:obj:`~.cudaDestroyTextureObject`, :py:obj:`~.cuTexObjectCreate` """ cdef cudaTextureObject_t pTexObject = cudaTextureObject_t() - cdef cyruntime.cudaResourceDesc* cypResDesc_ptr = pResDesc._pvt_ptr if pResDesc is not None else NULL - cdef cyruntime.cudaTextureDesc* cypTexDesc_ptr = pTexDesc._pvt_ptr if pTexDesc is not None else NULL - cdef cyruntime.cudaResourceViewDesc* cypResViewDesc_ptr = pResViewDesc._pvt_ptr if pResViewDesc is not None else NULL + cdef cyruntime.cudaResourceDesc* cypResDesc_ptr = pResDesc._pvt_ptr if pResDesc is not None else NULL + cdef cyruntime.cudaTextureDesc* cypTexDesc_ptr = pTexDesc._pvt_ptr if pTexDesc is not None else NULL + cdef cyruntime.cudaResourceViewDesc* cypResViewDesc_ptr = pResViewDesc._pvt_ptr if pResViewDesc is not None else NULL with nogil: err = cyruntime.cudaCreateTextureObject(pTexObject._pvt_ptr, cypResDesc_ptr, cypTexDesc_ptr, cypResViewDesc_ptr) if err != cyruntime.cudaSuccess: @@ -32754,7 +32754,7 @@ def cudaCreateSurfaceObject(pResDesc : Optional[cudaResourceDesc]): :py:obj:`~.cudaDestroySurfaceObject`, :py:obj:`~.cuSurfObjectCreate` """ cdef cudaSurfaceObject_t pSurfObject = cudaSurfaceObject_t() - cdef cyruntime.cudaResourceDesc* cypResDesc_ptr = pResDesc._pvt_ptr if pResDesc is not None else NULL + cdef cyruntime.cudaResourceDesc* cypResDesc_ptr = pResDesc._pvt_ptr if pResDesc is not None else NULL with nogil: err = cyruntime.cudaCreateSurfaceObject(pSurfObject._pvt_ptr, cypResDesc_ptr) if err != cyruntime.cudaSuccess: @@ -33246,7 +33246,7 @@ def cudaGraphAddKernelNode(graph, pDependencies : Optional[tuple[cudaGraphNode_t elif len(pDependencies) == 1: cypDependencies = (pDependencies[0])._pvt_ptr if numDependencies > len(pDependencies): raise RuntimeError("List is too small: " + str(len(pDependencies)) + " < " + str(numDependencies)) - cdef cyruntime.cudaKernelNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaKernelNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphAddKernelNode(pGraphNode._pvt_ptr, cygraph, cypDependencies, numDependencies, cypNodeParams_ptr) if len(pDependencies) > 1 and cypDependencies is not NULL: @@ -33337,7 +33337,7 @@ def cudaGraphKernelNodeSetParams(node, pNodeParams : Optional[cudaKernelNodePara else: pnode = int(cudaGraphNode_t(node)) cynode = pnode - cdef cyruntime.cudaKernelNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaKernelNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphKernelNodeSetParams(cynode, cypNodeParams_ptr) return (_cudaError_t(err),) @@ -33470,7 +33470,7 @@ def cudaGraphKernelNodeSetAttribute(hNode, attr not None : cudaKernelNodeAttrID, phNode = int(cudaGraphNode_t(hNode)) cyhNode = phNode cdef cyruntime.cudaKernelNodeAttrID cyattr = int(attr) - cdef cyruntime.cudaKernelNodeAttrValue* cyvalue_ptr = value._pvt_ptr if value is not None else NULL + cdef cyruntime.cudaKernelNodeAttrValue* cyvalue_ptr = value._pvt_ptr if value is not None else NULL with nogil: err = cyruntime.cudaGraphKernelNodeSetAttribute(cyhNode, cyattr, cyvalue_ptr) return (_cudaError_t(err),) @@ -33542,7 +33542,7 @@ def cudaGraphAddMemcpyNode(graph, pDependencies : Optional[tuple[cudaGraphNode_t elif len(pDependencies) == 1: cypDependencies = (pDependencies[0])._pvt_ptr if numDependencies > len(pDependencies): raise RuntimeError("List is too small: " + str(len(pDependencies)) + " < " + str(numDependencies)) - cdef cyruntime.cudaMemcpy3DParms* cypCopyParams_ptr = pCopyParams._pvt_ptr if pCopyParams is not None else NULL + cdef cyruntime.cudaMemcpy3DParms* cypCopyParams_ptr = pCopyParams._pvt_ptr if pCopyParams is not None else NULL with nogil: err = cyruntime.cudaGraphAddMemcpyNode(pGraphNode._pvt_ptr, cygraph, cypDependencies, numDependencies, cypCopyParams_ptr) if len(pDependencies) > 1 and cypDependencies is not NULL: @@ -33754,7 +33754,7 @@ def cudaGraphMemcpyNodeSetParams(node, pNodeParams : Optional[cudaMemcpy3DParms] else: pnode = int(cudaGraphNode_t(node)) cynode = pnode - cdef cyruntime.cudaMemcpy3DParms* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaMemcpy3DParms* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphMemcpyNodeSetParams(cynode, cypNodeParams_ptr) return (_cudaError_t(err),) @@ -33904,7 +33904,7 @@ def cudaGraphAddMemsetNode(graph, pDependencies : Optional[tuple[cudaGraphNode_t elif len(pDependencies) == 1: cypDependencies = (pDependencies[0])._pvt_ptr if numDependencies > len(pDependencies): raise RuntimeError("List is too small: " + str(len(pDependencies)) + " < " + str(numDependencies)) - cdef cyruntime.cudaMemsetParams* cypMemsetParams_ptr = pMemsetParams._pvt_ptr if pMemsetParams is not None else NULL + cdef cyruntime.cudaMemsetParams* cypMemsetParams_ptr = pMemsetParams._pvt_ptr if pMemsetParams is not None else NULL with nogil: err = cyruntime.cudaGraphAddMemsetNode(pGraphNode._pvt_ptr, cygraph, cypDependencies, numDependencies, cypMemsetParams_ptr) if len(pDependencies) > 1 and cypDependencies is not NULL: @@ -33986,7 +33986,7 @@ def cudaGraphMemsetNodeSetParams(node, pNodeParams : Optional[cudaMemsetParams]) else: pnode = int(cudaGraphNode_t(node)) cynode = pnode - cdef cyruntime.cudaMemsetParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaMemsetParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphMemsetNodeSetParams(cynode, cypNodeParams_ptr) return (_cudaError_t(err),) @@ -34053,7 +34053,7 @@ def cudaGraphAddHostNode(graph, pDependencies : Optional[tuple[cudaGraphNode_t] elif len(pDependencies) == 1: cypDependencies = (pDependencies[0])._pvt_ptr if numDependencies > len(pDependencies): raise RuntimeError("List is too small: " + str(len(pDependencies)) + " < " + str(numDependencies)) - cdef cyruntime.cudaHostNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaHostNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphAddHostNode(pGraphNode._pvt_ptr, cygraph, cypDependencies, numDependencies, cypNodeParams_ptr) if len(pDependencies) > 1 and cypDependencies is not NULL: @@ -34135,7 +34135,7 @@ def cudaGraphHostNodeSetParams(node, pNodeParams : Optional[cudaHostNodeParams]) else: pnode = int(cudaGraphNode_t(node)) cynode = pnode - cdef cyruntime.cudaHostNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaHostNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphHostNodeSetParams(cynode, cypNodeParams_ptr) return (_cudaError_t(err),) @@ -34550,7 +34550,7 @@ def cudaGraphAddExternalSemaphoresSignalNode(graph, pDependencies : Optional[tup elif len(pDependencies) == 1: cypDependencies = (pDependencies[0])._pvt_ptr if numDependencies > len(pDependencies): raise RuntimeError("List is too small: " + str(len(pDependencies)) + " < " + str(numDependencies)) - cdef cyruntime.cudaExternalSemaphoreSignalNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaExternalSemaphoreSignalNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphAddExternalSemaphoresSignalNode(pGraphNode._pvt_ptr, cygraph, cypDependencies, numDependencies, cynodeParams_ptr) if len(pDependencies) > 1 and cypDependencies is not NULL: @@ -34594,7 +34594,7 @@ def cudaGraphExternalSemaphoresSignalNodeSetParams(hNode, nodeParams : Optional[ else: phNode = int(cudaGraphNode_t(hNode)) cyhNode = phNode - cdef cyruntime.cudaExternalSemaphoreSignalNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaExternalSemaphoreSignalNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExternalSemaphoresSignalNodeSetParams(cyhNode, cynodeParams_ptr) return (_cudaError_t(err),) @@ -34628,7 +34628,7 @@ def cudaGraphAddExternalSemaphoresWaitNode(graph, pDependencies : Optional[tuple elif len(pDependencies) == 1: cypDependencies = (pDependencies[0])._pvt_ptr if numDependencies > len(pDependencies): raise RuntimeError("List is too small: " + str(len(pDependencies)) + " < " + str(numDependencies)) - cdef cyruntime.cudaExternalSemaphoreWaitNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaExternalSemaphoreWaitNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphAddExternalSemaphoresWaitNode(pGraphNode._pvt_ptr, cygraph, cypDependencies, numDependencies, cynodeParams_ptr) if len(pDependencies) > 1 and cypDependencies is not NULL: @@ -34672,7 +34672,7 @@ def cudaGraphExternalSemaphoresWaitNodeSetParams(hNode, nodeParams : Optional[cu else: phNode = int(cudaGraphNode_t(hNode)) cyhNode = phNode - cdef cyruntime.cudaExternalSemaphoreWaitNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaExternalSemaphoreWaitNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExternalSemaphoresWaitNodeSetParams(cyhNode, cynodeParams_ptr) return (_cudaError_t(err),) @@ -34706,7 +34706,7 @@ def cudaGraphAddMemAllocNode(graph, pDependencies : Optional[tuple[cudaGraphNode elif len(pDependencies) == 1: cypDependencies = (pDependencies[0])._pvt_ptr if numDependencies > len(pDependencies): raise RuntimeError("List is too small: " + str(len(pDependencies)) + " < " + str(numDependencies)) - cdef cyruntime.cudaMemAllocNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaMemAllocNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphAddMemAllocNode(pGraphNode._pvt_ptr, cygraph, cypDependencies, numDependencies, cynodeParams_ptr) if len(pDependencies) > 1 and cypDependencies is not NULL: @@ -36403,7 +36403,7 @@ def cudaGraphInstantiateWithParams(graph, instantiateParams : Optional[cudaGraph pgraph = int(cudaGraph_t(graph)) cygraph = pgraph cdef cudaGraphExec_t pGraphExec = cudaGraphExec_t() - cdef cyruntime.cudaGraphInstantiateParams* cyinstantiateParams_ptr = instantiateParams._pvt_ptr if instantiateParams is not None else NULL + cdef cyruntime.cudaGraphInstantiateParams* cyinstantiateParams_ptr = instantiateParams._pvt_ptr if instantiateParams is not None else NULL with nogil: err = cyruntime.cudaGraphInstantiateWithParams(pGraphExec._pvt_ptr, cygraph, cyinstantiateParams_ptr) if err != cyruntime.cudaSuccess: @@ -36530,7 +36530,7 @@ def cudaGraphExecKernelNodeSetParams(hGraphExec, node, pNodeParams : Optional[cu else: phGraphExec = int(cudaGraphExec_t(hGraphExec)) cyhGraphExec = phGraphExec - cdef cyruntime.cudaKernelNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaKernelNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExecKernelNodeSetParams(cyhGraphExec, cynode, cypNodeParams_ptr) return (_cudaError_t(err),) @@ -36595,7 +36595,7 @@ def cudaGraphExecMemcpyNodeSetParams(hGraphExec, node, pNodeParams : Optional[cu else: phGraphExec = int(cudaGraphExec_t(hGraphExec)) cyhGraphExec = phGraphExec - cdef cyruntime.cudaMemcpy3DParms* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaMemcpy3DParms* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExecMemcpyNodeSetParams(cyhGraphExec, cynode, cypNodeParams_ptr) return (_cudaError_t(err),) @@ -36766,7 +36766,7 @@ def cudaGraphExecMemsetNodeSetParams(hGraphExec, node, pNodeParams : Optional[cu else: phGraphExec = int(cudaGraphExec_t(hGraphExec)) cyhGraphExec = phGraphExec - cdef cyruntime.cudaMemsetParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaMemsetParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExecMemsetNodeSetParams(cyhGraphExec, cynode, cypNodeParams_ptr) return (_cudaError_t(err),) @@ -36821,7 +36821,7 @@ def cudaGraphExecHostNodeSetParams(hGraphExec, node, pNodeParams : Optional[cuda else: phGraphExec = int(cudaGraphExec_t(hGraphExec)) cyhGraphExec = phGraphExec - cdef cyruntime.cudaHostNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL + cdef cyruntime.cudaHostNodeParams* cypNodeParams_ptr = pNodeParams._pvt_ptr if pNodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExecHostNodeSetParams(cyhGraphExec, cynode, cypNodeParams_ptr) return (_cudaError_t(err),) @@ -36950,7 +36950,7 @@ def cudaGraphExecExternalSemaphoresSignalNodeSetParams(hGraphExec, hNode, nodePa else: phGraphExec = int(cudaGraphExec_t(hGraphExec)) cyhGraphExec = phGraphExec - cdef cyruntime.cudaExternalSemaphoreSignalNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaExternalSemaphoreSignalNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExecExternalSemaphoresSignalNodeSetParams(cyhGraphExec, cyhNode, cynodeParams_ptr) return (_cudaError_t(err),) @@ -36977,7 +36977,7 @@ def cudaGraphExecExternalSemaphoresWaitNodeSetParams(hGraphExec, hNode, nodePara else: phGraphExec = int(cudaGraphExec_t(hGraphExec)) cyhGraphExec = phGraphExec - cdef cyruntime.cudaExternalSemaphoreWaitNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaExternalSemaphoreWaitNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExecExternalSemaphoresWaitNodeSetParams(cyhGraphExec, cyhNode, cynodeParams_ptr) return (_cudaError_t(err),) @@ -37882,7 +37882,7 @@ def cudaGraphAddNode(graph, pDependencies : Optional[tuple[cudaGraphNode_t] | li string.memcpy(&cydependencyData[idx], (dependencyData[idx])._pvt_ptr, sizeof(cyruntime.cudaGraphEdgeData)) elif len(dependencyData) == 1: cydependencyData = (dependencyData[0])._pvt_ptr - cdef cyruntime.cudaGraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaGraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphAddNode(pGraphNode._pvt_ptr, cygraph, cypDependencies, cydependencyData, numDependencies, cynodeParams_ptr) if len(pDependencies) > 1 and cypDependencies is not NULL: @@ -37932,7 +37932,7 @@ def cudaGraphNodeSetParams(node, nodeParams : Optional[cudaGraphNodeParams]): else: pnode = int(cudaGraphNode_t(node)) cynode = pnode - cdef cyruntime.cudaGraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaGraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphNodeSetParams(cynode, cynodeParams_ptr) return (_cudaError_t(err),) @@ -38045,7 +38045,7 @@ def cudaGraphExecNodeSetParams(graphExec, node, nodeParams : Optional[cudaGraphN else: pgraphExec = int(cudaGraphExec_t(graphExec)) cygraphExec = pgraphExec - cdef cyruntime.cudaGraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL + cdef cyruntime.cudaGraphNodeParams* cynodeParams_ptr = nodeParams._pvt_ptr if nodeParams is not None else NULL with nogil: err = cyruntime.cudaGraphExecNodeSetParams(cygraphExec, cynode, cynodeParams_ptr) return (_cudaError_t(err),) @@ -39152,7 +39152,7 @@ def cudaDevSmResourceSplitByCount(unsigned int nbGroups, input_ : Optional[cudaD if cyresult is NULL: raise MemoryError('Failed to allocate length x size memory: ' + str(nbGroups) + 'x' + str(sizeof(cyruntime.cudaDevResource))) cdef unsigned int cynbGroups = nbGroups - cdef cyruntime.cudaDevResource* cyinput__ptr = input_._pvt_ptr if input_ is not None else NULL + cdef cyruntime.cudaDevResource* cyinput__ptr = input_._pvt_ptr if input_ is not None else NULL cdef cudaDevResource remaining = cudaDevResource() with nogil: err = cyruntime.cudaDevSmResourceSplitByCount(cyresult, &cynbGroups, cyinput__ptr, remaining._pvt_ptr, flags, minCount) @@ -39314,7 +39314,7 @@ def cudaDevSmResourceSplit(unsigned int nbGroups, input_ : Optional[cudaDevResou cyresult = calloc(nbGroups, sizeof(cyruntime.cudaDevResource)) if cyresult is NULL: raise MemoryError('Failed to allocate length x size memory: ' + str(nbGroups) + 'x' + str(sizeof(cyruntime.cudaDevResource))) - cdef cyruntime.cudaDevResource* cyinput__ptr = input_._pvt_ptr if input_ is not None else NULL + cdef cyruntime.cudaDevResource* cyinput__ptr = input_._pvt_ptr if input_ is not None else NULL cdef cudaDevResource remainder = cudaDevResource() cdef cyruntime.cudaDevSmResourceGroupParams* cygroupParams = NULL if len(groupParams) > 1: @@ -39986,7 +39986,7 @@ def cudaDeviceGetExecutionCtx(int device): def cudaGetExportTable(pExportTableId : Optional[cudaUUID_t]): """""" cdef void_ptr ppExportTable = 0 - cdef cyruntime.cudaUUID_t* cypExportTableId_ptr = pExportTableId._pvt_ptr if pExportTableId is not None else NULL + cdef cyruntime.cudaUUID_t* cypExportTableId_ptr = pExportTableId._pvt_ptr if pExportTableId is not None else NULL with nogil: err = cyruntime.cudaGetExportTable(&ppExportTable, cypExportTableId_ptr) if err != cyruntime.cudaSuccess: @@ -40691,7 +40691,7 @@ def cudaEGLStreamProducerReturnFrame(conn, eglframe : Optional[cudaEglFrame], pS cyconn = conn else: raise TypeError("Argument 'conn' is not instance of type (expected , found " + str(type(conn))) - cdef cyruntime.cudaEglFrame* cyeglframe_ptr = eglframe._pvt_ptr if eglframe is not None else NULL + cdef cyruntime.cudaEglFrame* cyeglframe_ptr = eglframe._pvt_ptr if eglframe is not None else NULL with nogil: err = cyruntime.cudaEGLStreamProducerReturnFrame(cyconn, cyeglframe_ptr, cypStream) return (_cudaError_t(err),) From 7ab468293f85db1a9c71ee399a87cc2dc43b5d6b Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:04:49 -0400 Subject: [PATCH 007/244] CI: Detect changed paths and gate cuda.bindings tests (#1908) * ci: detect changed paths and gate cuda.bindings tests Add a detect-changes job to ci.yml that classifies which top-level modules were touched by a PR (cuda_bindings, cuda_core, cuda_pathfinder, cuda_python, cuda_python_test_helpers, shared infra) using dorny/paths-filter. The job emits composed gating outputs that account for the dependency graph (pathfinder -> bindings -> core). Thread a new skip-bindings-test input through the reusable test-wheel workflows so cuda.bindings tests (and their Cython counterparts) are skipped when the detect-changes output for test_bindings is false. For PRs that only touch cuda_core this skips the expensive bindings suite while still running cuda.core and cuda.pathfinder tests against the wheel built in the current CI run. Split the existing SKIP_CUDA_BINDINGS_TEST env var in ci/tools/env-vars into two orthogonal flags: USE_BACKPORT_BINDINGS drives the backport branch download path (CTK major mismatch), while SKIP_CUDA_BINDINGS_TEST remains the test-time gate. This lets path-filter-based skips reuse the existing SKIP_CUDA_BINDINGS_TEST plumbing without triggering a cross-branch artifact fetch. Non-PR events (push to main, tag, schedule, workflow_dispatch) still exercise the full pipeline. Refs #299 * fix: set explicit dorny base and decouple cython-test skip Two fixes from code review: 1. Set `base: main` on dorny/paths-filter so backport PRs targeting non-default branches still diff against main for path detection. 2. Decouple SKIP_CYTHON_TEST from SKIP_BINDINGS_TEST_OVERRIDE. The path-filter override only skips bindings tests; cuda.core Cython tests should still run on core-only PRs. Cython skip is now driven solely by CTK minor-version mismatch. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove explicit dorny base, rely on v3 default dorny/paths-filter v3 already diffs against the repo default branch for non-default-branch pushes. Explicit base: main was redundant and would produce wrong baselines for backport PRs targeting release branches (all files seen as changed vs main). Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: replace dorny/paths-filter with native git diff Remove third-party dorny/paths-filter action dependency. Use git merge-base + git diff --name-only + grep instead. Same behavior, zero supply-chain risk, full control over base ref resolution. Co-Authored-By: Claude Opus 4.6 (1M context) * style: use herestring instead of echo pipe in has_match Co-Authored-By: Claude Opus 4.6 (1M context) * ci: resolve PR base branch at runtime for detect-changes diff copy-pr-bot mirrors every PR to a pull-request/ branch regardless of whether the upstream PR targets main or a backport branch such as 12.9.x. Diffing against origin/main unconditionally therefore misclassifies changed paths on backport PRs and silently suppresses the cuda.bindings test matrix. Look up the real base ref via nv-gha-runners/get-pr-info (already used elsewhere in this repo) and pass it to git merge-base so the changed- paths classification matches the PR's actual target. * ci: require detect-changes success in final checks aggregation The checks job used if:always() plus shell-level result inspection of doc and the three test jobs, treating skipped as non-fatal to preserve intentional [doc-only] skips. detect-changes is a needs prerequisite of every test job but was absent from checks.needs, so a failure in the gating step silently cascaded into test-job skips and a green final status. Add detect-changes to checks.needs and require its result to be success. The legitimate doc-only skip path is unaffected because that leaves detect-changes itself successful. --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 161 +++++++++++++++++++++++ .github/workflows/test-wheel-linux.yml | 14 +- .github/workflows/test-wheel-windows.yml | 14 +- ci/tools/env-vars | 19 ++- 4 files changed, 201 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7e0c652243..525c210889c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,151 @@ jobs: echo "skip=${skip}" >> "$GITHUB_OUTPUT" echo "doc_only=${doc_only}" >> "$GITHUB_OUTPUT" + # Detect which top-level modules were touched by the PR so downstream build + # and test jobs can avoid rebuilding/retesting modules unaffected by the + # change. See issue #299. + # + # Dependency graph (verified in pyproject.toml files): + # cuda_pathfinder -> (no internal deps) + # cuda_bindings -> cuda_pathfinder + # cuda_core -> cuda_pathfinder, cuda_bindings + # cuda_python -> cuda_bindings (meta package) + # + # A change to cuda_pathfinder (or shared infra) forces a rebuild of every + # downstream module. A change to cuda_bindings forces rebuild of cuda_core. + # A change to cuda_core alone skips rebuilding/retesting cuda_bindings. + # On push to main, tag refs, schedule, or workflow_dispatch events we + # unconditionally run everything because there is no meaningful "changed + # paths" baseline for those events. + detect-changes: + runs-on: ubuntu-latest + outputs: + bindings: ${{ steps.compose.outputs.bindings }} + core: ${{ steps.compose.outputs.core }} + pathfinder: ${{ steps.compose.outputs.pathfinder }} + python_meta: ${{ steps.compose.outputs.python_meta }} + test_helpers: ${{ steps.compose.outputs.test_helpers }} + shared: ${{ steps.compose.outputs.shared }} + build_bindings: ${{ steps.compose.outputs.build_bindings }} + build_core: ${{ steps.compose.outputs.build_core }} + build_pathfinder: ${{ steps.compose.outputs.build_pathfinder }} + test_bindings: ${{ steps.compose.outputs.test_bindings }} + test_core: ${{ steps.compose.outputs.test_core }} + test_pathfinder: ${{ steps.compose.outputs.test_pathfinder }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + # copy-pr-bot pushes every PR (whether it targets main or a backport + # branch such as 12.9.x) to pull-request/, so the base branch + # cannot be inferred from github.ref_name. Look it up via the + # upstream PR metadata so the diff below is rooted at the right place. + - name: Resolve PR base branch + id: pr-info + if: ${{ startsWith(github.ref_name, 'pull-request/') }} + uses: nv-gha-runners/get-pr-info@main + + - name: Detect changed paths + id: filter + if: ${{ startsWith(github.ref_name, 'pull-request/') }} + env: + BASE_REF: ${{ fromJSON(steps.pr-info.outputs.pr-info).base.ref }} + run: | + # Diff against the merge base with the PR's actual target branch. + # Uses merge-base so diverged branches only show files changed on + # the PR side, not upstream commits. + if [[ -z "${BASE_REF}" ]]; then + echo "Could not resolve PR base branch from get-pr-info output" >&2 + exit 1 + fi + base=$(git merge-base HEAD "origin/${BASE_REF}") + changed=$(git diff --name-only "$base"...HEAD) + + has_match() { + grep -qE "$1" <<< "$changed" && echo true || echo false + } + + { + echo "bindings=$(has_match '^cuda_bindings/')" + echo "core=$(has_match '^cuda_core/')" + echo "pathfinder=$(has_match '^cuda_pathfinder/')" + echo "python_meta=$(has_match '^cuda_python/')" + echo "test_helpers=$(has_match '^cuda_python_test_helpers/')" + echo "shared=$(has_match '^(\.github/|ci/|scripts/|toolshed/|conftest\.py$|pyproject\.toml$|pixi\.(toml|lock)$|pytest\.ini$|ruff\.toml$)')" + } >> "$GITHUB_OUTPUT" + + - name: Compose gating outputs + id: compose + env: + IS_PR: ${{ startsWith(github.ref_name, 'pull-request/') }} + BINDINGS: ${{ steps.filter.outputs.bindings || 'false' }} + CORE: ${{ steps.filter.outputs.core || 'false' }} + PATHFINDER: ${{ steps.filter.outputs.pathfinder || 'false' }} + PYTHON_META: ${{ steps.filter.outputs.python_meta || 'false' }} + TEST_HELPERS: ${{ steps.filter.outputs.test_helpers || 'false' }} + SHARED: ${{ steps.filter.outputs.shared || 'false' }} + run: | + set -euxo pipefail + # Non-PR events (push to main, tag push, schedule, workflow_dispatch) + # always exercise the full pipeline because there is no baseline for + # a meaningful diff. + if [[ "${IS_PR}" != "true" ]]; then + bindings=true + core=true + pathfinder=true + python_meta=true + test_helpers=true + shared=true + else + bindings="${BINDINGS}" + core="${CORE}" + pathfinder="${PATHFINDER}" + python_meta="${PYTHON_META}" + test_helpers="${TEST_HELPERS}" + shared="${SHARED}" + fi + + or_flag() { + for v in "$@"; do + if [[ "${v}" == "true" ]]; then + echo "true" + return + fi + done + echo "false" + } + + # Build gating: pathfinder change forces rebuild of bindings and + # core; bindings change forces rebuild of core. shared changes force + # a full rebuild. + build_pathfinder="$(or_flag "${shared}" "${pathfinder}")" + build_bindings="$(or_flag "${shared}" "${pathfinder}" "${bindings}")" + build_core="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${core}")" + + # Test gating: tests for a module must run whenever that module, any + # of its runtime dependencies, the shared test helper package, or + # shared infra changes. pathfinder tests are cheap and always run. + test_pathfinder=true + test_bindings="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${test_helpers}")" + test_core="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${core}" "${test_helpers}")" + + { + echo "bindings=${bindings}" + echo "core=${core}" + echo "pathfinder=${pathfinder}" + echo "python_meta=${python_meta}" + echo "test_helpers=${test_helpers}" + echo "shared=${shared}" + echo "build_bindings=${build_bindings}" + echo "build_core=${build_core}" + echo "build_pathfinder=${build_pathfinder}" + echo "test_bindings=${test_bindings}" + echo "test_core=${test_core}" + echo "test_pathfinder=${test_pathfinder}" + } >> "$GITHUB_OUTPUT" + # NOTE: Build jobs are intentionally split by platform rather than using a single # matrix. This allows each test job to depend only on its corresponding build, # so faster platforms can proceed through build & test without waiting for slower @@ -151,6 +296,7 @@ jobs: needs: - ci-vars - should-skip + - detect-changes - build-linux-64 secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml @@ -159,6 +305,7 @@ jobs: host-platform: ${{ matrix.host-platform }} build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }} # See test-linux-64 for why test jobs are split by platform. test-linux-aarch64: @@ -174,6 +321,7 @@ jobs: needs: - ci-vars - should-skip + - detect-changes - build-linux-aarch64 secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml @@ -182,6 +330,7 @@ jobs: host-platform: ${{ matrix.host-platform }} build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }} # See test-linux-64 for why test jobs are split by platform. test-windows: @@ -197,6 +346,7 @@ jobs: needs: - ci-vars - should-skip + - detect-changes - build-windows secrets: inherit uses: ./.github/workflows/test-wheel-windows.yml @@ -205,6 +355,7 @@ jobs: host-platform: ${{ matrix.host-platform }} build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }} doc: name: Docs @@ -228,6 +379,7 @@ jobs: runs-on: ubuntu-latest needs: - should-skip + - detect-changes - test-linux-64 - test-linux-aarch64 - test-windows @@ -254,7 +406,16 @@ jobs: # # Note: When [doc-only] is in PR title, test jobs are intentionally # skipped and should not cause failure. + # + # detect-changes gates whether heavy test matrices run at all; if it + # does not succeed, downstream test jobs are skipped rather than + # failed, which would otherwise go unnoticed here. Require its + # success explicitly so a broken gating step cannot masquerade as a + # green CI run. doc_only=${{ needs.should-skip.outputs.doc-only }} + if ${{ needs.detect-changes.result != 'success' }}; then + exit 1 + fi if ${{ needs.doc.result == 'cancelled' || needs.doc.result == 'failure' }}; then exit 1 fi diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 270ed445a98..796040333b5 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -22,6 +22,13 @@ on: nruns: type: number default: 1 + # When true, cuda.bindings tests (and the Cython tests that depend on + # them) are skipped even when CTK majors match. Callers set this based + # on the output of the detect-changes job in ci.yml so PRs that only + # touch unrelated modules avoid the expensive bindings test suite. + skip-bindings-test: + type: boolean + default: false defaults: run: @@ -113,6 +120,7 @@ jobs: LOCAL_CTK: ${{ matrix.LOCAL_CTK }} PY_VER: ${{ matrix.PY_VER }} SHA: ${{ github.sha }} + SKIP_BINDINGS_TEST_OVERRIDE: ${{ inputs.skip-bindings-test && '1' || '0' }} run: ./ci/tools/env-vars test - name: Download cuda-pathfinder build artifacts @@ -122,21 +130,21 @@ jobs: path: ./cuda_pathfinder - name: Download cuda-python build artifacts - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '0' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: cuda-python-wheel path: . - name: Download cuda.bindings build artifacts - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '0' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - name: Download cuda-python & cuda.bindings build artifacts from the prior branch - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '1'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '1' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index 91f098b0e90..765823c6bfc 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -22,6 +22,13 @@ on: nruns: type: number default: 1 + # When true, cuda.bindings tests (and the Cython tests that depend on + # them) are skipped even when CTK majors match. Callers set this based + # on the output of the detect-changes job in ci.yml so PRs that only + # touch unrelated modules avoid the expensive bindings test suite. + skip-bindings-test: + type: boolean + default: false jobs: compute-matrix: @@ -107,6 +114,7 @@ jobs: LOCAL_CTK: ${{ matrix.LOCAL_CTK }} PY_VER: ${{ matrix.PY_VER }} SHA: ${{ github.sha }} + SKIP_BINDINGS_TEST_OVERRIDE: ${{ inputs.skip-bindings-test && '1' || '0' }} shell: bash --noprofile --norc -xeuo pipefail {0} run: ./ci/tools/env-vars test @@ -117,21 +125,21 @@ jobs: path: ./cuda_pathfinder - name: Download cuda-python build artifacts - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '0' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: cuda-python-wheel path: . - name: Download cuda.bindings build artifacts - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '0' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - name: Download cuda-python & cuda.bindings build artifacts from the prior branch - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '1'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '1' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/ci/tools/env-vars b/ci/tools/env-vars index 793928babc0..17db607c29b 100755 --- a/ci/tools/env-vars +++ b/ci/tools/env-vars @@ -52,11 +52,27 @@ elif [[ "${1}" == "test" ]]; then BUILD_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${BUILD_CUDA_VER})" TEST_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${CUDA_VER})" CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${BUILD_CUDA_VER}-${HOST_PLATFORM}" + # USE_BACKPORT_BINDINGS flags the CTK-major-mismatch case where the + # current-run bindings wheel was built for a different CTK major than the + # one under test, so we must pull the bindings wheel from the backport + # branch instead. This is independent of whether bindings tests run. + # SKIP_CUDA_BINDINGS_TEST is the test-time gate: it is set when the CTK + # majors differ OR when the caller tells us to skip for path-filter + # reasons via SKIP_BINDINGS_TEST_OVERRIDE. if [[ ${BUILD_CUDA_MAJOR} != ${TEST_CUDA_MAJOR} ]]; then + USE_BACKPORT_BINDINGS=1 SKIP_CUDA_BINDINGS_TEST=1 SKIP_CYTHON_TEST=1 else - SKIP_CUDA_BINDINGS_TEST=0 + USE_BACKPORT_BINDINGS=0 + # Path-filter override only skips bindings tests, NOT cython tests + # for other modules (e.g. cuda.core). Cython skip is driven solely + # by the build/test CTK minor-version mismatch. + if [[ "${SKIP_BINDINGS_TEST_OVERRIDE:-0}" == "1" ]]; then + SKIP_CUDA_BINDINGS_TEST=1 + else + SKIP_CUDA_BINDINGS_TEST=0 + fi BUILD_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_CUDA_VER})" TEST_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${CUDA_VER})" if [[ ${BUILD_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then @@ -80,6 +96,7 @@ elif [[ "${1}" == "test" ]]; then echo "SKIP_CUDA_BINDINGS_TEST=${SKIP_CUDA_BINDINGS_TEST}" echo "SKIP_CYTHON_TEST=${SKIP_CYTHON_TEST}" echo "TEST_CUDA_MAJOR=${TEST_CUDA_MAJOR}" + echo "USE_BACKPORT_BINDINGS=${USE_BACKPORT_BINDINGS}" } >> $GITHUB_ENV fi From 82ca96235012c11c33b2130205e4b70a7e1ef949 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:06:59 -0400 Subject: [PATCH 008/244] build(cuda_bindings): declare cuda-pathfinder as a host-dependency (#1926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pixi-build-python runs `pip install --no-build-isolation` against the pixi host environment, which bypasses `[build-system] requires` in `pyproject.toml`. As a result, building `cuda_bindings` from source via the pixi path-dependency workflow failed because `cuda_pathfinder` was not present at build time — it was only declared in `pyproject.toml`. Declare `cuda-pathfinder` as a path host-dependency in `cuda_bindings/pixi.toml` so pixi installs it into the host env before the no-isolation build runs. Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_bindings/pixi.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/cuda_bindings/pixi.toml b/cuda_bindings/pixi.toml index 2c301378f0e..0d46eee8fcb 100644 --- a/cuda_bindings/pixi.toml +++ b/cuda_bindings/pixi.toml @@ -121,6 +121,7 @@ setuptools = ">=80" setuptools-scm = ">=8" cython = ">=3.2,<3.3" pyclibrary = ">=0.1.7" +cuda-pathfinder = { path = "../cuda_pathfinder" } cuda-cudart-static = "*" cuda-nvrtc-dev = "*" cuda-profiler-api = "*" From c6cf3727d80c19c838ecfec54f067364da3c93ce Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:06:17 -0400 Subject: [PATCH 009/244] test(cufile): prime libcufile to avoid SIGFPE from 1.17.1 state-poisoning bug (#1925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(cufile): prime libcufile before parameter-set tests to avoid SIGFPE Under pytest-randomly, the cuFile test module fatally crashes with SIGFPE in CUFileDrv::ReadVersionInfo (unguarded div %rcx with rcx=0) inside libcufile.so cuFileDriverOpen+0xe. The crash is deterministic given specific test orderings and was reproducible with seed 2758108007. Root cause is a libcufile 1.17.1 bug. Calling cuFileSetParameterSizeT (or other pre-open configuration APIs) BEFORE the first cuFileDriverOpen leaves an internal version list uninitialized; the next driver_open then divides by its zero length. Minimal repro: pytest tests/test_cufile.py::test_set_get_parameter_size_t \\ tests/test_cufile.py::test_buf_register_invalid_flags Fix: add a module-scope autouse _cufile_driver_prewarm fixture that performs one driver_open/driver_close before any test in the module runs. That single cycle initializes libcufile's version list; both test regimes (driver-open tests via the function-scope `driver` fixture, and driver-closed parameter-set tests) then work under any ordering. Also swap test_set_parameter_posix_pool_slab_array's inline driver_open/close for the `driver` fixture. pytest fixture ordering guarantees driver_config (which calls set_parameter_posix_pool_slab_array while closed) runs before `driver` opens, matching the previous manual ordering. Co-Authored-By: Claude Opus 4.6 (1M context) * test(cufile): drop useless yield from prewarm fixture The _cufile_driver_prewarm fixture has no teardown — the open/close cycle is setup-only. Keeping a trailing `yield` made ruff's PT022 (pytest-useless-yield-fixture) flag it. Drop the yield so the fixture runs as pure setup. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_bindings/tests/test_cufile.py | 55 +++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/cuda_bindings/tests/test_cufile.py b/cuda_bindings/tests/test_cufile.py index a4400f637a3..f6dbc2b50cb 100644 --- a/cuda_bindings/tests/test_cufile.py +++ b/cuda_bindings/tests/test_cufile.py @@ -140,6 +140,51 @@ def ctx(): cuda.cuDevicePrimaryCtxRelease(device) +@pytest.fixture(scope="module", autouse=True) +def _cufile_driver_prewarm(): + """Prime libcufile with one driver_open/close cycle before any test runs. + + The cuFile test module mixes two incompatible regimes: + + - Driver-open tests (buf_register_*, cufile_read_write, batch_io, stats, + etc.) need cuFileDriverOpen; they use the function-scope `driver` + fixture to open/close per test. + - Driver-closed tests (test_set_get_parameter_*, test_set_parameter_posix_*) + must run with the driver CLOSED — libcufile rejects parameter-set calls + when the driver is open (DRIVER_ALREADY_OPEN, 5026). + + Workaround for NVIDIA libcufile 1.17.1 bug: calling cuFileSetParameterSizeT + (or similar pre-open configuration APIs) BEFORE the first cuFileDriverOpen + leaves an internal version list uninitialized such that a later + cuFileDriverOpen SIGFPEs in CUFileDrv::ReadVersionInfo (div-by-zero). + Under random ordering, a driver-closed test can run before any + driver-open test, poisoning libcufile and tearing down pytest with a fatal + signal on the next driver_open. + + One open/close cycle up front primes libcufile's version list. After that, + both regimes work: the per-test `driver` fixture can open/close freely, + and parameter-set tests run against the (now properly initialized) closed + driver. + + Note: per-test driver_open/close is not ideal on throughput grounds, but + it is forced by the libcufile API — parameter-set tests cannot coexist + with a session-wide open driver. + """ + (err,) = cuda.cuInit(0) + assert err == cuda.CUresult.CUDA_SUCCESS + err, device = cuda.cuDeviceGet(0) + assert err == cuda.CUresult.CUDA_SUCCESS + err, dctx = cuda.cuDevicePrimaryCtxRetain(device) + assert err == cuda.CUresult.CUDA_SUCCESS + (err,) = cuda.cuCtxSetCurrent(dctx) + assert err == cuda.CUresult.CUDA_SUCCESS + try: + cufile.driver_open() + cufile.driver_close() + finally: + cuda.cuDevicePrimaryCtxRelease(device) + + @pytest.fixture def driver(ctx): cufile.driver_open() @@ -1896,8 +1941,7 @@ def driver_config(slab_sizes, slab_counts): @pytest.mark.skipif( cufileVersionLessThan(1150), reason="cuFile parameter APIs require cuFile library version 13.0 or later" ) -@pytest.mark.usefixtures("ctx") -def test_set_parameter_posix_pool_slab_array(slab_sizes, slab_counts, driver_config): +def test_set_parameter_posix_pool_slab_array(slab_sizes, slab_counts, driver_config, driver): """Test cuFile POSIX pool slab array configuration.""" # After setting parameters, retrieve them back to verify n_slab_sizes = len(slab_sizes) @@ -1907,12 +1951,7 @@ def test_set_parameter_posix_pool_slab_array(slab_sizes, slab_counts, driver_con retrieved_sizes_addr = ctypes.addressof(retrieved_sizes) retrieved_counts_addr = ctypes.addressof(retrieved_counts) - # Open cuFile driver AFTER setting parameters - cufile.driver_open() - try: - cufile.get_parameter_posix_pool_slab_array(retrieved_sizes_addr, retrieved_counts_addr, n_slab_sizes) - finally: - cufile.driver_close() + cufile.get_parameter_posix_pool_slab_array(retrieved_sizes_addr, retrieved_counts_addr, n_slab_sizes) # Verify they match what we set assert list(retrieved_sizes) == slab_sizes From 4365958a64ff23ca90d9178b9a42cb6ed44af038 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 17 Apr 2026 10:07:12 -0400 Subject: [PATCH 010/244] nvbug5939707: Don't run tests requiring __nanosleep on < sm70 (#1939) --- cuda_core/tests/memory_ipc/test_event_ipc.py | 1 + cuda_core/tests/test_event.py | 4 +++- cuda_core/tests/test_helpers.py | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cuda_core/tests/memory_ipc/test_event_ipc.py b/cuda_core/tests/memory_ipc/test_event_ipc.py index 793ae0744c3..61fa7ca8536 100644 --- a/cuda_core/tests/memory_ipc/test_event_ipc.py +++ b/cuda_core/tests/memory_ipc/test_event_ipc.py @@ -15,6 +15,7 @@ NBYTES = 64 +@pytest.mark.skipif(Device().compute_capability.major < 7, reason="__nanosleep is only available starting Volta (sm70)") class TestEventIpc: """Check the basic usage of IPC-enabled events with a latch kernel.""" diff --git a/cuda_core/tests/test_event.py b/cuda_core/tests/test_event.py index ddceacd77c7..1f62a131261 100644 --- a/cuda_core/tests/test_event.py +++ b/cuda_core/tests/test_event.py @@ -1,4 +1,4 @@ -# 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 @@ -21,6 +21,7 @@ def test_event_init_disabled(): cuda.core._event.Event() # Ensure back door is locked. +@pytest.mark.skipif(Device().compute_capability.major < 7, reason="__nanosleep is only available starting Volta (sm70)") def test_timing_success(init_cuda): options = EventOptions(enable_timing=True) device = Device() @@ -117,6 +118,7 @@ def test_error_timing_recorded(): event3 - event2 +@pytest.mark.skipif(Device().compute_capability.major < 7, reason="__nanosleep is only available starting Volta (sm70)") def test_error_timing_incomplete(): device = Device() device.set_current() diff --git a/cuda_core/tests/test_helpers.py b/cuda_core/tests/test_helpers.py index 23a5b71c854..9cf93fbd21d 100644 --- a/cuda_core/tests/test_helpers.py +++ b/cuda_core/tests/test_helpers.py @@ -1,4 +1,4 @@ -# 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 @@ -17,6 +17,7 @@ NBYTES = 64 +@pytest.mark.skipif(Device().compute_capability.major < 7, reason="__nanosleep is only available starting Volta (sm70)") def test_latchkernel(): """Test LatchKernel.""" log = TimestampedLogger(enabled=ENABLE_LOGGING) From 190df1088eced8d82eeb988a43a087058bd93ece Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 17 Apr 2026 10:07:50 -0400 Subject: [PATCH 011/244] nvbug6084615: Don't assert anything about content of nvmlSystemGetDriverBranch (#1938) --- cuda_core/tests/system/test_system_system.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cuda_core/tests/system/test_system_system.py b/cuda_core/tests/system/test_system_system.py index cb260a0e0af..d21b8d7bf09 100644 --- a/cuda_core/tests/system/test_system_system.py +++ b/cuda_core/tests/system/test_system_system.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 @@ -101,4 +101,3 @@ def test_get_driver_branch(): driver_branch = system.get_driver_branch() assert isinstance(driver_branch, str) assert len(driver_branch) > 0 - assert driver_branch[0] == "r" From 330b30efa6ea796c16f75270b80e579d90c5e2b5 Mon Sep 17 00:00:00 2001 From: Rui Luo Date: Fri, 17 Apr 2026 22:10:15 +0800 Subject: [PATCH 012/244] tests: add coverage tests for cuda core (#1923) * tests: add coverage tests for cuda core * tests: add launch-level coverage for ctypes/numpy subclass fallback --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- cuda_core/tests/test_event.py | 48 ++++++++ cuda_core/tests/test_launcher.py | 129 ++++++++++++++++++++ cuda_core/tests/test_linker.py | 21 ++++ cuda_core/tests/test_program.py | 104 ++++++++++++++++ cuda_core/tests/test_utils.py | 199 +++++++++++++++++++++++++++++++ 5 files changed, 501 insertions(+) diff --git a/cuda_core/tests/test_event.py b/cuda_core/tests/test_event.py index 1f62a131261..9c1b0d1e28f 100644 --- a/cuda_core/tests/test_event.py +++ b/cuda_core/tests/test_event.py @@ -195,6 +195,54 @@ def test_event_type_safety(init_cuda): assert (event is None) is False +def test_event_isub_not_implemented(init_cuda): + """Event.__isub__ returns NotImplemented for non-Event types.""" + device = Device() + stream = device.create_stream() + event = stream.record() + result = event.__isub__(42) + assert result is NotImplemented + + +def test_event_rsub_not_implemented(init_cuda): + """Event.__rsub__ returns NotImplemented for non-Event types.""" + device = Device() + stream = device.create_stream() + event = stream.record() + result = event.__rsub__(42) + assert result is NotImplemented + + +def test_event_get_ipc_descriptor_non_ipc(init_cuda): + """get_ipc_descriptor raises RuntimeError on a non-IPC event.""" + device = Device() + stream = device.create_stream() + event = stream.record() + with pytest.raises(RuntimeError, match="not IPC-enabled"): + event.get_ipc_descriptor() + + +def test_event_is_done_false(init_cuda): + """Event.is_done returns False when captured work has not yet completed.""" + device = Device() + latch = LatchKernel(device) + stream = device.create_stream() + latch.launch(stream) + event = stream.record() + # The latch holds the kernel; the event cannot be done yet. + assert event.is_done is False + latch.release() + event.sync() + + +def test_ipc_event_descriptor_direct_init(): + """IPCEventDescriptor cannot be instantiated directly.""" + import cuda.core._event as _event_module + + with pytest.raises(RuntimeError, match="cannot be instantiated directly"): + _event_module.IPCEventDescriptor() + + # ============================================================================ # Event Hash Tests # ============================================================================ diff --git a/cuda_core/tests/test_launcher.py b/cuda_core/tests/test_launcher.py index 899fdee4f54..fb43e068dbb 100644 --- a/cuda_core/tests/test_launcher.py +++ b/cuda_core/tests/test_launcher.py @@ -387,3 +387,132 @@ def test_kernel_arg_unsupported_type(): with pytest.raises(TypeError, match="unsupported type"): ParamHolder(["not_a_valid_kernel_arg"]) + + +def test_kernel_arg_ctypes_subclass_isinstance_fallback(): + """Subclassed ctypes types hit the isinstance fallback in prepare_ctypes_arg.""" + from cuda.core._kernel_arg_handler import ParamHolder + + class MyInt32(ctypes.c_int32): + pass + + class MyFloat(ctypes.c_float): + pass + + class MyBool(ctypes.c_bool): + pass + + # These should NOT raise — they should be handled via isinstance fallback + holder = ParamHolder([MyInt32(42), MyFloat(3.14), MyBool(True)]) + assert holder.ptr != 0 + + +@requires_module(np, "2.1") +def test_launch_scalar_argument_ctypes_subclass_fallback(): + """Subclassed ctypes scalars survive the launch path and reach the kernel correctly.""" + + class MyInt32(ctypes.c_int32): + pass + + dev = Device() + dev.set_current() + + mr = LegacyPinnedMemoryResource() + b = mr.allocate(np.dtype(np.int32).itemsize) + arr = np.from_dlpack(b).view(np.int32) + arr[:] = 0 + + scalar = MyInt32(-123456) + + code = r""" + template + __global__ void write_scalar(T* arr, T val) { + arr[0] = val; + } + """ + + arch = "".join(f"{i}" for i in dev.compute_capability) + pro_opts = ProgramOptions(std="c++17", arch=f"sm_{arch}") + prog = Program(code, code_type="c++", options=pro_opts) + ker_name = "write_scalar" + mod = prog.compile("cubin", name_expressions=(ker_name,)) + ker = mod.get_kernel(ker_name) + + # This exercises the prepare_ctypes_arg isinstance fallback through a real launch. + stream = dev.default_stream + config = LaunchConfig(grid=1, block=1) + launch(stream, config, ker, arr.ctypes.data, scalar) + stream.sync() + + assert arr[0] == scalar.value + + +def test_kernel_arg_numpy_subclass_isinstance_fallback(): + """Subclassed numpy scalars hit the isinstance fallback in prepare_numpy_arg.""" + from cuda.core._kernel_arg_handler import ParamHolder + + class MyInt32(np.int32): + pass + + class MyFloat32(np.float32): + pass + + holder = ParamHolder([MyInt32(7), MyFloat32(2.5)]) + assert holder.ptr != 0 + + +@requires_module(np, "2.1") +def test_launch_scalar_argument_numpy_subclass_fallback(): + """Subclassed numpy scalars survive the launch path and reach the kernel correctly.""" + + class MyFloat32(np.float32): + pass + + dev = Device() + dev.set_current() + + mr = LegacyPinnedMemoryResource() + b = mr.allocate(np.dtype(np.float32).itemsize) + arr = np.from_dlpack(b).view(np.float32) + arr[:] = 0.0 + + scalar = MyFloat32(3.14) + + code = r""" + template + __global__ void write_scalar(T* arr, T val) { + arr[0] = val; + } + """ + + arch = "".join(f"{i}" for i in dev.compute_capability) + pro_opts = ProgramOptions(std="c++17", arch=f"sm_{arch}") + prog = Program(code, code_type="c++", options=pro_opts) + ker_name = "write_scalar" + mod = prog.compile("cubin", name_expressions=(ker_name,)) + ker = mod.get_kernel(ker_name) + + # This exercises the prepare_numpy_arg isinstance fallback through a real launch. + stream = dev.default_stream + config = LaunchConfig(grid=1, block=1) + launch(stream, config, ker, arr.ctypes.data, scalar) + stream.sync() + + assert arr[0] == scalar + + +def test_kernel_arg_python_isinstance_fallbacks(): + """Subclassed Python builtins hit the isinstance fallback in ParamHolder.""" + from cuda.core._kernel_arg_handler import ParamHolder + + class MyBool(int): + """type(x) is not int, so fast path skips; isinstance(x, int) catches it.""" + + class MyFloat(float): + pass + + class MyComplex(complex): + pass + + holder = ParamHolder([MyBool(1), MyFloat(1.5), MyComplex(1 + 2j)]) + assert holder.ptr != 0 diff --git a/cuda_core/tests/test_linker.py b/cuda_core/tests/test_linker.py index 30a6a033495..0d4ff91dcd9 100644 --- a/cuda_core/tests/test_linker.py +++ b/cuda_core/tests/test_linker.py @@ -221,3 +221,24 @@ def test_linker_logs_cached_after_link(compile_ptx_functions): # Calling again should return the same observable values. assert linker.get_error_log() == err_log assert linker.get_info_log() == info_log + + +def test_linker_handle(compile_ptx_functions): + """Linker.handle returns a non-null handle object.""" + options = LinkerOptions(arch=ARCH) + linker = Linker(*compile_ptx_functions, options=options) + handle = linker.handle + assert handle is not None + assert int(handle) != 0 + + +@pytest.mark.skipif(is_culink_backend, reason="nvjitlink options only tested with nvjitlink backend") +def test_linker_options_nvjitlink_options_as_str(): + """_prepare_nvjitlink_options(as_bytes=False) returns plain strings.""" + opts = LinkerOptions(arch=ARCH, debug=True, lineinfo=True) + options = opts._prepare_nvjitlink_options(as_bytes=False) + assert isinstance(options, list) + assert all(isinstance(o, str) for o in options) + assert f"-arch={ARCH}" in options + assert "-g" in options + assert "-lineinfo" in options diff --git a/cuda_core/tests/test_program.py b/cuda_core/tests/test_program.py index a062f3714e1..52107073a6c 100644 --- a/cuda_core/tests/test_program.py +++ b/cuda_core/tests/test_program.py @@ -773,3 +773,107 @@ def test_program_options_as_bytes_nvvm_unsupported_option(): options = ProgramOptions(arch="sm_80", lineinfo=True) with pytest.raises(CUDAError, match="not supported by NVVM backend"): options.as_bytes("nvvm") + + +def test_program_options_repr(): + """ProgramOptions.__repr__ returns a human-readable string.""" + opts = ProgramOptions(name="mykernel", arch="sm_80") + r = repr(opts) + assert "ProgramOptions" in r + assert "mykernel" in r + assert "sm_80" in r + + +def test_program_options_bad_define_macro_short_tuple(): + """define_macro with a 1-element tuple raises RuntimeError.""" + opts = ProgramOptions(name="test", arch="sm_80", define_macro=("ONLY_NAME",)) + with pytest.raises(RuntimeError, match="Expected define_macro tuple"): + opts.as_bytes("nvrtc") + + +def test_program_options_bad_define_macro_non_str_value(): + """define_macro tuple with a non-string value raises RuntimeError.""" + opts = ProgramOptions(name="test", arch="sm_80", define_macro=("MY_MACRO", 99)) + with pytest.raises(RuntimeError, match="Expected define_macro tuple"): + opts.as_bytes("nvrtc") + + +def test_program_options_bad_define_macro_list_non_str(): + """define_macro list containing a non-str/non-tuple item raises RuntimeError.""" + opts = ProgramOptions(name="test", arch="sm_80", define_macro=[42]) + with pytest.raises(RuntimeError, match="Expected define_macro"): + opts.as_bytes("nvrtc") + + +def test_program_options_bad_define_macro_list_bad_tuple(): + """define_macro list with a malformed tuple inside raises RuntimeError.""" + opts = ProgramOptions(name="test", arch="sm_80", define_macro=[("ONLY_NAME",)]) + with pytest.raises(RuntimeError, match="Expected define_macro"): + opts.as_bytes("nvrtc") + + +def test_ptx_program_extra_sources_unsupported(ptx_code_object): + """PTX backend raises ValueError when extra_sources is specified.""" + options = ProgramOptions(extra_sources=[("module1", b"data")]) + with pytest.raises(ValueError, match="extra_sources is not supported by the PTX backend"): + Program(ptx_code_object.code.decode(), "ptx", options) + + +def test_ptx_program_handle_is_linker_handle(init_cuda, ptx_code_object): + """Program.handle for the PTX backend delegates to the linker handle.""" + program = Program(ptx_code_object.code.decode(), "ptx") + handle = program.handle + assert handle is not None + assert int(handle) != 0 + program.close() + + +@nvvm_available +def test_nvvm_program_wrong_code_type(): + """NVVM backend raises TypeError when code is not str/bytes/bytearray.""" + with pytest.raises(TypeError, match="NVVM IR code must be provided as str, bytes, or bytearray"): + Program(42, "nvvm") + + +def test_extra_sources_not_sequence(): + """extra_sources must be a sequence; non-sequence raises TypeError.""" + with pytest.raises(TypeError, match="extra_sources must be a sequence of 2-tuples"): + ProgramOptions(name="test", arch="sm_80", extra_sources=42) + + +def test_extra_sources_bad_module_not_tuple(): + """extra_sources items must be 2-tuples; non-tuple item raises TypeError.""" + with pytest.raises(TypeError, match="Each extra module must be a 2-tuple"): + ProgramOptions(name="test", arch="sm_80", extra_sources=["not_a_tuple"]) + + +def test_extra_sources_bad_module_name_not_str(): + """extra_sources module name must be a string; non-str raises TypeError.""" + with pytest.raises(TypeError, match="Module name at index 0 must be a string"): + ProgramOptions(name="test", arch="sm_80", extra_sources=[(42, b"source")]) + + +def test_extra_sources_bad_module_source_wrong_type(): + """extra_sources module source must be str/bytes/bytearray.""" + with pytest.raises(TypeError, match="Module source at index 0 must be str"): + ProgramOptions(name="test", arch="sm_80", extra_sources=[("mod", 42)]) + + +def test_extra_sources_empty_source(): + """extra_sources module source cannot be empty bytes.""" + with pytest.raises(ValueError, match="Module source for 'mod'.*cannot be empty"): + ProgramOptions(name="test", arch="sm_80", extra_sources=[("mod", b"")]) + + +def test_nvrtc_compile_with_logs_capture(init_cuda): + """Program.compile with logs= exercises the NVRTC program-log reading path.""" + import io + + # #warning generates a non-empty NVRTC program log, ensuring logsize > 1. + code = '#warning "test log capture"\nextern "C" __global__ void my_kernel() {}' + program = Program(code, "c++") + logs = io.StringIO() + result = program.compile("ptx", logs=logs) + assert isinstance(result, ObjectCode) + assert logs.getvalue(), "Expected non-empty compilation log from #warning directive" + program.close() diff --git a/cuda_core/tests/test_utils.py b/cuda_core/tests/test_utils.py index 4bdebcbde36..6228c2b2222 100644 --- a/cuda_core/tests/test_utils.py +++ b/cuda_core/tests/test_utils.py @@ -712,3 +712,202 @@ def test_ml_dtypes_bfloat16_dlpack_requires_ml_dtypes(init_cuda, no_ml_dtypes, a smv = api(a, stream_ptr=0) with pytest.raises(NotImplementedError, match=r"requires `ml_dtypes`"): smv.dtype # noqa: B018 + + +def test_strided_memory_view_repr(): + """__repr__ returns a descriptive string.""" + src = np.arange(6, dtype=np.int32).reshape(2, 3) + view = StridedMemoryView.from_any_interface(src, stream_ptr=-1) + r = repr(view) + assert r.startswith("StridedMemoryView(ptr=") + + +def test_strided_memory_view_copy_to_raises(): + """copy_to raises NotImplementedError.""" + src = np.zeros(5, dtype=np.float32) + view = StridedMemoryView.from_any_interface(src, stream_ptr=-1) + with pytest.raises(NotImplementedError, match="copy_to"): + view.copy_to(view, stream=None) + + +def test_strided_memory_view_get_layout_error(): + """get_layout raises ValueError for an empty (uninitialized) StridedMemoryView.""" + with pytest.warns(DeprecationWarning, match="deprecated"): + view = StridedMemoryView() + with pytest.raises(ValueError, match="Cannot infer layout"): + _ = view._layout + + +@pytest.mark.skipif(cp is None, reason="CuPy is not installed") +def test_strided_memory_view_deprecated_cai_init(init_cuda): + """Deprecated StridedMemoryView(cai_obj) init path for CAI-only objects.""" + src = cp.zeros(5, dtype=cp.float32) + dev = Device() + stream = dev.create_stream() + cai_only = _EnforceCAIView(src) + with pytest.deprecated_call(): + view = StridedMemoryView(cai_only, stream_ptr=stream.handle) + assert view.is_device_accessible is True + assert view.ptr == src.data.ptr + + +@pytest.mark.skipif(cp is None, reason="CuPy is not installed") +def test_from_any_interface_cai_fallback(init_cuda): + """from_any_interface falls back to CAI when an object has no __dlpack__.""" + src = cp.zeros(5, dtype=cp.float32) + dev = Device() + stream = dev.create_stream() + cai_only = _EnforceCAIView(src) + view = StridedMemoryView.from_any_interface(cai_only, stream_ptr=stream.handle) + assert view.is_device_accessible is True + assert view.ptr == src.data.ptr + + +def test_strided_memory_view_copy_from_raises(): + """copy_from raises NotImplementedError.""" + src = np.zeros(5, dtype=np.float32) + view = StridedMemoryView.from_any_interface(src, stream_ptr=-1) + with pytest.raises(NotImplementedError, match="copy_from"): + view.copy_from(view, stream=None) + + +def test_strided_memory_view_view_no_args_returns_self(): + """view() with layout=None and dtype=None returns self.""" + src = np.arange(6, dtype=np.int32).reshape(2, 3) + view = StridedMemoryView.from_any_interface(src, stream_ptr=-1) + same = view.view(layout=None, dtype=None) + assert same is view + + +def test_strided_memory_view_view_with_dtype_only(): + """view() with only dtype re-interprets using current layout.""" + src = np.arange(4, dtype=np.float32) + view = StridedMemoryView.from_any_interface(src, stream_ptr=-1) + viewed = view.view(dtype=np.dtype("int32")) + assert viewed.dtype == np.dtype("int32") + assert viewed._layout == view._layout + + +def test_dlpack_export_structured_dtype_raises(): + """Structured dtypes are rejected for DLPack export.""" + dt = np.dtype([("x", np.float32), ("y", np.int32)]) # itemsize=8 + # Create a valid view first, then re-view with the structured dtype to + # bypass numpy's own __dlpack__ rejection during import. + src = np.zeros(3, dtype=np.float64) # itemsize=8 to match + view = StridedMemoryView.from_any_interface(src, stream_ptr=-1) + bad_view = view.view(dtype=dt) + with pytest.raises(BufferError, match="Structured dtypes"): + bad_view.__dlpack__() + + +def test_dlpack_export_unsupported_dtype_raises(): + """Unsupported dtype kind is rejected for DLPack export.""" + # numpy void dtype (kind='V', typestr='|V4') hits the else branch + # in _smv_dtype_numpy_to_dlpack at _memoryview.pyx:577 + src = np.zeros(3, dtype=np.float32) # itemsize=4 to match V4 + view = StridedMemoryView.from_any_interface(src, stream_ptr=-1) + bad_view = view.view(dtype=np.dtype("V4")) + with pytest.raises(BufferError, match="Unsupported dtype for DLPack export"): + bad_view.__dlpack__() + + +class _FakeCAIv2: + """Object with CUDA Array Interface v2 (unsupported).""" + + def __init__(self): + self.__cuda_array_interface__ = { + "version": 2, + "shape": (5,), + "typestr": " Date: Fri, 17 Apr 2026 10:16:53 -0400 Subject: [PATCH 013/244] Remove _SynchronousMemoryResource from cuda.core._memory public API (#1933) Remove _SynchronousMemoryResource from __all__ in _legacy.py since it is a private symbol (underscore-prefixed) that should not be part of the public API surface. Update internal imports in _device.pyx and test_launcher.py to use the full private module path (cuda.core._memory._legacy) instead of the package-level import. --- cuda_core/cuda/core/_device.pyx | 2 +- cuda_core/cuda/core/_memory/_legacy.py | 4 ++-- cuda_core/tests/test_launcher.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cuda_core/cuda/core/_device.pyx b/cuda_core/cuda/core/_device.pyx index fee701b14ed..1ea2df564c4 100644 --- a/cuda_core/cuda/core/_device.pyx +++ b/cuda_core/cuda/core/_device.pyx @@ -1145,7 +1145,7 @@ class Device: from cuda.core._memory import DeviceMemoryResource self._memory_resource = DeviceMemoryResource(self._device_id) else: - from cuda.core._memory import _SynchronousMemoryResource + from cuda.core._memory._legacy import _SynchronousMemoryResource self._memory_resource = _SynchronousMemoryResource(self._device_id) return self._memory_resource diff --git a/cuda_core/cuda/core/_memory/_legacy.py b/cuda_core/cuda/core/_memory/_legacy.py index 919c37fc50d..24ce88487ea 100644 --- a/cuda_core/cuda/core/_memory/_legacy.py +++ b/cuda_core/cuda/core/_memory/_legacy.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 @@ -17,7 +17,7 @@ driver, ) -__all__ = ["LegacyPinnedMemoryResource", "_SynchronousMemoryResource"] +__all__ = ["LegacyPinnedMemoryResource"] class LegacyPinnedMemoryResource(MemoryResource): diff --git a/cuda_core/tests/test_launcher.py b/cuda_core/tests/test_launcher.py index fb43e068dbb..7bd480edf23 100644 --- a/cuda_core/tests/test_launcher.py +++ b/cuda_core/tests/test_launcher.py @@ -24,7 +24,7 @@ ProgramOptions, launch, ) -from cuda.core._memory import _SynchronousMemoryResource +from cuda.core._memory._legacy import _SynchronousMemoryResource from cuda.core._utils.cuda_utils import CUDAError From 9cee313287843190f2f23e716c59bd0b203513f5 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 17 Apr 2026 10:20:01 -0400 Subject: [PATCH 014/244] Make namespace hygiene easier in cuda.core.__init__.py (#1934) * Make namespace hygiene easier in cuda.core.__init__.py * Simplify further --- cuda_core/cuda/core/__init__.py | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index 69caf1c0de7..7a3ce9ceb85 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -4,29 +4,29 @@ from cuda.core._version import __version__ -try: + +def _import_versioned_module(): + import importlib + from cuda import bindings -except ImportError: - raise ImportError("cuda.bindings 12.x or 13.x must be installed") from None -else: - cuda_major, cuda_minor = bindings.__version__.split(".")[:2] + + cuda_major = bindings.__version__.split(".")[0] if cuda_major not in ("12", "13"): raise ImportError("cuda.bindings 12.x or 13.x must be installed") -import importlib + subdir = f"cu{cuda_major}" + try: + versioned_mod = importlib.import_module(f".{subdir}", __package__) + # Import all symbols from the module + globals().update(versioned_mod.__dict__) + except ImportError: + # This is not a wheel build, but a conda or local build, do nothing + pass + + +_import_versioned_module() +del _import_versioned_module -subdir = f"cu{cuda_major}" -try: - versioned_mod = importlib.import_module(f".{subdir}", __package__) - # Import all symbols from the module - globals().update(versioned_mod.__dict__) -except ImportError: - # This is not a wheel build, but a conda or local build, do nothing - pass -else: - del versioned_mod -finally: - del bindings, importlib, subdir, cuda_major, cuda_minor from cuda.core import system, utils from cuda.core._device import Device From f73c1c87f9496417e4419f2f181cb5471dbc848a Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 17 Apr 2026 10:27:01 -0400 Subject: [PATCH 015/244] Remove _StridedLayout from cuda.core public API (#1932) _StridedLayout is a private symbol (underscore-prefixed) that was imported from cuda.core._layout into cuda/core/__init__.py, making it unintentionally available as a public name. All internal usage (in _memoryview.pyx) already imports it directly via cimport from the private module, so the re-export in __init__.py is unnecessary. --- cuda_core/cuda/core/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index 7a3ce9ceb85..dfd52accea3 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -34,7 +34,6 @@ def _import_versioned_module(): from cuda.core._graphics import GraphicsResource from cuda.core._launch_config import LaunchConfig from cuda.core._launcher import launch -from cuda.core._layout import _StridedLayout from cuda.core._linker import Linker, LinkerOptions from cuda.core._memory import ( Buffer, From 56b53ca6976ac99b0d5c1c71090e9df1ba8e3027 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 17 Apr 2026 10:49:32 -0400 Subject: [PATCH 016/244] Add Cython ABI checking tool to toolshed (#1929) * Add Cython ABI checking tool to toolshed * Update toolshed/check_cython_abi.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update toolshed/check_cython_abi.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update toolshed/check_cython_abi.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address comments in PR * Address some of the comments in the PR --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- toolshed/check_cython_abi.py | 218 +++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 toolshed/check_cython_abi.py diff --git a/toolshed/check_cython_abi.py b/toolshed/check_cython_abi.py new file mode 100644 index 00000000000..2199d526a3d --- /dev/null +++ b/toolshed/check_cython_abi.py @@ -0,0 +1,218 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +""" +Tool to check for Cython ABI changes in a given package. + +There are different types of ABI changes, only one of which is covered by this tool: + +- cdef function signatures (capsule strings) — covered here +- cdef class struct size (tp_basicsize) — not covered +- cdef class vtable layout / method reordering — not covered, and this one fails as silent UB rather than an import-time error +- Fused specialization ordering — partially covered (reorders manifest as capsule-name deltas, but the mapping is non-obvious) + +The workflow is basically: + +1) Build and install a "clean" upstream version of the package. + +2) Generate ABI files from the package by running (in the same venv in which the + package is installed), where `package_name` is the import path to the package, + e.g. `cuda.bindings`: + + python check_cython_abi.py generate + +3) Checkout a version with the changes to be tested, and build and install. + +4) Check the ABI against the previously generated files by running: + + python check_cython_abi.py check +""" + +import ctypes +import importlib +import json +import sys +import sysconfig +from pathlib import Path + +EXT_SUFFIX = sysconfig.get_config_var("EXT_SUFFIX") +ABI_SUFFIX = ".abi.json" + + +_pycapsule_get_name = ctypes.pythonapi.PyCapsule_GetName +_pycapsule_get_name.restype = ctypes.c_char_p +_pycapsule_get_name.argtypes = [ctypes.py_object] + + +def get_capsule_name(v: object) -> str: + return _pycapsule_get_name(v).decode("utf-8") + + +def short_stem(name: str) -> str: + return name.split(".", 1)[0] + + +def get_package_path(package_name: str) -> Path: + package = importlib.import_module(package_name) + return Path(package.__file__).parent + + +def import_from_path(root_package: str, root_dir: Path, path: Path) -> object: + path = path.relative_to(root_dir) + parts = [root_package] + list(path.parts[:-1]) + [short_stem(path.name)] + return importlib.import_module(".".join(parts)) + + +def so_path_to_abi_path(so_path: Path, build_dir: Path, abi_dir: Path) -> Path: + abi_name = short_stem(so_path.name) + ABI_SUFFIX + return abi_dir / so_path.parent.relative_to(build_dir) / abi_name + + +def abi_path_to_so_path(abi_path: Path, build_dir: Path, abi_dir: Path) -> Path: + so_name = short_stem(abi_path.name) + EXT_SUFFIX + return build_dir / abi_path.parent.relative_to(abi_dir) / so_name + + +def is_cython_module(module: object) -> bool: + # This is kind of quick-and-dirty, but seems to work + return hasattr(module, "__pyx_capi__") + + +def module_to_json(module: object) -> dict: + """ + Converts extracts information about a Cython-compiled .so into JSON-serializable information. + """ + # Sort the dictionary by keys to make diffs in the JSON files smaller + pyx_capi = module.__pyx_capi__ + + return { + "functions": {k: get_capsule_name(pyx_capi[k]) for k in sorted(pyx_capi.keys())}, + } + + +def check_functions(expected: dict[str, str], found: dict[str, str]) -> tuple[bool, bool]: + has_errors = False + has_allowed_changes = False + for k, v in expected.items(): + if k not in found: + print(f" Missing symbol: {k}") + has_errors = True + elif found[k] != v: + print(f" Changed symbol: {k}: expected {v}, got {found[k]}") + has_errors = True + for k, v in found.items(): + if k not in expected: + print(f" Added symbol: {k}") + has_allowed_changes = True + return has_errors, has_allowed_changes + + +def compare(expected: dict, found: dict) -> tuple[bool, bool]: + has_errors = False + has_allowed_changes = False + + errors, allowed_changes = check_functions(expected["functions"], found["functions"]) + has_errors |= errors + has_allowed_changes |= allowed_changes + + return has_errors, has_allowed_changes + + +def check(package: str, abi_dir: Path) -> bool: + build_dir = get_package_path(package) + + has_errors = False + has_allowed_changes = False + for abi_path in Path(abi_dir).glob(f"**/*{ABI_SUFFIX}"): + so_path = abi_path_to_so_path(abi_path, build_dir, abi_dir) + if so_path.is_file(): + try: + module = import_from_path(package, build_dir, so_path) + except ImportError: + print(f"Failed to import module for {so_path.relative_to(build_dir)}") + has_errors = True + continue + if is_cython_module(module): + found_json = module_to_json(module) + with open(abi_path, encoding="utf-8") as f: + expected_json = json.load(f) + print(f"Checking module: {so_path.relative_to(build_dir)}") + check_errors, check_allowed_changes = compare(expected_json, found_json) + has_errors |= check_errors + has_allowed_changes |= check_allowed_changes + else: + print(f"Module no longer has an exposed ABI or is no longer Cython: {so_path.relative_to(build_dir)}") + has_errors = True + else: + print(f"No module found for {abi_path.relative_to(abi_dir)}") + has_errors = True + + for so_path in Path(build_dir).glob(f"**/*{EXT_SUFFIX}"): + module = import_from_path(package, build_dir, so_path) + if hasattr(module, "__pyx_capi__"): + abi_path = so_path_to_abi_path(so_path, build_dir, abi_dir) + if not abi_path.is_file(): + print(f"New module added {so_path.relative_to(build_dir)}") + has_allowed_changes = True + + print() + if has_errors: + print("ERRORS FOUND") + return True + elif has_allowed_changes: + print("Allowed changes found.") + else: + print("No changes found.") + return False + + +def regenerate(package: str, abi_dir: Path) -> bool: + if abi_dir.is_dir(): + print(f"ABI directory {abi_dir} already exists. Please remove it before regenerating.") + return True + + build_dir = get_package_path(package) + for so_path in Path(build_dir).glob(f"**/*{EXT_SUFFIX}"): + try: + module = import_from_path(package, build_dir, so_path) + except ImportError: + print(f"Failed to import module: {so_path.relative_to(build_dir)}") + continue + if is_cython_module(module): + print(f"Generating ABI from {so_path.relative_to(build_dir)}") + abi_path = so_path_to_abi_path(so_path, build_dir, abi_dir) + abi_path.parent.mkdir(parents=True, exist_ok=True) + with open(abi_path, "w", encoding="utf-8") as f: + json.dump(module_to_json(module), f, indent=2) + + return False + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + prog="check_cython_abi", description="Checks for changes in the Cython ABI of a given package" + ) + + subparsers = parser.add_subparsers() + + regen_parser = subparsers.add_parser("generate", help="Regenerate the ABI files") + regen_parser.set_defaults(func=regenerate) + regen_parser.add_argument("package", help="Python package to collect data from") + regen_parser.add_argument("dir", help="Output directory to save data to") + + check_parser = subparsers.add_parser("check", help="Check the API against existing ABI files") + check_parser.set_defaults(func=check) + check_parser.add_argument("package", help="Python package to collect data from") + check_parser.add_argument("dir", help="Input directory to read data from") + + args = parser.parse_args() + if hasattr(args, "func"): + if args.func(args.package, Path(args.dir)): + sys.exit(1) + else: + parser.print_help() + sys.exit(1) From b991295574d644ab1cb5e6fbcde36cdd92bf26a5 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:27:39 -0400 Subject: [PATCH 017/244] ci: guard fromJSON in detect-changes against empty pr-info output (#1940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The detect-changes job's `Detect changed paths` step gates on `startsWith(github.ref_name, 'pull-request/')`, but GitHub Actions evaluates step-level `env:` expressions eagerly — the `if:` gate does not short-circuit them. On push to main, tag, or schedule events the preceding `Resolve PR base branch` step is skipped and its outputs are empty strings, so `fromJSON(steps.pr-info.outputs.pr-info)` raises a template error and fails the step even though its `if:` would otherwise skip it. That failure cascades into the final "Check job status" aggregation, turning every push-to-main CI run red (see run 24566662170). Guard the `fromJSON` call with a short-circuit so the expression resolves to an empty string when pr-info did not run. On PR events the expression still evaluates to the PR's actual base ref. --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 525c210889c..f575d2f851e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,7 +121,15 @@ jobs: id: filter if: ${{ startsWith(github.ref_name, 'pull-request/') }} env: - BASE_REF: ${{ fromJSON(steps.pr-info.outputs.pr-info).base.ref }} + # GitHub Actions evaluates step-level `env:` expressions eagerly — + # the step's `if:` gate does NOT short-circuit them. On non-PR + # events (push/tag/schedule), `pr-info` is skipped and its outputs + # are empty strings, so `fromJSON('')` would raise a template error + # and fail the step despite `if:` being false. Guard the + # `fromJSON` call with a short-circuit so the expression resolves + # to an empty string on non-PR events; the step is still gated + # off by `if:`, so `BASE_REF` is never consumed there. + BASE_REF: ${{ steps.pr-info.outputs.pr-info && fromJSON(steps.pr-info.outputs.pr-info).base.ref || '' }} run: | # Diff against the merge base with the PR's actual target branch. # Uses merge-base so diverged branches only show files changed on From a18022c73f853c8c4a2f827b39456da171444e7c Mon Sep 17 00:00:00 2001 From: Abhilash Majumder Date: Fri, 17 Apr 2026 23:45:07 +0530 Subject: [PATCH 018/244] [NVVM] Expose nvvm version detection in cuda.bindings.utils. (#1837) * refactor cuda bindings utils for nvvm * [pre-commit.ci] auto code formatting * refresh * renaming * exceptions * refresh * [pre-commit.ci] auto code formatting * refresh * [pre-commit.ci] auto code formatting * refresh * refresh * refresh * [pre-commit.ci] auto code formatting * refresh * [pre-commit.ci] auto code formatting * refresh --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- cuda_bindings/cuda/bindings/utils/__init__.py | 3 +- .../cuda/bindings/utils/_nvvm_utils.py | 89 +++++++++++++ cuda_bindings/tests/test_utils.py | 53 +++++++- cuda_core/tests/test_program.py | 125 +++++------------- 4 files changed, 178 insertions(+), 92 deletions(-) create mode 100644 cuda_bindings/cuda/bindings/utils/_nvvm_utils.py diff --git a/cuda_bindings/cuda/bindings/utils/__init__.py b/cuda_bindings/cuda/bindings/utils/__init__.py index e4fcb15e8fd..3fe75ca13f0 100644 --- a/cuda_bindings/cuda/bindings/utils/__init__.py +++ b/cuda_bindings/cuda/bindings/utils/__init__.py @@ -1,7 +1,8 @@ -# 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: LicenseRef-NVIDIA-SOFTWARE-LICENSE from typing import Any, Callable +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 diff --git a/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py b/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py new file mode 100644 index 00000000000..de7b111e7bf --- /dev/null +++ b/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +from typing import Sequence + +_PRECHECK_NVVM_IR = """target triple = "nvptx64-unknown-cuda" +target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-i128:128:128-f32:32:32-f64:64:64-v16:16:16-v32:32:32-v64:64:64-v128:128:128-n16:32:64" + +define void @dummy_kernel() {{ +entry: + ret void +}} + +!nvvm.annotations = !{{!0}} +!0 = !{{void ()* @dummy_kernel, !"kernel", i32 1}} + +!nvvmir.version = !{{!1}} +!1 = !{{i32 {major}, i32 {minor}, i32 {debug_major}, i32 {debug_minor}}} +""" + + +def check_nvvm_compiler_options(options: Sequence[str]) -> bool: + """ + Abstracted from https://github.com/NVIDIA/numba-cuda/pull/681 + + Check if the specified options are supported by the current libNVVM version. + + The options are a list of strings, each representing a compiler option. + + If the test program fails to compile, the options are not supported and False + is returned. + + If the test program compiles successfully, True is returned. + + cuda.bindings.nvvm returns exceptions instead of return codes. + + Parameters + ---------- + options : Sequence[str] + List of compiler options as strings (e.g., ["-arch=compute_90", "-g"]). + + Returns + ------- + bool + True if the options are supported, False otherwise. + + Examples + -------- + >>> from cuda.bindings.utils import check_nvvm_compiler_options + >>> check_nvvm_compiler_options(["-arch=compute_90", "-g"]) + True + """ + try: + from cuda.bindings import nvvm + except ModuleNotFoundError as exc: + if exc.name == "nvvm": + return False + raise + + from cuda.bindings._internal.nvvm import _inspect_function_pointer + + if _inspect_function_pointer("__nvvmCreateProgram") == 0: + return False + + program = nvvm.create_program() + try: + major, minor, debug_major, debug_minor = nvvm.ir_version() + precheck_ir = _PRECHECK_NVVM_IR.format( + major=major, + minor=minor, + debug_major=debug_major, + debug_minor=debug_minor, + ) + precheck_ir_bytes = precheck_ir.encode("utf-8") + nvvm.add_module_to_program( + program, + precheck_ir_bytes, + len(precheck_ir_bytes), + "precheck.ll", + ) + try: + nvvm.compile_program(program, len(options), options) + except nvvm.nvvmError as e: + if e.status == nvvm.Result.ERROR_INVALID_OPTION: + return False + raise + finally: + nvvm.destroy_program(program) + return True diff --git a/cuda_bindings/tests/test_utils.py b/cuda_bindings/tests/test_utils.py index fd89cd00bde..01e30269291 100644 --- a/cuda_bindings/tests/test_utils.py +++ b/cuda_bindings/tests/test_utils.py @@ -11,10 +11,29 @@ from cuda.bindings import driver, runtime from cuda.bindings._internal.utils import get_c_compiler -from cuda.bindings.utils import get_cuda_native_handle, get_minimal_required_cuda_ver_from_ptx_ver, get_ptx_ver +from cuda.bindings.utils import ( + check_nvvm_compiler_options, + get_cuda_native_handle, + get_minimal_required_cuda_ver_from_ptx_ver, + get_ptx_ver, +) have_cufile = importlib.util.find_spec("cuda.bindings.cufile") is not None + +def _is_libnvvm_available() -> bool: + from cuda.bindings._internal.nvvm import _inspect_function_pointer + from cuda.pathfinder import DynamicLibNotFoundError + + try: + return _inspect_function_pointer("__nvvmCreateProgram") != 0 + except DynamicLibNotFoundError: + return False + + +_libnvvm_available = _is_libnvvm_available() +_skip_no_libnvvm = pytest.mark.skipif(not _libnvvm_available, reason="libNVVM not available") + ptx_88_kernel = r""" .version 8.8 .target sm_75 @@ -118,3 +137,35 @@ def test_get_c_compiler(): c_compiler = get_c_compiler() prefix = ("GCC", "Clang", "MSVC", "Unknown") assert sum(c_compiler.startswith(p) for p in prefix) == 1 + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_valid(): + assert check_nvvm_compiler_options(["-arch=compute_90"]) is True + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_invalid(): + assert check_nvvm_compiler_options(["--this-is-not-a-valid-option"]) is False + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_empty(): + assert check_nvvm_compiler_options([]) is True + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_multiple_valid(): + assert check_nvvm_compiler_options(["-arch=compute_90", "-opt=3", "-g"]) is True + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_arch_detection(): + assert check_nvvm_compiler_options(["-arch=compute_90"]) is True + assert check_nvvm_compiler_options(["-arch=compute_99999"]) is False + + +def test_check_nvvm_compiler_options_no_libnvvm(): + if _libnvvm_available: + pytest.skip("libNVVM is available; this test targets the fallback path") + assert check_nvvm_compiler_options(["-arch=compute_90"]) is False diff --git a/cuda_core/tests/test_program.py b/cuda_core/tests/test_program.py index 52107073a6c..992ce336555 100644 --- a/cuda_core/tests/test_program.py +++ b/cuda_core/tests/test_program.py @@ -70,81 +70,25 @@ def _has_nvrtc_pch_apis_for_tests(): ) -_libnvvm_version = None -_libnvvm_version_attempted = False - -precheck_nvvm_ir = """target triple = "nvptx64-unknown-cuda" -target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-i128:128:128-f32:32:32-f64:64:64-v16:16:16-v32:32:32-v64:64:64-v128:128:128-n16:32:64" - -define void @dummy_kernel() {{ - entry: - ret void -}} - -!nvvm.annotations = !{{!0}} -!0 = !{{void ()* @dummy_kernel, !"kernel", i32 1}} - -!nvvmir.version = !{{!1}} -!1 = !{{i32 {major}, i32 {minor}, i32 {debug_major}, i32 {debug_minor}}} -""" - - -def _get_libnvvm_version_for_tests(): - """ - Detect libNVVM version by compiling dummy IR and analyzing the PTX output. - - Workaround for the lack of direct libNVVM version API (nvbugs 5312315). - The approach: - - Compile a small dummy NVVM IR to PTX - - Use PTX version analysis APIs if available to infer libNVVM version - - Cache the result for future use - """ - global _libnvvm_version, _libnvvm_version_attempted +def _has_check_nvvm_compiler_options(): + try: + import cuda.bindings.utils as utils + except ModuleNotFoundError: + return False + return hasattr(utils, "check_nvvm_compiler_options") - if _libnvvm_version_attempted: - return _libnvvm_version - _libnvvm_version_attempted = True +has_nvvm_option_checker = pytest.mark.skipif( + not _has_check_nvvm_compiler_options(), + reason="cuda.bindings.utils.check_nvvm_compiler_options not available (cuda-bindings too old?)", +) - try: - from cuda.core._program import _get_nvvm_module - nvvm = _get_nvvm_module() - - try: - from cuda.bindings.utils import get_minimal_required_cuda_ver_from_ptx_ver, get_ptx_ver - except ImportError: - _libnvvm_version = None - return _libnvvm_version - - program = nvvm.create_program() - try: - major, minor, debug_major, debug_minor = nvvm.ir_version() - global precheck_nvvm_ir - precheck_nvvm_ir = precheck_nvvm_ir.format( - major=major, minor=minor, debug_major=debug_major, debug_minor=debug_minor - ) - precheck_ir_bytes = precheck_nvvm_ir.encode("utf-8") - nvvm.add_module_to_program(program, precheck_ir_bytes, len(precheck_ir_bytes), "precheck.ll") - - options = ["-arch=compute_90"] - nvvm.verify_program(program, len(options), options) - nvvm.compile_program(program, len(options), options) - - ptx_size = nvvm.get_compiled_result_size(program) - ptx_data = bytearray(ptx_size) - nvvm.get_compiled_result(program, ptx_data) - ptx_str = ptx_data.decode("utf-8") - ptx_version = get_ptx_ver(ptx_str) - cuda_version = get_minimal_required_cuda_ver_from_ptx_ver(ptx_version) - _libnvvm_version = cuda_version - return _libnvvm_version - finally: - nvvm.destroy_program(program) +def _check_nvvm_arch(arch: str) -> bool: + """Check if the given NVVM arch is supported by the installed libNVVM.""" + from cuda.bindings.utils import check_nvvm_compiler_options - except Exception: - _libnvvm_version = None - return _libnvvm_version + return check_nvvm_compiler_options([f"-arch={arch}"]) @pytest.fixture(scope="session") @@ -524,10 +468,13 @@ def test_nvvm_compile_invalid_ir(): ), pytest.param( ProgramOptions(name="test_sm110_1", arch="sm_110", device_code_optimize=False), - marks=pytest.mark.skipif( - (_get_libnvvm_version_for_tests() or 0) < 13000, - reason="Compute capability 110 requires libNVVM >= 13.0", - ), + marks=[ + has_nvvm_option_checker, + pytest.mark.skipif( + _has_check_nvvm_compiler_options() and not _check_nvvm_arch("compute_110"), + reason="Compute capability 110 not supported by installed libNVVM", + ), + ], ), pytest.param( ProgramOptions( @@ -539,17 +486,23 @@ def test_nvvm_compile_invalid_ir(): fma=True, device_code_optimize=True, ), - marks=pytest.mark.skipif( - (_get_libnvvm_version_for_tests() or 0) < 13000, - reason="Compute capability 110 requires libNVVM >= 13.0", - ), + marks=[ + has_nvvm_option_checker, + pytest.mark.skipif( + _has_check_nvvm_compiler_options() and not _check_nvvm_arch("compute_110"), + reason="Compute capability 110 not supported by installed libNVVM", + ), + ], ), pytest.param( ProgramOptions(name="test_sm110_3", arch="sm_110", link_time_optimization=True), - marks=pytest.mark.skipif( - (_get_libnvvm_version_for_tests() or 0) < 13000, - reason="Compute capability 110 requires libNVVM >= 13.0", - ), + marks=[ + has_nvvm_option_checker, + pytest.mark.skipif( + _has_check_nvvm_compiler_options() and not _check_nvvm_arch("compute_110"), + reason="Compute capability 110 not supported by installed libNVVM", + ), + ], ), ], ) @@ -729,12 +682,8 @@ def test_program_options_as_bytes_nvrtc(): """Test ProgramOptions.as_bytes() for NVRTC backend""" options = ProgramOptions(arch="sm_80", debug=True, lineinfo=True, ftz=True) nvrtc_options = options.as_bytes("nvrtc") - - # Should return list of bytes assert isinstance(nvrtc_options, list) assert all(isinstance(opt, bytes) for opt in nvrtc_options) - - # Decode to check content options_str = [opt.decode() for opt in nvrtc_options] assert "-arch=sm_80" in options_str assert "--device-debug" in options_str @@ -747,12 +696,8 @@ def test_program_options_as_bytes_nvvm(): """Test ProgramOptions.as_bytes() for NVVM backend""" options = ProgramOptions(arch="sm_80", debug=True, ftz=True, device_code_optimize=True) nvvm_options = options.as_bytes("nvvm") - - # Should return list of bytes (same as other backends) assert isinstance(nvvm_options, list) assert all(isinstance(opt, bytes) for opt in nvvm_options) - - # Decode to check content options_str = [opt.decode() for opt in nvvm_options] assert "-arch=compute_80" in options_str assert "-g" in options_str From 355fcaaff0c97be2abccfa1e94630c651bf72ef7 Mon Sep 17 00:00:00 2001 From: Daniel Rodriguez Date: Mon, 20 Apr 2026 01:23:57 -0500 Subject: [PATCH 019/244] cuda.bindings latency benchmarks - part 3 (#1948) * Migrate to /benchmarks directory * Add Memory benchmarks * Add memory benchmarks * Move to top level of the repo * lint * lint * lint * Recover missing SPDX follow-ups from PR 1913 GitHub merged PR 1913 before the later local commits were pushed, so replay the recovered SPDX policy follow-ups and related license fixes here. Context: https://github.com/NVIDIA/cuda-python/pull/1913#issuecomment-4271701561 Made-with: Cursor * Fix benchmarks/cuda_bindings/pytest-legacy license identifiers * Move legacy benchmark Ruff suppressions with code move The naming-rule suppressions used to live under cuda_bindings/benchmarks, so move the needed legacy-path suppressions to the relocated benchmarks/cuda_bindings pytest-legacy path and drop the stale old-path entry. Made-with: Cursor * Fix to new benchmark paths --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- .coveragerc | 2 +- .github/workflows/test-wheel-linux.yml | 2 +- .spdx-ignore | 3 - .../cuda_bindings}/.gitignore | 0 benchmarks/cuda_bindings/AGENTS.md | 6 + .../cuda_bindings}/README.md | 2 +- .../benchmarks/bench_ctx_device.py | 20 ++-- .../cuda_bindings}/benchmarks/bench_event.py | 20 ++-- .../cuda_bindings}/benchmarks/bench_launch.py | 16 +-- .../cuda_bindings/benchmarks/bench_memory.py | 88 +++++++++++++++ .../benchmarks/bench_pointer_attributes.py | 4 +- .../cuda_bindings}/benchmarks/bench_stream.py | 16 +-- .../benchmarks/cpp/CMakeLists.txt | 1 + .../benchmarks/cpp/bench_ctx_device.cpp | 0 .../benchmarks/cpp/bench_event.cpp | 0 .../benchmarks/cpp/bench_launch.cpp | 33 ------ .../benchmarks/cpp/bench_memory.cpp | 106 ++++++++++++++++++ .../cpp/bench_pointer_attributes.cpp | 0 .../benchmarks/cpp/bench_stream.cpp | 0 .../benchmarks/cpp/bench_support.hpp | 0 .../cuda_bindings}/compare.py | 0 .../cuda_bindings}/pixi.lock | 48 ++++---- .../cuda_bindings}/pixi.toml | 2 +- .../cuda_bindings}/pytest-legacy/conftest.py | 2 +- .../cuda_bindings}/pytest-legacy/kernels.py | 2 +- .../cuda_bindings}/pytest-legacy/test_cupy.py | 2 +- .../pytest-legacy/test_launch_latency.py | 2 +- .../pytest-legacy/test_numba.py | 2 +- .../pytest-legacy/test_pointer_attributes.py | 2 +- .../cuda_bindings}/run_cpp.py | 0 .../cuda_bindings}/run_pyperf.py | 0 .../cuda_bindings}/runner/__init__.py | 0 .../cuda_bindings}/runner/cpp.py | 0 .../cuda_bindings}/runner/main.py | 2 +- .../cuda_bindings}/runner/runtime.py | 0 .../cuda_bindings}/tests/test_runner.py | 4 +- .../cuda_python_test_helpers/nvvm_bitcode.py | 2 +- ruff.toml | 7 +- toolshed/build_static_bitcode_input.py | 2 +- toolshed/check_spdx.py | 75 ++++++++++--- toolshed/dump_cutile_b64.py | 2 +- 41 files changed, 343 insertions(+), 132 deletions(-) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/.gitignore (100%) create mode 100644 benchmarks/cuda_bindings/AGENTS.md rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/README.md (97%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/bench_ctx_device.py (75%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/bench_event.py (76%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/bench_launch.py (87%) create mode 100644 benchmarks/cuda_bindings/benchmarks/bench_memory.py rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/bench_pointer_attributes.py (86%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/bench_stream.py (73%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/cpp/CMakeLists.txt (98%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/cpp/bench_ctx_device.cpp (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/cpp/bench_event.cpp (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/cpp/bench_launch.cpp (84%) create mode 100644 benchmarks/cuda_bindings/benchmarks/cpp/bench_memory.cpp rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/cpp/bench_pointer_attributes.cpp (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/cpp/bench_stream.cpp (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/benchmarks/cpp/bench_support.hpp (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/compare.py (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/pixi.lock (98%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/pixi.toml (97%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/pytest-legacy/conftest.py (97%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/pytest-legacy/kernels.py (97%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/pytest-legacy/test_cupy.py (98%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/pytest-legacy/test_launch_latency.py (99%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/pytest-legacy/test_numba.py (95%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/pytest-legacy/test_pointer_attributes.py (98%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/run_cpp.py (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/run_pyperf.py (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/runner/__init__.py (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/runner/cpp.py (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/runner/main.py (98%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/runner/runtime.py (100%) rename {cuda_bindings/benchmarks => benchmarks/cuda_bindings}/tests/test_runner.py (97%) diff --git a/.coveragerc b/.coveragerc index 36f0f7879af..1e1776fd566 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 [paths] source = diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 796040333b5..708238e20fd 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -274,7 +274,7 @@ jobs: if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0' }} run: | pip install pyperf - pushd cuda_bindings/benchmarks + pushd benchmarks/cuda_bindings python run_pyperf.py --fast --min-time 1 popd diff --git a/.spdx-ignore b/.spdx-ignore index 8c1d155c47c..7263b5414f7 100644 --- a/.spdx-ignore +++ b/.spdx-ignore @@ -8,9 +8,6 @@ LICENSE requirements*.txt cuda_bindings/examples/* -# Will be moved in (see https://github.com/NVIDIA/cuda-python/pull/1913#issuecomment-4252968149) -cuda_bindings/benchmarks/* - # Vendored cuda_core/cuda/core/_include/dlpack.h diff --git a/cuda_bindings/benchmarks/.gitignore b/benchmarks/cuda_bindings/.gitignore similarity index 100% rename from cuda_bindings/benchmarks/.gitignore rename to benchmarks/cuda_bindings/.gitignore diff --git a/benchmarks/cuda_bindings/AGENTS.md b/benchmarks/cuda_bindings/AGENTS.md new file mode 100644 index 00000000000..b9096a737f8 --- /dev/null +++ b/benchmarks/cuda_bindings/AGENTS.md @@ -0,0 +1,6 @@ +# cuda.bindings benchmarks + +Read the README.md in this directory for more details about the benchmarks. + +When generating code verify that that the code is correct based on the source for cuda-bindings +that can be found in ../../cuda_bindings diff --git a/cuda_bindings/benchmarks/README.md b/benchmarks/cuda_bindings/README.md similarity index 97% rename from cuda_bindings/benchmarks/README.md rename to benchmarks/cuda_bindings/README.md index 75e16db0317..f8d5ccf0436 100644 --- a/cuda_bindings/benchmarks/README.md +++ b/benchmarks/cuda_bindings/README.md @@ -37,7 +37,7 @@ See: https://pyperf.readthedocs.io/en/latest/system.html#system pixi run -e wheel -- python -m pyperf system show # Apply tuning (may require root) -sudo $(pixi run -e wheel -- which python) -m pyperf system tune +$(pixi run -e wheel -- which python) -m pyperf system tune ``` ### Running benchmarks diff --git a/cuda_bindings/benchmarks/benchmarks/bench_ctx_device.py b/benchmarks/cuda_bindings/benchmarks/bench_ctx_device.py similarity index 75% rename from cuda_bindings/benchmarks/benchmarks/bench_ctx_device.py rename to benchmarks/cuda_bindings/benchmarks/bench_ctx_device.py index 1c82cd4046c..2e2cd11d93b 100644 --- a/cuda_bindings/benchmarks/benchmarks/bench_ctx_device.py +++ b/benchmarks/cuda_bindings/benchmarks/bench_ctx_device.py @@ -15,48 +15,48 @@ def bench_ctx_get_current(loops: int) -> float: - _cuCtxGetCurrent = cuda.cuCtxGetCurrent + _fn = cuda.cuCtxGetCurrent t0 = time.perf_counter() for _ in range(loops): - _cuCtxGetCurrent() + _fn() return time.perf_counter() - t0 def bench_ctx_set_current(loops: int) -> float: - _cuCtxSetCurrent = cuda.cuCtxSetCurrent + _fn = cuda.cuCtxSetCurrent _ctx = CTX t0 = time.perf_counter() for _ in range(loops): - _cuCtxSetCurrent(_ctx) + _fn(_ctx) return time.perf_counter() - t0 def bench_ctx_get_device(loops: int) -> float: - _cuCtxGetDevice = cuda.cuCtxGetDevice + _fn = cuda.cuCtxGetDevice t0 = time.perf_counter() for _ in range(loops): - _cuCtxGetDevice() + _fn() return time.perf_counter() - t0 def bench_device_get(loops: int) -> float: - _cuDeviceGet = cuda.cuDeviceGet + _fn = cuda.cuDeviceGet t0 = time.perf_counter() for _ in range(loops): - _cuDeviceGet(0) + _fn(0) return time.perf_counter() - t0 def bench_device_get_attribute(loops: int) -> float: - _cuDeviceGetAttribute = cuda.cuDeviceGetAttribute + _fn = cuda.cuDeviceGetAttribute _attr = ATTRIBUTE _dev = DEVICE t0 = time.perf_counter() for _ in range(loops): - _cuDeviceGetAttribute(_attr, _dev) + _fn(_attr, _dev) return time.perf_counter() - t0 diff --git a/cuda_bindings/benchmarks/benchmarks/bench_event.py b/benchmarks/cuda_bindings/benchmarks/bench_event.py similarity index 76% rename from cuda_bindings/benchmarks/benchmarks/bench_event.py rename to benchmarks/cuda_bindings/benchmarks/bench_event.py index e8e319115de..041adc25538 100644 --- a/cuda_bindings/benchmarks/benchmarks/bench_event.py +++ b/benchmarks/cuda_bindings/benchmarks/bench_event.py @@ -20,43 +20,43 @@ def bench_event_create_destroy(loops: int) -> float: - _cuEventCreate = cuda.cuEventCreate - _cuEventDestroy = cuda.cuEventDestroy + _create = cuda.cuEventCreate + _destroy = cuda.cuEventDestroy _flags = EVENT_FLAGS t0 = time.perf_counter() for _ in range(loops): - _, e = _cuEventCreate(_flags) - _cuEventDestroy(e) + _, e = _create(_flags) + _destroy(e) return time.perf_counter() - t0 def bench_event_record(loops: int) -> float: - _cuEventRecord = cuda.cuEventRecord + _fn = cuda.cuEventRecord _event = EVENT _stream = STREAM t0 = time.perf_counter() for _ in range(loops): - _cuEventRecord(_event, _stream) + _fn(_event, _stream) return time.perf_counter() - t0 def bench_event_query(loops: int) -> float: - _cuEventQuery = cuda.cuEventQuery + _fn = cuda.cuEventQuery _event = EVENT t0 = time.perf_counter() for _ in range(loops): - _cuEventQuery(_event) + _fn(_event) return time.perf_counter() - t0 def bench_event_synchronize(loops: int) -> float: - _cuEventSynchronize = cuda.cuEventSynchronize + _fn = cuda.cuEventSynchronize _event = EVENT t0 = time.perf_counter() for _ in range(loops): - _cuEventSynchronize(_event) + _fn(_event) return time.perf_counter() - t0 diff --git a/cuda_bindings/benchmarks/benchmarks/bench_launch.py b/benchmarks/cuda_bindings/benchmarks/bench_launch.py similarity index 87% rename from cuda_bindings/benchmarks/benchmarks/bench_launch.py rename to benchmarks/cuda_bindings/benchmarks/bench_launch.py index 931194fbd32..abf3f946ccc 100644 --- a/cuda_bindings/benchmarks/benchmarks/bench_launch.py +++ b/benchmarks/cuda_bindings/benchmarks/bench_launch.py @@ -82,19 +82,19 @@ def _ensure_launch_state() -> None: def bench_launch_empty_kernel(loops: int) -> float: _ensure_launch_state() - _cuLaunchKernel = cuda.cuLaunchKernel + _fn = cuda.cuLaunchKernel _kernel = EMPTY_KERNEL _stream = STREAM t0 = time.perf_counter() for _ in range(loops): - _cuLaunchKernel(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, 0, 0) + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, 0, 0) return time.perf_counter() - t0 def bench_launch_small_kernel(loops: int) -> float: _ensure_launch_state() - _cuLaunchKernel = cuda.cuLaunchKernel + _fn = cuda.cuLaunchKernel _kernel = SMALL_KERNEL _stream = STREAM _args = (FLOAT_PTR,) @@ -102,13 +102,13 @@ def bench_launch_small_kernel(loops: int) -> float: t0 = time.perf_counter() for _ in range(loops): - _cuLaunchKernel(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) return time.perf_counter() - t0 def bench_launch_16_args(loops: int) -> float: _ensure_launch_state() - _cuLaunchKernel = cuda.cuLaunchKernel + _fn = cuda.cuLaunchKernel _kernel = KERNEL_16_ARGS _stream = STREAM _args = INT_PTRS @@ -116,18 +116,18 @@ def bench_launch_16_args(loops: int) -> float: t0 = time.perf_counter() for _ in range(loops): - _cuLaunchKernel(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) return time.perf_counter() - t0 def bench_launch_16_args_pre_packed(loops: int) -> float: _ensure_launch_state() - _cuLaunchKernel = cuda.cuLaunchKernel + _fn = cuda.cuLaunchKernel _kernel = KERNEL_16_ARGS _stream = STREAM _packed = PACKED_16 t0 = time.perf_counter() for _ in range(loops): - _cuLaunchKernel(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, _packed, 0) + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, _packed, 0) return time.perf_counter() - t0 diff --git a/benchmarks/cuda_bindings/benchmarks/bench_memory.py b/benchmarks/cuda_bindings/benchmarks/bench_memory.py new file mode 100644 index 00000000000..875c0604066 --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/bench_memory.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import time + +import numpy as np +from runner.runtime import alloc_persistent, ensure_context + +from cuda.bindings import driver as cuda + +ensure_context() + +# Allocation size for alloc/free benchmarks +ALLOC_SIZE = 1024 + +# Small transfer size (8 bytes) to measure call overhead, not bandwidth +COPY_SIZE = 8 + +# Pre-allocate device memory and host buffers for memcpy benchmarks +DST_DPTR = alloc_persistent(COPY_SIZE) +SRC_DPTR = alloc_persistent(COPY_SIZE) +HOST_SRC = np.zeros(COPY_SIZE, dtype=np.uint8) +HOST_DST = np.zeros(COPY_SIZE, dtype=np.uint8) + +# Stream for async operations +_err, STREAM = cuda.cuStreamCreate(cuda.CUstream_flags.CU_STREAM_NON_BLOCKING.value) + + +def bench_mem_alloc_free(loops: int) -> float: + _alloc = cuda.cuMemAlloc + _free = cuda.cuMemFree + _size = ALLOC_SIZE + + t0 = time.perf_counter() + for _ in range(loops): + _, ptr = _alloc(_size) + _free(ptr) + return time.perf_counter() - t0 + + +def bench_mem_alloc_async_free_async(loops: int) -> float: + _alloc = cuda.cuMemAllocAsync + _free = cuda.cuMemFreeAsync + _size = ALLOC_SIZE + _stream = STREAM + + t0 = time.perf_counter() + for _ in range(loops): + _, ptr = _alloc(_size, _stream) + _free(ptr, _stream) + return time.perf_counter() - t0 + + +def bench_memcpy_htod(loops: int) -> float: + _fn = cuda.cuMemcpyHtoD + _dst = DST_DPTR + _src = HOST_SRC + _size = COPY_SIZE + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_dst, _src, _size) + return time.perf_counter() - t0 + + +def bench_memcpy_dtoh(loops: int) -> float: + _fn = cuda.cuMemcpyDtoH + _dst = HOST_DST + _src = SRC_DPTR + _size = COPY_SIZE + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_dst, _src, _size) + return time.perf_counter() - t0 + + +def bench_memcpy_dtod(loops: int) -> float: + _fn = cuda.cuMemcpyDtoD + _dst = DST_DPTR + _src = SRC_DPTR + _size = COPY_SIZE + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_dst, _src, _size) + return time.perf_counter() - t0 diff --git a/cuda_bindings/benchmarks/benchmarks/bench_pointer_attributes.py b/benchmarks/cuda_bindings/benchmarks/bench_pointer_attributes.py similarity index 86% rename from cuda_bindings/benchmarks/benchmarks/bench_pointer_attributes.py rename to benchmarks/cuda_bindings/benchmarks/bench_pointer_attributes.py index a02b82c399c..191da263ee5 100644 --- a/cuda_bindings/benchmarks/benchmarks/bench_pointer_attributes.py +++ b/benchmarks/cuda_bindings/benchmarks/bench_pointer_attributes.py @@ -15,11 +15,11 @@ def bench_pointer_get_attribute(loops: int) -> float: # Local references to avoid global lookups in the hot loop - _cuPointerGetAttribute = cuda.cuPointerGetAttribute + _fn = cuda.cuPointerGetAttribute _attr = ATTRIBUTE _ptr = PTR t0 = time.perf_counter() for _ in range(loops): - _cuPointerGetAttribute(_attr, _ptr) + _fn(_attr, _ptr) return time.perf_counter() - t0 diff --git a/cuda_bindings/benchmarks/benchmarks/bench_stream.py b/benchmarks/cuda_bindings/benchmarks/bench_stream.py similarity index 73% rename from cuda_bindings/benchmarks/benchmarks/bench_stream.py rename to benchmarks/cuda_bindings/benchmarks/bench_stream.py index d816099ed56..3aab9288fc6 100644 --- a/cuda_bindings/benchmarks/benchmarks/bench_stream.py +++ b/benchmarks/cuda_bindings/benchmarks/bench_stream.py @@ -14,32 +14,32 @@ def bench_stream_create_destroy(loops: int) -> float: - _cuStreamCreate = cuda.cuStreamCreate - _cuStreamDestroy = cuda.cuStreamDestroy + _create = cuda.cuStreamCreate + _destroy = cuda.cuStreamDestroy _flags = cuda.CUstream_flags.CU_STREAM_NON_BLOCKING.value t0 = time.perf_counter() for _ in range(loops): - _, s = _cuStreamCreate(_flags) - _cuStreamDestroy(s) + _, s = _create(_flags) + _destroy(s) return time.perf_counter() - t0 def bench_stream_query(loops: int) -> float: - _cuStreamQuery = cuda.cuStreamQuery + _fn = cuda.cuStreamQuery _stream = STREAM t0 = time.perf_counter() for _ in range(loops): - _cuStreamQuery(_stream) + _fn(_stream) return time.perf_counter() - t0 def bench_stream_synchronize(loops: int) -> float: - _cuStreamSynchronize = cuda.cuStreamSynchronize + _fn = cuda.cuStreamSynchronize _stream = STREAM t0 = time.perf_counter() for _ in range(loops): - _cuStreamSynchronize(_stream) + _fn(_stream) return time.perf_counter() - t0 diff --git a/cuda_bindings/benchmarks/benchmarks/cpp/CMakeLists.txt b/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt similarity index 98% rename from cuda_bindings/benchmarks/benchmarks/cpp/CMakeLists.txt rename to benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt index b4285834aa6..83326911af5 100644 --- a/cuda_bindings/benchmarks/benchmarks/cpp/CMakeLists.txt +++ b/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt @@ -82,6 +82,7 @@ add_driver_benchmark(bench_pointer_attributes) add_driver_benchmark(bench_ctx_device) add_driver_benchmark(bench_stream) add_driver_benchmark(bench_event) +add_driver_benchmark(bench_memory) # NVRTC benchmarks (require nvrtc for kernel compilation) if(NVRTC_INCLUDE_DIR AND NVRTC_LIBRARY) diff --git a/cuda_bindings/benchmarks/benchmarks/cpp/bench_ctx_device.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_ctx_device.cpp similarity index 100% rename from cuda_bindings/benchmarks/benchmarks/cpp/bench_ctx_device.cpp rename to benchmarks/cuda_bindings/benchmarks/cpp/bench_ctx_device.cpp diff --git a/cuda_bindings/benchmarks/benchmarks/cpp/bench_event.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_event.cpp similarity index 100% rename from cuda_bindings/benchmarks/benchmarks/cpp/bench_event.cpp rename to benchmarks/cuda_bindings/benchmarks/cpp/bench_event.cpp diff --git a/cuda_bindings/benchmarks/benchmarks/cpp/bench_launch.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp similarity index 84% rename from cuda_bindings/benchmarks/benchmarks/cpp/bench_launch.cpp rename to benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp index fb65da6d74c..a2494269639 100644 --- a/cuda_bindings/benchmarks/benchmarks/cpp/bench_launch.cpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp @@ -168,39 +168,6 @@ int main(int argc, char** argv) { }); } - // --- launch_small_kernel --- - { - void* params[] = {&float_ptr}; - suite.run("launch.launch_small_kernel", [&]() { - check_cu( - cuLaunchKernel(small_kernel, 1, 1, 1, 1, 1, 1, 0, stream, params, nullptr), - "cuLaunchKernel failed" - ); - }); - } - - // --- launch_16_args --- - { - suite.run("launch.launch_16_args", [&]() { - check_cu( - cuLaunchKernel(kernel_16_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_16, nullptr), - "cuLaunchKernel failed" - ); - }); - } - - // --- launch_16_args_pre_packed (same as above for C++ — no packing overhead) --- - // In C++ the params are always pre-packed, so this is identical to launch_16_args. - // We include it for naming parity with the Python benchmark. - { - suite.run("launch.launch_16_args_pre_packed", [&]() { - check_cu( - cuLaunchKernel(kernel_16_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_16, nullptr), - "cuLaunchKernel failed" - ); - }); - } - // Cleanup for (int i = 0; i < 16; ++i) { check_cu(cuMemFree(int_ptrs[i]), "cuMemFree failed"); diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_memory.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_memory.cpp new file mode 100644 index 00000000000..4e71b73fb5e --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_memory.cpp @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "bench_support.hpp" + +#include +#include +#include +#include + + +static void check_cu(CUresult status, const char* message) { + if (status != CUDA_SUCCESS) { + const char* error_name = nullptr; + cuGetErrorName(status, &error_name); + std::cerr << message << ": " << (error_name ? error_name : "unknown") << '\n'; + std::exit(1); + } +} + + +static constexpr size_t ALLOC_SIZE = 1024; +static constexpr size_t COPY_SIZE = 8; + + +int main(int argc, char** argv) { + bench::Options options = bench::parse_args(argc, argv); + + // Setup + check_cu(cuInit(0), "cuInit failed"); + + CUdevice device; + check_cu(cuDeviceGet(&device, 0), "cuDeviceGet failed"); + + CUcontext ctx; + CUctxCreateParams ctxParams = {}; + check_cu(cuCtxCreate(&ctx, &ctxParams, 0, device), "cuCtxCreate failed"); + + CUstream stream; + check_cu(cuStreamCreate(&stream, CU_STREAM_NON_BLOCKING), "cuStreamCreate failed"); + + // Pre-allocate device memory for memcpy benchmarks + CUdeviceptr dst_dptr, src_dptr; + check_cu(cuMemAlloc(&dst_dptr, COPY_SIZE), "cuMemAlloc failed"); + check_cu(cuMemAlloc(&src_dptr, COPY_SIZE), "cuMemAlloc failed"); + + // Host buffers for memcpy + uint8_t host_src[COPY_SIZE] = {}; + uint8_t host_dst[COPY_SIZE] = {}; + + bench::BenchmarkSuite suite(options); + + // --- mem_alloc_free --- + { + CUdeviceptr ptr; + suite.run("memory.mem_alloc_free", [&]() { + check_cu(cuMemAlloc(&ptr, ALLOC_SIZE), "cuMemAlloc failed"); + check_cu(cuMemFree(ptr), "cuMemFree failed"); + }); + } + + // --- mem_alloc_async_free_async --- + { + CUdeviceptr ptr; + suite.run("memory.mem_alloc_async_free_async", [&]() { + check_cu(cuMemAllocAsync(&ptr, ALLOC_SIZE, stream), "cuMemAllocAsync failed"); + check_cu(cuMemFreeAsync(ptr, stream), "cuMemFreeAsync failed"); + }); + } + + check_cu(cuStreamSynchronize(stream), "cuStreamSynchronize failed"); + + // --- memcpy_htod --- + { + suite.run("memory.memcpy_htod", [&]() { + check_cu(cuMemcpyHtoD(dst_dptr, host_src, COPY_SIZE), "cuMemcpyHtoD failed"); + }); + } + + // --- memcpy_dtoh --- + { + suite.run("memory.memcpy_dtoh", [&]() { + check_cu(cuMemcpyDtoH(host_dst, src_dptr, COPY_SIZE), "cuMemcpyDtoH failed"); + }); + } + + // --- memcpy_dtod --- + { + suite.run("memory.memcpy_dtod", [&]() { + check_cu(cuMemcpyDtoD(dst_dptr, src_dptr, COPY_SIZE), "cuMemcpyDtoD failed"); + }); + } + + // Cleanup + check_cu(cuMemFree(dst_dptr), "cuMemFree failed"); + check_cu(cuMemFree(src_dptr), "cuMemFree failed"); + check_cu(cuStreamDestroy(stream), "cuStreamDestroy failed"); + check_cu(cuCtxDestroy(ctx), "cuCtxDestroy failed"); + + suite.write(); + + return 0; +} diff --git a/cuda_bindings/benchmarks/benchmarks/cpp/bench_pointer_attributes.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_pointer_attributes.cpp similarity index 100% rename from cuda_bindings/benchmarks/benchmarks/cpp/bench_pointer_attributes.cpp rename to benchmarks/cuda_bindings/benchmarks/cpp/bench_pointer_attributes.cpp diff --git a/cuda_bindings/benchmarks/benchmarks/cpp/bench_stream.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_stream.cpp similarity index 100% rename from cuda_bindings/benchmarks/benchmarks/cpp/bench_stream.cpp rename to benchmarks/cuda_bindings/benchmarks/cpp/bench_stream.cpp diff --git a/cuda_bindings/benchmarks/benchmarks/cpp/bench_support.hpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp similarity index 100% rename from cuda_bindings/benchmarks/benchmarks/cpp/bench_support.hpp rename to benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp diff --git a/cuda_bindings/benchmarks/compare.py b/benchmarks/cuda_bindings/compare.py similarity index 100% rename from cuda_bindings/benchmarks/compare.py rename to benchmarks/cuda_bindings/compare.py diff --git a/cuda_bindings/benchmarks/pixi.lock b/benchmarks/cuda_bindings/pixi.lock similarity index 98% rename from cuda_bindings/benchmarks/pixi.lock rename to benchmarks/cuda_bindings/pixi.lock index c610db2f45e..c571d4756cf 100644 --- a/cuda_bindings/benchmarks/pixi.lock +++ b/benchmarks/cuda_bindings/pixi.lock @@ -38,8 +38,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart-static_linux-64-13.2.51-h376f20c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cudart_linux-64-13.2.51-h376f20c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-driver-dev_linux-64-13.2.51-h376f20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.51-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.51-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.78-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-13.2.51-h69a702a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.2.51-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-13.2.51-h4bc722e_0.conda @@ -66,7 +66,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.77-h3ff7636_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.0.44-h85c024f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda @@ -130,7 +130,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - - conda: .. + - conda: ../../cuda_bindings - conda: ../../cuda_pathfinder wheel: channels: @@ -406,7 +406,7 @@ packages: license_family: GPL size: 31705 timestamp: 1771378159534 -- conda: .. +- conda: ../../cuda_bindings name: cuda-bindings version: 13.2.0 build: hb0f4dca_0 @@ -419,11 +419,11 @@ packages: - cuda-pathfinder - libnvjitlink - cuda-nvrtc - - cuda-nvrtc >=13.2.51,<14.0a0 + - cuda-nvrtc >=13.2.78,<14.0a0 - cuda-nvvm - libnvfatbin - libcufile - - libcufile >=1.17.0.44,<2.0a0 + - libcufile >=1.17.1.22,<2.0a0 - libgcc >=15 - libgcc >=15 - libstdcxx >=15 @@ -643,17 +643,17 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 35339417 timestamp: 1768272955912 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.51-hecca717_0.conda - sha256: 9de235d328b7124f715805715e9918eb7f8aa5b9c56a2afa62b84f84f98077a5 - md5: 0413baaa73be1a39d5d8e442184acc78 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-13.2.78-hecca717_0.conda + sha256: 73fbc9d15c062c3ea60891e8183002f6b055fa6638402d17581677af0aaa20d8 + md5: 66623d882c42506fa3f1780b90841400 depends: - __glibc >=2.17,<3.0.a0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 35736655 - timestamp: 1773100338749 + size: 35670504 + timestamp: 1776109867257 - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.1.115-hecca717_0.conda sha256: 2c929c592ca1909e3944edec62b77403d256156a4010bfa17fb0b948d33e54d3 md5: 1096fce4abad7dd975ce6d9953fceb6a @@ -668,20 +668,20 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 35845 timestamp: 1768273073971 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.51-hecca717_0.conda - sha256: be60eb4e84ff4846b27b323eca402b075f52caf6c138ebb06268fbaa26ef1879 - md5: 83535200a9e77165d5291b4ac82ebf6a +- conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvrtc-dev-13.2.78-hecca717_0.conda + sha256: 12505f1bbc222acf2a63da5c84e4176d2f9c18b458e2bde28939fdf326b6d292 + md5: cc313f0ea18ebc6e713a8980611431f5 depends: - __glibc >=2.17,<3.0.a0 - - cuda-nvrtc 13.2.51 hecca717_0 + - cuda-nvrtc 13.2.78 hecca717_0 - cuda-version >=13.2,<13.3.0a0 - libgcc >=14 - libstdcxx >=14 constrains: - - cuda-nvrtc-static >=13.2.51 + - cuda-nvrtc-static >=13.2.78 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 36305 - timestamp: 1773100458841 + size: 36312 + timestamp: 1776109983818 - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-13.2.51-h69a702a_0.conda sha256: d0111ba8fa12b96d38989d2016ecec0c11410c0e566d839ed54f3925591efb0b md5: 03cd3639b8e13623c7b91b1cb0136402 @@ -1018,9 +1018,9 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 990938 timestamp: 1768273732081 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.0.44-h85c024f_0.conda - sha256: dc2b0c43aeacbaa686061353807e718236d8c5b346f624e76fed98b066898e19 - md5: 6d8ed8335d144ec7303b8d3587b2205c +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcufile-1.17.1.22-h85c024f_0.conda + sha256: a24ad0ca488aa3e237049cd5b5c6d7fe3d2d4330682ed329203064e332ea1d74 + md5: 056a67706108efd1f9c24682ba8d3685 depends: - __glibc >=2.28,<3.0.a0 - cuda-version >=13.2,<13.3.0a0 @@ -1028,8 +1028,8 @@ packages: - libstdcxx >=14 - rdma-core >=61.0 license: LicenseRef-NVIDIA-End-User-License-Agreement - size: 1085341 - timestamp: 1773100191342 + size: 1082447 + timestamp: 1776110053053 - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda sha256: c84e8dccb65ad5149c0121e4b54bdc47fa39303fd5f4979b8c44bb51b39a369b md5: 1707cdd636af2ff697b53186572c9f77 diff --git a/cuda_bindings/benchmarks/pixi.toml b/benchmarks/cuda_bindings/pixi.toml similarity index 97% rename from cuda_bindings/benchmarks/pixi.toml rename to benchmarks/cuda_bindings/pixi.toml index a448e8d3e46..dbbddcd9397 100644 --- a/cuda_bindings/benchmarks/pixi.toml +++ b/benchmarks/cuda_bindings/pixi.toml @@ -45,7 +45,7 @@ pre-commit = "*" cuda-bindings = "==13.1.0" [feature.bindings-source.dependencies] -cuda-bindings = { path = ".." } +cuda-bindings = { path = "../../cuda_bindings" } [environments] wheel = { features = ["cu13", "cu13-pinned", "bench", "cpp-bench", "dev", "bindings-wheel"] } diff --git a/cuda_bindings/benchmarks/pytest-legacy/conftest.py b/benchmarks/cuda_bindings/pytest-legacy/conftest.py similarity index 97% rename from cuda_bindings/benchmarks/pytest-legacy/conftest.py rename to benchmarks/cuda_bindings/pytest-legacy/conftest.py index 0ea7b1d7723..5d0cc95e7a0 100644 --- a/cuda_bindings/benchmarks/pytest-legacy/conftest.py +++ b/benchmarks/cuda_bindings/pytest-legacy/conftest.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 import numpy as np import pytest diff --git a/cuda_bindings/benchmarks/pytest-legacy/kernels.py b/benchmarks/cuda_bindings/pytest-legacy/kernels.py similarity index 97% rename from cuda_bindings/benchmarks/pytest-legacy/kernels.py rename to benchmarks/cuda_bindings/pytest-legacy/kernels.py index 36646fba003..7e741110a3e 100644 --- a/cuda_bindings/benchmarks/pytest-legacy/kernels.py +++ b/benchmarks/cuda_bindings/pytest-legacy/kernels.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 kernel_string = """\ #define ITEM_PARAM(x, T) T x diff --git a/cuda_bindings/benchmarks/pytest-legacy/test_cupy.py b/benchmarks/cuda_bindings/pytest-legacy/test_cupy.py similarity index 98% rename from cuda_bindings/benchmarks/pytest-legacy/test_cupy.py rename to benchmarks/cuda_bindings/pytest-legacy/test_cupy.py index 76dd6e6a45f..3eea752ce09 100644 --- a/cuda_bindings/benchmarks/pytest-legacy/test_cupy.py +++ b/benchmarks/cuda_bindings/pytest-legacy/test_cupy.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 import ctypes diff --git a/cuda_bindings/benchmarks/pytest-legacy/test_launch_latency.py b/benchmarks/cuda_bindings/pytest-legacy/test_launch_latency.py similarity index 99% rename from cuda_bindings/benchmarks/pytest-legacy/test_launch_latency.py rename to benchmarks/cuda_bindings/pytest-legacy/test_launch_latency.py index dd994081a0a..ad421de382a 100755 --- a/cuda_bindings/benchmarks/pytest-legacy/test_launch_latency.py +++ b/benchmarks/cuda_bindings/pytest-legacy/test_launch_latency.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 import ctypes diff --git a/cuda_bindings/benchmarks/pytest-legacy/test_numba.py b/benchmarks/cuda_bindings/pytest-legacy/test_numba.py similarity index 95% rename from cuda_bindings/benchmarks/pytest-legacy/test_numba.py rename to benchmarks/cuda_bindings/pytest-legacy/test_numba.py index dfe084c6b1c..d9ae0cdfeed 100644 --- a/cuda_bindings/benchmarks/pytest-legacy/test_numba.py +++ b/benchmarks/cuda_bindings/pytest-legacy/test_numba.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 import numpy as np import pytest diff --git a/cuda_bindings/benchmarks/pytest-legacy/test_pointer_attributes.py b/benchmarks/cuda_bindings/pytest-legacy/test_pointer_attributes.py similarity index 98% rename from cuda_bindings/benchmarks/pytest-legacy/test_pointer_attributes.py rename to benchmarks/cuda_bindings/pytest-legacy/test_pointer_attributes.py index fae72ffd79a..6df32ec5118 100644 --- a/cuda_bindings/benchmarks/pytest-legacy/test_pointer_attributes.py +++ b/benchmarks/cuda_bindings/pytest-legacy/test_pointer_attributes.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 import random diff --git a/cuda_bindings/benchmarks/run_cpp.py b/benchmarks/cuda_bindings/run_cpp.py similarity index 100% rename from cuda_bindings/benchmarks/run_cpp.py rename to benchmarks/cuda_bindings/run_cpp.py diff --git a/cuda_bindings/benchmarks/run_pyperf.py b/benchmarks/cuda_bindings/run_pyperf.py similarity index 100% rename from cuda_bindings/benchmarks/run_pyperf.py rename to benchmarks/cuda_bindings/run_pyperf.py diff --git a/cuda_bindings/benchmarks/runner/__init__.py b/benchmarks/cuda_bindings/runner/__init__.py similarity index 100% rename from cuda_bindings/benchmarks/runner/__init__.py rename to benchmarks/cuda_bindings/runner/__init__.py diff --git a/cuda_bindings/benchmarks/runner/cpp.py b/benchmarks/cuda_bindings/runner/cpp.py similarity index 100% rename from cuda_bindings/benchmarks/runner/cpp.py rename to benchmarks/cuda_bindings/runner/cpp.py diff --git a/cuda_bindings/benchmarks/runner/main.py b/benchmarks/cuda_bindings/runner/main.py similarity index 98% rename from cuda_bindings/benchmarks/runner/main.py rename to benchmarks/cuda_bindings/runner/main.py index 4089aa55597..b0f6e76f417 100644 --- a/cuda_bindings/benchmarks/runner/main.py +++ b/benchmarks/cuda_bindings/runner/main.py @@ -53,7 +53,7 @@ def _discover_module_functions(module_path: Path) -> list[str]: return [ node.name for node in tree.body - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("bench_") + if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name.startswith("bench_") ] diff --git a/cuda_bindings/benchmarks/runner/runtime.py b/benchmarks/cuda_bindings/runner/runtime.py similarity index 100% rename from cuda_bindings/benchmarks/runner/runtime.py rename to benchmarks/cuda_bindings/runner/runtime.py diff --git a/cuda_bindings/benchmarks/tests/test_runner.py b/benchmarks/cuda_bindings/tests/test_runner.py similarity index 97% rename from cuda_bindings/benchmarks/tests/test_runner.py rename to benchmarks/cuda_bindings/tests/test_runner.py index 612094dac9c..f26baf8f5b5 100644 --- a/cuda_bindings/benchmarks/tests/test_runner.py +++ b/benchmarks/cuda_bindings/tests/test_runner.py @@ -10,8 +10,8 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[3] -RUNNER_MAIN_PATH = REPO_ROOT / "cuda_bindings/benchmarks/runner/main.py" -BENCH_LAUNCH_PATH = REPO_ROOT / "cuda_bindings/benchmarks/benchmarks/bench_launch.py" +RUNNER_MAIN_PATH = REPO_ROOT / "benchmarks/cuda_bindings/runner/main.py" +BENCH_LAUNCH_PATH = REPO_ROOT / "benchmarks/cuda_bindings/benchmarks/bench_launch.py" def load_module_from_path(module_name: str, module_path: Path): diff --git a/cuda_python_test_helpers/cuda_python_test_helpers/nvvm_bitcode.py b/cuda_python_test_helpers/cuda_python_test_helpers/nvvm_bitcode.py index ddb6eae107e..e6366ac95d8 100644 --- a/cuda_python_test_helpers/cuda_python_test_helpers/nvvm_bitcode.py +++ b/cuda_python_test_helpers/cuda_python_test_helpers/nvvm_bitcode.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 import binascii diff --git a/ruff.toml b/ruff.toml index 704e422c19e..210f852cd3e 100644 --- a/ruff.toml +++ b/ruff.toml @@ -124,13 +124,18 @@ inline-quotes = "double" # CUDA bindings mirror C API naming conventions (CamelCase types, camelCase functions) # Keep examples opted-in to enforce naming conventions in example-local identifiers. -"cuda_bindings/{benchmarks,cuda,docs,tests}/**" = [ +"cuda_bindings/{cuda,docs,tests}/**" = [ "N801", # invalid-class-name "N802", # invalid-function-name "N803", # invalid-argument-name "N806", # non-lowercase-variable-in-function "N816", # mixed-case-variable-in-global-scope ] +"benchmarks/cuda_bindings/pytest-legacy/**" = [ + "N801", # invalid-class-name + "N802", # invalid-function-name + "N806", # non-lowercase-variable-in-function +] "cuda_bindings/{build_hooks.py,setup.py}" = ["N801", "N802", "N803", "N806", "N816"] # scripts and build tooling — print is the expected output method diff --git a/toolshed/build_static_bitcode_input.py b/toolshed/build_static_bitcode_input.py index 273ce33244c..e2400100dde 100755 --- a/toolshed/build_static_bitcode_input.py +++ b/toolshed/build_static_bitcode_input.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 """ Helper to produce static bitcode input for test_nvvm.py. diff --git a/toolshed/check_spdx.py b/toolshed/check_spdx.py index 6be42282bf4..3d521425540 100644 --- a/toolshed/check_spdx.py +++ b/toolshed/check_spdx.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import datetime +import fnmatch import os import re import subprocess @@ -17,12 +18,28 @@ LICENSE_IDENTIFIER_REGEX = re.compile(re.escape(SPDX_LICENSE_IDENTIFIER_PREFIX) + rb"(?P[^\r\n]+)") -EXPECTED_LICENSE_IDENTIFIERS = ( - ("cuda_bindings/", "LicenseRef-NVIDIA-SOFTWARE-LICENSE"), - ("cuda_core/", "Apache-2.0"), - ("cuda_pathfinder/", "Apache-2.0"), - ("cuda_python/", "LicenseRef-NVIDIA-SOFTWARE-LICENSE"), -) +TOP_LEVEL_FILE_LICENSE_IDENTIFIER = "Apache-2.0" + +# Every top-level directory needs to have an entry here, so new paths +# can't slip in without a reviewed license decision. +TOP_LEVEL_DIRS_LICENSE_IDENTIFIERS = { + ".github": "Apache-2.0", + "benchmarks": "Apache-2.0", + "ci": "Apache-2.0", + "cuda_bindings": "LicenseRef-NVIDIA-SOFTWARE-LICENSE", + "cuda_core": "Apache-2.0", + "cuda_pathfinder": "Apache-2.0", + "cuda_python": "LicenseRef-NVIDIA-SOFTWARE-LICENSE", + "cuda_python_test_helpers": "Apache-2.0", + "scripts": "Apache-2.0", + "toolshed": "Apache-2.0", +} + +SPECIAL_CASE_LICENSE_IDENTIFIERS = { + # key: repo-relative path or glob, value: expected SPDX license identifier + "cuda_bindings/benchmarks/*": "Apache-2.0", + "cuda_bindings/benchmarks/pytest-legacy/*": "LicenseRef-NVIDIA-SOFTWARE-LICENSE", +} SPDX_IGNORE_FILENAME = ".spdx-ignore" @@ -63,12 +80,34 @@ def normalize_repo_path(filepath): return PureWindowsPath(filepath).as_posix() +def get_top_level_directory(normalized_path): + if "/" not in normalized_path: + return None + return normalized_path.split("/", 1)[0] + + def get_expected_license_identifier(filepath): normalized_path = normalize_repo_path(filepath) - for prefix, license_identifier in EXPECTED_LICENSE_IDENTIFIERS: - if normalized_path.startswith(prefix): - return license_identifier - return None + matching_special_cases = [ + (prefix, license_identifier) + for prefix, license_identifier in SPECIAL_CASE_LICENSE_IDENTIFIERS.items() + if fnmatch.fnmatchcase(normalized_path, prefix) + ] + if matching_special_cases: + return max(matching_special_cases, key=lambda item: len(item[0]))[1], None + + top_level_directory = get_top_level_directory(normalized_path) + if top_level_directory is None: + return TOP_LEVEL_FILE_LICENSE_IDENTIFIER, None + + if top_level_directory not in TOP_LEVEL_DIRS_LICENSE_IDENTIFIERS: + return ( + None, + f"MISSING TOP_LEVEL_DIRS_LICENSE_IDENTIFIERS entry for top-level directory " + f"{top_level_directory!r} required by {filepath!r}", + ) + + return TOP_LEVEL_DIRS_LICENSE_IDENTIFIERS[top_level_directory], None def validate_required_spdx_field(filepath, blob, expected_bytes): @@ -82,10 +121,11 @@ def extract_license_identifier(blob): match = LICENSE_IDENTIFIER_REGEX.search(blob) if match is None: return None - try: - return match.group("license_identifier").decode("ascii") - except UnicodeDecodeError: - return None + license_identifier = match.group("license_identifier").decode("ascii", errors="replace").strip() + for comment_suffix in ("-->", "*/"): + if license_identifier.endswith(comment_suffix): + license_identifier = license_identifier.removesuffix(comment_suffix).rstrip() + return license_identifier or None def validate_license_identifier(filepath, blob): @@ -94,9 +134,10 @@ def validate_license_identifier(filepath, blob): print(f"MISSING valid SPDX license identifier in {filepath!r}") return False - expected_license_identifier = get_expected_license_identifier(filepath) - if expected_license_identifier is None: - return True + expected_license_identifier, configuration_error = get_expected_license_identifier(filepath) + if configuration_error is not None: + print(configuration_error) + return False if license_identifier != expected_license_identifier: print( diff --git a/toolshed/dump_cutile_b64.py b/toolshed/dump_cutile_b64.py index 84013ea94b9..422bf95232b 100644 --- a/toolshed/dump_cutile_b64.py +++ b/toolshed/dump_cutile_b64.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-License-Identifier: Apache-2.0 """ Embeds a sample cuTile kernel, executes it with CUDA_TILE_DUMP_BYTECODE=., From 6ec277e78cd8ad9e804fffc6cb82f5cef2421da1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 20 Apr 2026 23:24:53 +0700 Subject: [PATCH 020/244] [no-ci] Use collaborator permission API for restricted-paths-guard trust check (#1930) * Use collaborator permission API instead of event payload author_association The webhook event payload's author_association field is unreliable for PRs originating from forks: even if the author is an org member or explicit collaborator with maintain/write permissions, fork PRs receive CONTRIBUTOR. This change queries the collaborator permission API directly to get the author's actual permission level (admin/maintain/write/triage/read/none), which is authoritative regardless of whether the PR comes from a fork or a branch in the main repo. Requires contents:write permission to access the collaborator API endpoint. Made-with: Cursor * TEMPORARY: Switch to pull_request trigger for testing This commit is for testing the collaborator permission check and must be reverted before merge: 1. Changes trigger from pull_request_target to pull_request so this branch's workflow definition runs instead of main's. 2. Adds a dummy change to cuda_bindings/pyproject.toml to trigger the restricted-paths detection. REVERT THIS COMMIT BEFORE MERGE. Made-with: Cursor * Revert "TEMPORARY: Switch to pull_request trigger for testing" This reverts commit b814323f08a32cc4e4427858488937ba8253b182. * Add explicit handling for non-trusted permission levels Address review feedback: explicitly handle the fallthrough case in the permission check to make it clear that triage, read, none, and API errors are not trusted signals. Made-with: Cursor * TEMPORARY: Switch to pull_request trigger for testing This commit is for testing the collaborator permission check and must be reverted before merge: 1. Changes trigger from pull_request_target to pull_request so this branch's workflow definition runs instead of main's. 2. Adds a dummy change to cuda_bindings/pyproject.toml to trigger the restricted-paths detection. REVERT THIS COMMIT BEFORE MERGE. Made-with: Cursor * Revert "TEMPORARY: Switch to pull_request trigger for testing" This reverts commit 8686adb4be9f702b393358a87a95d755bd0b217f. * Fail restricted-paths guard on collaborator API errors Treat 404 responses from the collaborator permission API as the expected non-collaborator case, but fail the workflow for any other API error so restricted-paths review labels are not added based on an unknown result. Made-with: Cursor * TEMPORARY: Switch to pull_request trigger for testing This commit is for testing the collaborator permission check and must be reverted before merge: 1. Changes trigger from pull_request_target to pull_request so this branch's workflow definition runs instead of main's. 2. Adds a dummy change to cuda_bindings/pyproject.toml to trigger the restricted-paths detection. REVERT THIS COMMIT BEFORE MERGE. Made-with: Cursor * Revert "TEMPORARY: Switch to pull_request trigger for testing" This reverts commit 2a019b6e14ffa214e87a541d7f9df1d66a525f62. --- .github/workflows/restricted-paths-guard.yml | 64 +++++++++++++++++--- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/.github/workflows/restricted-paths-guard.yml b/.github/workflows/restricted-paths-guard.yml index 7bfc4f29a67..8d7e6eccd4f 100644 --- a/.github/workflows/restricted-paths-guard.yml +++ b/.github/workflows/restricted-paths-guard.yml @@ -19,12 +19,13 @@ jobs: if: github.repository_owner == 'NVIDIA' runs-on: ubuntu-latest permissions: + contents: write # needed for collaborator permission check pull-requests: write steps: - name: Inspect PR author signals for restricted paths env: - # PR metadata inputs - AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association || 'NONE' }} + # PR metadata inputs (author_association from event payload is + # unreliable for fork PRs, so we query the collaborator API directly) PR_AUTHOR: ${{ github.event.pull_request.user.login }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_URL: ${{ github.event.pull_request.html_url }} @@ -38,6 +39,9 @@ jobs: run: | set -euo pipefail + COLLABORATOR_PERMISSION="not checked" + COLLABORATOR_PERMISSION_API_ERROR="" + if ! MATCHING_RESTRICTED_PATHS=$( gh api \ --paginate \ @@ -63,7 +67,7 @@ jobs: echo "" echo "- **Error**: Failed to inspect the PR file list." echo "- **Author**: $PR_AUTHOR" - echo "- **Author association**: $AUTHOR_ASSOCIATION" + echo "- **Collaborator permission**: $COLLABORATOR_PERMISSION" echo "" echo "Please update the PR at: $PR_URL" } >> "$GITHUB_STEP_SUMMARY" @@ -83,7 +87,7 @@ jobs: echo "" echo "- **Error**: Failed to inspect the current PR labels." echo "- **Author**: $PR_AUTHOR" - echo "- **Author association**: $AUTHOR_ASSOCIATION" + echo "- **Collaborator permission**: $COLLABORATOR_PERMISSION" echo "" echo "Please update the PR at: $PR_URL" } >> "$GITHUB_STEP_SUMMARY" @@ -102,16 +106,56 @@ jobs: echo '```' } + write_collaborator_permission_api_error() { + echo "- **Collaborator permission API error**:" + echo '```text' + printf '%s\n' "$COLLABORATOR_PERMISSION_API_ERROR" + echo '```' + } + HAS_TRUSTED_SIGNAL=false LABEL_ACTION="not needed (no restricted paths)" TRUSTED_SIGNALS="(none)" if [ "$TOUCHES_RESTRICTED_PATHS" = "true" ]; then - case "$AUTHOR_ASSOCIATION" in - COLLABORATOR|MEMBER|OWNER) + # Distinguish a legitimate 404 "not a collaborator" response from + # actual API failures. The former is an expected untrusted case; + # the latter fails the workflow so it can be rerun later. + if COLLABORATOR_PERMISSION_RESPONSE=$( + gh api "repos/$REPO/collaborators/$PR_AUTHOR/permission" \ + --jq '.permission' 2>&1 + ); then + COLLABORATOR_PERMISSION="$COLLABORATOR_PERMISSION_RESPONSE" + elif [[ "$COLLABORATOR_PERMISSION_RESPONSE" == *"(HTTP 404)"* ]]; then + COLLABORATOR_PERMISSION="none" + else + COLLABORATOR_PERMISSION="unknown" + COLLABORATOR_PERMISSION_API_ERROR="$COLLABORATOR_PERMISSION_RESPONSE" + echo "::error::Failed to inspect collaborator permission for $PR_AUTHOR." + { + echo "## Restricted Paths Guard Failed" + echo "" + echo "- **Error**: Failed to inspect collaborator permission." + echo "- **Author**: $PR_AUTHOR" + echo "- **Collaborator permission**: $COLLABORATOR_PERMISSION" + echo "" + write_matching_restricted_paths + echo "" + write_collaborator_permission_api_error + echo "" + echo "Please retry this workflow. If the failure persists, inspect the collaborator permission API error above." + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + case "$COLLABORATOR_PERMISSION" in + admin|maintain|write) HAS_TRUSTED_SIGNAL=true - LABEL_ACTION="not needed (author association is a trusted signal)" - TRUSTED_SIGNALS="author_association:$AUTHOR_ASSOCIATION" + LABEL_ACTION="not needed (collaborator permission is a trusted signal)" + TRUSTED_SIGNALS="collaborator_permission:$COLLABORATOR_PERMISSION" + ;; + *) + # triage, read, or none: not a trusted signal ;; esac fi @@ -136,7 +180,7 @@ jobs: echo "" echo "- **Error**: Failed to add the \`$REVIEW_LABEL\` label." echo "- **Author**: $PR_AUTHOR" - echo "- **Author association**: $AUTHOR_ASSOCIATION" + echo "- **Collaborator permission**: $COLLABORATOR_PERMISSION" echo "" write_matching_restricted_paths echo "" @@ -154,7 +198,7 @@ jobs: echo "## Restricted Paths Guard Completed" echo "" echo "- **Author**: $PR_AUTHOR" - echo "- **Author association**: $AUTHOR_ASSOCIATION" + echo "- **Collaborator permission**: $COLLABORATOR_PERMISSION" echo "- **Touches restricted paths**: $TOUCHES_RESTRICTED_PATHS" echo "- **Restricted paths**: \`cuda_bindings/\`, \`cuda_python/\`" echo "- **Trusted signals**: $TRUSTED_SIGNALS" From ab29f67e4474185bd71a825c38d0bfc7bad0ceac Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Mon, 20 Apr 2026 15:14:21 -0400 Subject: [PATCH 021/244] Replace nvrtc Tempita templates with pre-generated Cython files (#1900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [DRAFT] Replace nvrtc Tempita templates with pre-generated Cython files Replace .pyx.in / .pxd.in Tempita templates with plain .pyx / .pxd files generated by the refactored cython-gen (cybind leof/cybind\!369). Changes: - Remove 6 Tempita template files (.pyx.in / .pxd.in) for nvrtc - Add 7 pre-generated Cython files with zero Tempita - Move internal layer from _bindings/ to _internal/ following cybind convention - Split platform-specific code into nvrtc_linux.pyx / nvrtc_windows.pyx - Internal layer now uses cybind-style loading (RTLD_DEFAULT + fallback, FunctionNotFoundError, _init_nvrtc/_check_or_init_nvrtc naming) This is a companion PR to cybind leof/cybind\!369. Build system changes (build_hooks.py, pyproject.toml) to remove pyclibrary are not yet included — this PR shows the generated output for review. Part of https://gitlab-master.nvidia.com/leof/cybind/-/issues/173 Co-Authored-By: Claude Opus 4.6 (1M context) * Regenerate nvrtc files with template-based generation Update generated artifacts to use cybind-style templates: - Fix excessive blank lines from Tempita stripping - Internal layer files now generated from template files under cybind/assets/templates/ via string.Template.substitute() - Proper license headers from templates Co-Authored-By: Claude Opus 4.6 (1M context) * Update cynvrtc.pyx to use _internal cimport (cybind convention) - cynvrtc.pyx now uses "from ._internal cimport nvrtc as _nvrtc" instead of the legacy "cimport _bindings.cynvrtc as cynvrtc" - Function wrappers reference _nvrtc._funcName() matching cybind style - cynvrtc.pxd gets proper template header Co-Authored-By: Claude Opus 4.6 (1M context) * Update build system: skip pyclibrary for nvrtc, fix cynvrtc source path - Remove nvrtc from _REQUIRED_HEADERS (no pyclibrary parsing needed; nvrtc .pyx/.pxd files are now pre-generated by cybind) - Update sources_list: cynvrtc.pyx moved from _bindings/ to bindings/ - _rename_architecture_specific_files already handles _internal/nvrtc_linux.pyx -> _internal/nvrtc.pyx Co-Authored-By: Claude Opus 4.6 (1M context) * Restore generator version in headers, remove duplicate cynvrtc source entry - Regenerate files with generator version in header comments - Remove explicit cynvrtc.pyx from sources_list (already covered by cuda_bindings_files glob) Co-Authored-By: Claude Opus 4.6 (1M context) * Fix pre-commit: strip trailing whitespace and ensure final newline Regenerated from latest cybind which now cleans up trailing whitespace and ensures files end with exactly one newline. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_bindings/build_hooks.py | 7 +- .../cuda/bindings/_bindings/cynvrtc.pyx.in | 789 ------------------ .../cynvrtc.pxd.in => _internal/nvrtc.pxd} | 87 +- .../cuda/bindings/_internal/nvrtc_linux.pyx | 599 +++++++++++++ .../cuda/bindings/_internal/nvrtc_windows.pyx | 511 ++++++++++++ .../bindings/{cynvrtc.pxd.in => cynvrtc.pxd} | 84 +- .../bindings/{cynvrtc.pyx.in => cynvrtc.pyx} | 136 +-- .../cuda/bindings/{nvrtc.pxd.in => nvrtc.pxd} | 5 +- .../cuda/bindings/{nvrtc.pyx.in => nvrtc.pyx} | 168 +--- 9 files changed, 1194 insertions(+), 1192 deletions(-) delete mode 100644 cuda_bindings/cuda/bindings/_bindings/cynvrtc.pyx.in rename cuda_bindings/cuda/bindings/{_bindings/cynvrtc.pxd.in => _internal/nvrtc.pxd} (66%) create mode 100644 cuda_bindings/cuda/bindings/_internal/nvrtc_linux.pyx create mode 100644 cuda_bindings/cuda/bindings/_internal/nvrtc_windows.pyx rename cuda_bindings/cuda/bindings/{cynvrtc.pxd.in => cynvrtc.pxd} (72%) rename cuda_bindings/cuda/bindings/{cynvrtc.pyx.in => cynvrtc.pyx} (50%) rename cuda_bindings/cuda/bindings/{nvrtc.pxd.in => nvrtc.pxd} (85%) rename cuda_bindings/cuda/bindings/{nvrtc.pyx.in => nvrtc.pyx} (90%) diff --git a/cuda_bindings/build_hooks.py b/cuda_bindings/build_hooks.py index ce4745f7a0e..094d8adfbf1 100644 --- a/cuda_bindings/build_hooks.py +++ b/cuda_bindings/build_hooks.py @@ -94,9 +94,7 @@ def _get_cuda_path() -> str: "driver_functions.h", "cuda_profiler_api.h", ], - "nvrtc": [ - "nvrtc.h", - ], + # nvrtc: headers no longer parsed at build time (pre-generated by cybind). # During compilation, Cython will reference C headers that are not # explicitly parsed above. These are the known dependencies: # @@ -423,7 +421,8 @@ def _cleanup_dst_files(): sources_list = [ # private (["cuda/bindings/_bindings/cydriver.pyx", "cuda/bindings/_bindings/loader.cpp"], None), - (["cuda/bindings/_bindings/cynvrtc.pyx"], None), + # cynvrtc.pyx is now in cuda/bindings/ (pre-generated by cybind), + # picked up by the cuda_bindings_files glob below. (["cuda/bindings/_bindings/cyruntime.pyx"], static_runtime_libraries), (["cuda/bindings/_bindings/cyruntime_ptds.pyx"], static_runtime_libraries), # utils diff --git a/cuda_bindings/cuda/bindings/_bindings/cynvrtc.pyx.in b/cuda_bindings/cuda/bindings/_bindings/cynvrtc.pyx.in deleted file mode 100644 index 2e1c7a67ccd..00000000000 --- a/cuda_bindings/cuda/bindings/_bindings/cynvrtc.pyx.in +++ /dev/null @@ -1,789 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. -{{if 'Windows' == platform.system()}} -import os -cimport cuda.bindings._lib.windll as windll -{{else}} -cimport cuda.bindings._lib.dlfcn as dlfcn -{{endif}} -from cuda.pathfinder import load_nvidia_dynamic_lib -from libc.stdint cimport intptr_t, uintptr_t -import threading - -cdef object __symbol_lock = threading.Lock() -cdef bint __cuPythonInit = False -{{if 'nvrtcGetErrorString' in found_functions}}cdef void *__nvrtcGetErrorString = NULL{{endif}} -{{if 'nvrtcVersion' in found_functions}}cdef void *__nvrtcVersion = NULL{{endif}} -{{if 'nvrtcGetNumSupportedArchs' in found_functions}}cdef void *__nvrtcGetNumSupportedArchs = NULL{{endif}} -{{if 'nvrtcGetSupportedArchs' in found_functions}}cdef void *__nvrtcGetSupportedArchs = NULL{{endif}} -{{if 'nvrtcCreateProgram' in found_functions}}cdef void *__nvrtcCreateProgram = NULL{{endif}} -{{if 'nvrtcDestroyProgram' in found_functions}}cdef void *__nvrtcDestroyProgram = NULL{{endif}} -{{if 'nvrtcCompileProgram' in found_functions}}cdef void *__nvrtcCompileProgram = NULL{{endif}} -{{if 'nvrtcGetPTXSize' in found_functions}}cdef void *__nvrtcGetPTXSize = NULL{{endif}} -{{if 'nvrtcGetPTX' in found_functions}}cdef void *__nvrtcGetPTX = NULL{{endif}} -{{if 'nvrtcGetCUBINSize' in found_functions}}cdef void *__nvrtcGetCUBINSize = NULL{{endif}} -{{if 'nvrtcGetCUBIN' in found_functions}}cdef void *__nvrtcGetCUBIN = NULL{{endif}} -{{if 'nvrtcGetLTOIRSize' in found_functions}}cdef void *__nvrtcGetLTOIRSize = NULL{{endif}} -{{if 'nvrtcGetLTOIR' in found_functions}}cdef void *__nvrtcGetLTOIR = NULL{{endif}} -{{if 'nvrtcGetOptiXIRSize' in found_functions}}cdef void *__nvrtcGetOptiXIRSize = NULL{{endif}} -{{if 'nvrtcGetOptiXIR' in found_functions}}cdef void *__nvrtcGetOptiXIR = NULL{{endif}} -{{if 'nvrtcGetProgramLogSize' in found_functions}}cdef void *__nvrtcGetProgramLogSize = NULL{{endif}} -{{if 'nvrtcGetProgramLog' in found_functions}}cdef void *__nvrtcGetProgramLog = NULL{{endif}} -{{if 'nvrtcAddNameExpression' in found_functions}}cdef void *__nvrtcAddNameExpression = NULL{{endif}} -{{if 'nvrtcGetLoweredName' in found_functions}}cdef void *__nvrtcGetLoweredName = NULL{{endif}} -{{if 'nvrtcGetPCHHeapSize' in found_functions}}cdef void *__nvrtcGetPCHHeapSize = NULL{{endif}} -{{if 'nvrtcSetPCHHeapSize' in found_functions}}cdef void *__nvrtcSetPCHHeapSize = NULL{{endif}} -{{if 'nvrtcGetPCHCreateStatus' in found_functions}}cdef void *__nvrtcGetPCHCreateStatus = NULL{{endif}} -{{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}}cdef void *__nvrtcGetPCHHeapSizeRequired = NULL{{endif}} -{{if 'nvrtcSetFlowCallback' in found_functions}}cdef void *__nvrtcSetFlowCallback = NULL{{endif}} -{{if 'nvrtcGetTileIRSize' in found_functions}}cdef void *__nvrtcGetTileIRSize = NULL{{endif}} -{{if 'nvrtcGetTileIR' in found_functions}}cdef void *__nvrtcGetTileIR = NULL{{endif}} - -cdef int _cuPythonInit() except -1 nogil: - global __cuPythonInit - - # Load library - with gil, __symbol_lock: - {{if 'Windows' == platform.system()}} - handle = load_nvidia_dynamic_lib("nvrtc")._handle_uint - - # Load function - {{if 'nvrtcGetErrorString' in found_functions}} - global __nvrtcGetErrorString - __nvrtcGetErrorString = windll.GetProcAddress(handle, 'nvrtcGetErrorString') - {{endif}} - {{if 'nvrtcVersion' in found_functions}} - global __nvrtcVersion - __nvrtcVersion = windll.GetProcAddress(handle, 'nvrtcVersion') - {{endif}} - {{if 'nvrtcGetNumSupportedArchs' in found_functions}} - global __nvrtcGetNumSupportedArchs - __nvrtcGetNumSupportedArchs = windll.GetProcAddress(handle, 'nvrtcGetNumSupportedArchs') - {{endif}} - {{if 'nvrtcGetSupportedArchs' in found_functions}} - global __nvrtcGetSupportedArchs - __nvrtcGetSupportedArchs = windll.GetProcAddress(handle, 'nvrtcGetSupportedArchs') - {{endif}} - {{if 'nvrtcCreateProgram' in found_functions}} - global __nvrtcCreateProgram - __nvrtcCreateProgram = windll.GetProcAddress(handle, 'nvrtcCreateProgram') - {{endif}} - {{if 'nvrtcDestroyProgram' in found_functions}} - global __nvrtcDestroyProgram - __nvrtcDestroyProgram = windll.GetProcAddress(handle, 'nvrtcDestroyProgram') - {{endif}} - {{if 'nvrtcCompileProgram' in found_functions}} - global __nvrtcCompileProgram - __nvrtcCompileProgram = windll.GetProcAddress(handle, 'nvrtcCompileProgram') - {{endif}} - {{if 'nvrtcGetPTXSize' in found_functions}} - global __nvrtcGetPTXSize - __nvrtcGetPTXSize = windll.GetProcAddress(handle, 'nvrtcGetPTXSize') - {{endif}} - {{if 'nvrtcGetPTX' in found_functions}} - global __nvrtcGetPTX - __nvrtcGetPTX = windll.GetProcAddress(handle, 'nvrtcGetPTX') - {{endif}} - {{if 'nvrtcGetCUBINSize' in found_functions}} - global __nvrtcGetCUBINSize - __nvrtcGetCUBINSize = windll.GetProcAddress(handle, 'nvrtcGetCUBINSize') - {{endif}} - {{if 'nvrtcGetCUBIN' in found_functions}} - global __nvrtcGetCUBIN - __nvrtcGetCUBIN = windll.GetProcAddress(handle, 'nvrtcGetCUBIN') - {{endif}} - {{if 'nvrtcGetLTOIRSize' in found_functions}} - global __nvrtcGetLTOIRSize - __nvrtcGetLTOIRSize = windll.GetProcAddress(handle, 'nvrtcGetLTOIRSize') - {{endif}} - {{if 'nvrtcGetLTOIR' in found_functions}} - global __nvrtcGetLTOIR - __nvrtcGetLTOIR = windll.GetProcAddress(handle, 'nvrtcGetLTOIR') - {{endif}} - {{if 'nvrtcGetOptiXIRSize' in found_functions}} - global __nvrtcGetOptiXIRSize - __nvrtcGetOptiXIRSize = windll.GetProcAddress(handle, 'nvrtcGetOptiXIRSize') - {{endif}} - {{if 'nvrtcGetOptiXIR' in found_functions}} - global __nvrtcGetOptiXIR - __nvrtcGetOptiXIR = windll.GetProcAddress(handle, 'nvrtcGetOptiXIR') - {{endif}} - {{if 'nvrtcGetProgramLogSize' in found_functions}} - global __nvrtcGetProgramLogSize - __nvrtcGetProgramLogSize = windll.GetProcAddress(handle, 'nvrtcGetProgramLogSize') - {{endif}} - {{if 'nvrtcGetProgramLog' in found_functions}} - global __nvrtcGetProgramLog - __nvrtcGetProgramLog = windll.GetProcAddress(handle, 'nvrtcGetProgramLog') - {{endif}} - {{if 'nvrtcAddNameExpression' in found_functions}} - global __nvrtcAddNameExpression - __nvrtcAddNameExpression = windll.GetProcAddress(handle, 'nvrtcAddNameExpression') - {{endif}} - {{if 'nvrtcGetLoweredName' in found_functions}} - global __nvrtcGetLoweredName - __nvrtcGetLoweredName = windll.GetProcAddress(handle, 'nvrtcGetLoweredName') - {{endif}} - {{if 'nvrtcGetPCHHeapSize' in found_functions}} - global __nvrtcGetPCHHeapSize - __nvrtcGetPCHHeapSize = windll.GetProcAddress(handle, 'nvrtcGetPCHHeapSize') - {{endif}} - {{if 'nvrtcSetPCHHeapSize' in found_functions}} - global __nvrtcSetPCHHeapSize - __nvrtcSetPCHHeapSize = windll.GetProcAddress(handle, 'nvrtcSetPCHHeapSize') - {{endif}} - {{if 'nvrtcGetPCHCreateStatus' in found_functions}} - global __nvrtcGetPCHCreateStatus - __nvrtcGetPCHCreateStatus = windll.GetProcAddress(handle, 'nvrtcGetPCHCreateStatus') - {{endif}} - {{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}} - global __nvrtcGetPCHHeapSizeRequired - __nvrtcGetPCHHeapSizeRequired = windll.GetProcAddress(handle, 'nvrtcGetPCHHeapSizeRequired') - {{endif}} - {{if 'nvrtcSetFlowCallback' in found_functions}} - global __nvrtcSetFlowCallback - __nvrtcSetFlowCallback = windll.GetProcAddress(handle, 'nvrtcSetFlowCallback') - {{endif}} - {{if 'nvrtcGetTileIRSize' in found_functions}} - global __nvrtcGetTileIRSize - __nvrtcGetTileIRSize = windll.GetProcAddress(handle, 'nvrtcGetTileIRSize') - {{endif}} - {{if 'nvrtcGetTileIR' in found_functions}} - global __nvrtcGetTileIR - __nvrtcGetTileIR = windll.GetProcAddress(handle, 'nvrtcGetTileIR') - {{endif}} - - {{else}} - handle = (load_nvidia_dynamic_lib("nvrtc")._handle_uint) - - # Load function - {{if 'nvrtcGetErrorString' in found_functions}} - global __nvrtcGetErrorString - __nvrtcGetErrorString = dlfcn.dlsym(handle, 'nvrtcGetErrorString') - {{endif}} - {{if 'nvrtcVersion' in found_functions}} - global __nvrtcVersion - __nvrtcVersion = dlfcn.dlsym(handle, 'nvrtcVersion') - {{endif}} - {{if 'nvrtcGetNumSupportedArchs' in found_functions}} - global __nvrtcGetNumSupportedArchs - __nvrtcGetNumSupportedArchs = dlfcn.dlsym(handle, 'nvrtcGetNumSupportedArchs') - {{endif}} - {{if 'nvrtcGetSupportedArchs' in found_functions}} - global __nvrtcGetSupportedArchs - __nvrtcGetSupportedArchs = dlfcn.dlsym(handle, 'nvrtcGetSupportedArchs') - {{endif}} - {{if 'nvrtcCreateProgram' in found_functions}} - global __nvrtcCreateProgram - __nvrtcCreateProgram = dlfcn.dlsym(handle, 'nvrtcCreateProgram') - {{endif}} - {{if 'nvrtcDestroyProgram' in found_functions}} - global __nvrtcDestroyProgram - __nvrtcDestroyProgram = dlfcn.dlsym(handle, 'nvrtcDestroyProgram') - {{endif}} - {{if 'nvrtcCompileProgram' in found_functions}} - global __nvrtcCompileProgram - __nvrtcCompileProgram = dlfcn.dlsym(handle, 'nvrtcCompileProgram') - {{endif}} - {{if 'nvrtcGetPTXSize' in found_functions}} - global __nvrtcGetPTXSize - __nvrtcGetPTXSize = dlfcn.dlsym(handle, 'nvrtcGetPTXSize') - {{endif}} - {{if 'nvrtcGetPTX' in found_functions}} - global __nvrtcGetPTX - __nvrtcGetPTX = dlfcn.dlsym(handle, 'nvrtcGetPTX') - {{endif}} - {{if 'nvrtcGetCUBINSize' in found_functions}} - global __nvrtcGetCUBINSize - __nvrtcGetCUBINSize = dlfcn.dlsym(handle, 'nvrtcGetCUBINSize') - {{endif}} - {{if 'nvrtcGetCUBIN' in found_functions}} - global __nvrtcGetCUBIN - __nvrtcGetCUBIN = dlfcn.dlsym(handle, 'nvrtcGetCUBIN') - {{endif}} - {{if 'nvrtcGetLTOIRSize' in found_functions}} - global __nvrtcGetLTOIRSize - __nvrtcGetLTOIRSize = dlfcn.dlsym(handle, 'nvrtcGetLTOIRSize') - {{endif}} - {{if 'nvrtcGetLTOIR' in found_functions}} - global __nvrtcGetLTOIR - __nvrtcGetLTOIR = dlfcn.dlsym(handle, 'nvrtcGetLTOIR') - {{endif}} - {{if 'nvrtcGetOptiXIRSize' in found_functions}} - global __nvrtcGetOptiXIRSize - __nvrtcGetOptiXIRSize = dlfcn.dlsym(handle, 'nvrtcGetOptiXIRSize') - {{endif}} - {{if 'nvrtcGetOptiXIR' in found_functions}} - global __nvrtcGetOptiXIR - __nvrtcGetOptiXIR = dlfcn.dlsym(handle, 'nvrtcGetOptiXIR') - {{endif}} - {{if 'nvrtcGetProgramLogSize' in found_functions}} - global __nvrtcGetProgramLogSize - __nvrtcGetProgramLogSize = dlfcn.dlsym(handle, 'nvrtcGetProgramLogSize') - {{endif}} - {{if 'nvrtcGetProgramLog' in found_functions}} - global __nvrtcGetProgramLog - __nvrtcGetProgramLog = dlfcn.dlsym(handle, 'nvrtcGetProgramLog') - {{endif}} - {{if 'nvrtcAddNameExpression' in found_functions}} - global __nvrtcAddNameExpression - __nvrtcAddNameExpression = dlfcn.dlsym(handle, 'nvrtcAddNameExpression') - {{endif}} - {{if 'nvrtcGetLoweredName' in found_functions}} - global __nvrtcGetLoweredName - __nvrtcGetLoweredName = dlfcn.dlsym(handle, 'nvrtcGetLoweredName') - {{endif}} - {{if 'nvrtcGetPCHHeapSize' in found_functions}} - global __nvrtcGetPCHHeapSize - __nvrtcGetPCHHeapSize = dlfcn.dlsym(handle, 'nvrtcGetPCHHeapSize') - {{endif}} - {{if 'nvrtcSetPCHHeapSize' in found_functions}} - global __nvrtcSetPCHHeapSize - __nvrtcSetPCHHeapSize = dlfcn.dlsym(handle, 'nvrtcSetPCHHeapSize') - {{endif}} - {{if 'nvrtcGetPCHCreateStatus' in found_functions}} - global __nvrtcGetPCHCreateStatus - __nvrtcGetPCHCreateStatus = dlfcn.dlsym(handle, 'nvrtcGetPCHCreateStatus') - {{endif}} - {{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}} - global __nvrtcGetPCHHeapSizeRequired - __nvrtcGetPCHHeapSizeRequired = dlfcn.dlsym(handle, 'nvrtcGetPCHHeapSizeRequired') - {{endif}} - {{if 'nvrtcSetFlowCallback' in found_functions}} - global __nvrtcSetFlowCallback - __nvrtcSetFlowCallback = dlfcn.dlsym(handle, 'nvrtcSetFlowCallback') - {{endif}} - {{if 'nvrtcGetTileIRSize' in found_functions}} - global __nvrtcGetTileIRSize - __nvrtcGetTileIRSize = dlfcn.dlsym(handle, 'nvrtcGetTileIRSize') - {{endif}} - {{if 'nvrtcGetTileIR' in found_functions}} - global __nvrtcGetTileIR - __nvrtcGetTileIR = dlfcn.dlsym(handle, 'nvrtcGetTileIR') - {{endif}} - - {{endif}} - __cuPythonInit = True - return 0 - -# Create a very small function to check whether we are init'ed, so the C -# compiler can inline it. -cdef inline int cuPythonInit() except -1 nogil: - if __cuPythonInit: - return 0 - return _cuPythonInit() - -{{if 'nvrtcGetErrorString' in found_functions}} - -cdef const char* _nvrtcGetErrorString(nvrtcResult result) except ?NULL nogil: - global __nvrtcGetErrorString - cuPythonInit() - if __nvrtcGetErrorString == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetErrorString" not found') - err = ( __nvrtcGetErrorString)(result) - return err -{{endif}} - -{{if 'nvrtcVersion' in found_functions}} - -cdef nvrtcResult _nvrtcVersion(int* major, int* minor) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcVersion - cuPythonInit() - if __nvrtcVersion == NULL: - with gil: - raise RuntimeError('Function "nvrtcVersion" not found') - err = ( __nvrtcVersion)(major, minor) - return err -{{endif}} - -{{if 'nvrtcGetNumSupportedArchs' in found_functions}} - -cdef nvrtcResult _nvrtcGetNumSupportedArchs(int* numArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetNumSupportedArchs - cuPythonInit() - if __nvrtcGetNumSupportedArchs == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetNumSupportedArchs" not found') - err = ( __nvrtcGetNumSupportedArchs)(numArchs) - return err -{{endif}} - -{{if 'nvrtcGetSupportedArchs' in found_functions}} - -cdef nvrtcResult _nvrtcGetSupportedArchs(int* supportedArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetSupportedArchs - cuPythonInit() - if __nvrtcGetSupportedArchs == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetSupportedArchs" not found') - err = ( __nvrtcGetSupportedArchs)(supportedArchs) - return err -{{endif}} - -{{if 'nvrtcCreateProgram' in found_functions}} - -cdef nvrtcResult _nvrtcCreateProgram(nvrtcProgram* prog, const char* src, const char* name, int numHeaders, const char** headers, const char** includeNames) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcCreateProgram - cuPythonInit() - if __nvrtcCreateProgram == NULL: - with gil: - raise RuntimeError('Function "nvrtcCreateProgram" not found') - err = ( __nvrtcCreateProgram)(prog, src, name, numHeaders, headers, includeNames) - return err -{{endif}} - -{{if 'nvrtcDestroyProgram' in found_functions}} - -cdef nvrtcResult _nvrtcDestroyProgram(nvrtcProgram* prog) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcDestroyProgram - cuPythonInit() - if __nvrtcDestroyProgram == NULL: - with gil: - raise RuntimeError('Function "nvrtcDestroyProgram" not found') - err = ( __nvrtcDestroyProgram)(prog) - return err -{{endif}} - -{{if 'nvrtcCompileProgram' in found_functions}} - -cdef nvrtcResult _nvrtcCompileProgram(nvrtcProgram prog, int numOptions, const char** options) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcCompileProgram - cuPythonInit() - if __nvrtcCompileProgram == NULL: - with gil: - raise RuntimeError('Function "nvrtcCompileProgram" not found') - err = ( __nvrtcCompileProgram)(prog, numOptions, options) - return err -{{endif}} - -{{if 'nvrtcGetPTXSize' in found_functions}} - -cdef nvrtcResult _nvrtcGetPTXSize(nvrtcProgram prog, size_t* ptxSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetPTXSize - cuPythonInit() - if __nvrtcGetPTXSize == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetPTXSize" not found') - err = ( __nvrtcGetPTXSize)(prog, ptxSizeRet) - return err -{{endif}} - -{{if 'nvrtcGetPTX' in found_functions}} - -cdef nvrtcResult _nvrtcGetPTX(nvrtcProgram prog, char* ptx) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetPTX - cuPythonInit() - if __nvrtcGetPTX == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetPTX" not found') - err = ( __nvrtcGetPTX)(prog, ptx) - return err -{{endif}} - -{{if 'nvrtcGetCUBINSize' in found_functions}} - -cdef nvrtcResult _nvrtcGetCUBINSize(nvrtcProgram prog, size_t* cubinSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetCUBINSize - cuPythonInit() - if __nvrtcGetCUBINSize == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetCUBINSize" not found') - err = ( __nvrtcGetCUBINSize)(prog, cubinSizeRet) - return err -{{endif}} - -{{if 'nvrtcGetCUBIN' in found_functions}} - -cdef nvrtcResult _nvrtcGetCUBIN(nvrtcProgram prog, char* cubin) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetCUBIN - cuPythonInit() - if __nvrtcGetCUBIN == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetCUBIN" not found') - err = ( __nvrtcGetCUBIN)(prog, cubin) - return err -{{endif}} - -{{if 'nvrtcGetLTOIRSize' in found_functions}} - -cdef nvrtcResult _nvrtcGetLTOIRSize(nvrtcProgram prog, size_t* LTOIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetLTOIRSize - cuPythonInit() - if __nvrtcGetLTOIRSize == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetLTOIRSize" not found') - err = ( __nvrtcGetLTOIRSize)(prog, LTOIRSizeRet) - return err -{{endif}} - -{{if 'nvrtcGetLTOIR' in found_functions}} - -cdef nvrtcResult _nvrtcGetLTOIR(nvrtcProgram prog, char* LTOIR) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetLTOIR - cuPythonInit() - if __nvrtcGetLTOIR == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetLTOIR" not found') - err = ( __nvrtcGetLTOIR)(prog, LTOIR) - return err -{{endif}} - -{{if 'nvrtcGetOptiXIRSize' in found_functions}} - -cdef nvrtcResult _nvrtcGetOptiXIRSize(nvrtcProgram prog, size_t* optixirSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetOptiXIRSize - cuPythonInit() - if __nvrtcGetOptiXIRSize == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetOptiXIRSize" not found') - err = ( __nvrtcGetOptiXIRSize)(prog, optixirSizeRet) - return err -{{endif}} - -{{if 'nvrtcGetOptiXIR' in found_functions}} - -cdef nvrtcResult _nvrtcGetOptiXIR(nvrtcProgram prog, char* optixir) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetOptiXIR - cuPythonInit() - if __nvrtcGetOptiXIR == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetOptiXIR" not found') - err = ( __nvrtcGetOptiXIR)(prog, optixir) - return err -{{endif}} - -{{if 'nvrtcGetProgramLogSize' in found_functions}} - -cdef nvrtcResult _nvrtcGetProgramLogSize(nvrtcProgram prog, size_t* logSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetProgramLogSize - cuPythonInit() - if __nvrtcGetProgramLogSize == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetProgramLogSize" not found') - err = ( __nvrtcGetProgramLogSize)(prog, logSizeRet) - return err -{{endif}} - -{{if 'nvrtcGetProgramLog' in found_functions}} - -cdef nvrtcResult _nvrtcGetProgramLog(nvrtcProgram prog, char* log) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetProgramLog - cuPythonInit() - if __nvrtcGetProgramLog == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetProgramLog" not found') - err = ( __nvrtcGetProgramLog)(prog, log) - return err -{{endif}} - -{{if 'nvrtcAddNameExpression' in found_functions}} - -cdef nvrtcResult _nvrtcAddNameExpression(nvrtcProgram prog, const char* name_expression) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcAddNameExpression - cuPythonInit() - if __nvrtcAddNameExpression == NULL: - with gil: - raise RuntimeError('Function "nvrtcAddNameExpression" not found') - err = ( __nvrtcAddNameExpression)(prog, name_expression) - return err -{{endif}} - -{{if 'nvrtcGetLoweredName' in found_functions}} - -cdef nvrtcResult _nvrtcGetLoweredName(nvrtcProgram prog, const char* name_expression, const char** lowered_name) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetLoweredName - cuPythonInit() - if __nvrtcGetLoweredName == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetLoweredName" not found') - err = ( __nvrtcGetLoweredName)(prog, name_expression, lowered_name) - return err -{{endif}} - -{{if 'nvrtcGetPCHHeapSize' in found_functions}} - -cdef nvrtcResult _nvrtcGetPCHHeapSize(size_t* ret) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetPCHHeapSize - cuPythonInit() - if __nvrtcGetPCHHeapSize == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetPCHHeapSize" not found') - err = ( __nvrtcGetPCHHeapSize)(ret) - return err -{{endif}} - -{{if 'nvrtcSetPCHHeapSize' in found_functions}} - -cdef nvrtcResult _nvrtcSetPCHHeapSize(size_t size) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcSetPCHHeapSize - cuPythonInit() - if __nvrtcSetPCHHeapSize == NULL: - with gil: - raise RuntimeError('Function "nvrtcSetPCHHeapSize" not found') - err = ( __nvrtcSetPCHHeapSize)(size) - return err -{{endif}} - -{{if 'nvrtcGetPCHCreateStatus' in found_functions}} - -cdef nvrtcResult _nvrtcGetPCHCreateStatus(nvrtcProgram prog) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetPCHCreateStatus - cuPythonInit() - if __nvrtcGetPCHCreateStatus == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetPCHCreateStatus" not found') - err = ( __nvrtcGetPCHCreateStatus)(prog) - return err -{{endif}} - -{{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}} - -cdef nvrtcResult _nvrtcGetPCHHeapSizeRequired(nvrtcProgram prog, size_t* size) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetPCHHeapSizeRequired - cuPythonInit() - if __nvrtcGetPCHHeapSizeRequired == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetPCHHeapSizeRequired" not found') - err = ( __nvrtcGetPCHHeapSizeRequired)(prog, size) - return err -{{endif}} - -{{if 'nvrtcSetFlowCallback' in found_functions}} - -cdef nvrtcResult _nvrtcSetFlowCallback(nvrtcProgram prog, void* callback, void* payload) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcSetFlowCallback - cuPythonInit() - if __nvrtcSetFlowCallback == NULL: - with gil: - raise RuntimeError('Function "nvrtcSetFlowCallback" not found') - err = ( __nvrtcSetFlowCallback)(prog, callback, payload) - return err -{{endif}} - -{{if 'nvrtcGetTileIRSize' in found_functions}} - -cdef nvrtcResult _nvrtcGetTileIRSize(nvrtcProgram prog, size_t* TileIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetTileIRSize - cuPythonInit() - if __nvrtcGetTileIRSize == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetTileIRSize" not found') - err = ( __nvrtcGetTileIRSize)(prog, TileIRSizeRet) - return err -{{endif}} - -{{if 'nvrtcGetTileIR' in found_functions}} - -cdef nvrtcResult _nvrtcGetTileIR(nvrtcProgram prog, char* TileIR) except ?NVRTC_ERROR_INVALID_INPUT nogil: - global __nvrtcGetTileIR - cuPythonInit() - if __nvrtcGetTileIR == NULL: - with gil: - raise RuntimeError('Function "nvrtcGetTileIR" not found') - err = ( __nvrtcGetTileIR)(prog, TileIR) - return err -{{endif}} - -cdef dict func_ptrs = None - -cpdef dict _inspect_function_pointers(): - global func_ptrs - if func_ptrs is not None: - return func_ptrs - - cuPythonInit() - cdef dict data = {} - - {{if 'nvrtcGetErrorString' in found_functions}} - global __nvrtcGetErrorString - data["__nvrtcGetErrorString"] = __nvrtcGetErrorString - {{else}} - data["__nvrtcGetErrorString"] = 0 - {{endif}} - - {{if 'nvrtcVersion' in found_functions}} - global __nvrtcVersion - data["__nvrtcVersion"] = __nvrtcVersion - {{else}} - data["__nvrtcVersion"] = 0 - {{endif}} - - {{if 'nvrtcGetNumSupportedArchs' in found_functions}} - global __nvrtcGetNumSupportedArchs - data["__nvrtcGetNumSupportedArchs"] = __nvrtcGetNumSupportedArchs - {{else}} - data["__nvrtcGetNumSupportedArchs"] = 0 - {{endif}} - - {{if 'nvrtcGetSupportedArchs' in found_functions}} - global __nvrtcGetSupportedArchs - data["__nvrtcGetSupportedArchs"] = __nvrtcGetSupportedArchs - {{else}} - data["__nvrtcGetSupportedArchs"] = 0 - {{endif}} - - {{if 'nvrtcCreateProgram' in found_functions}} - global __nvrtcCreateProgram - data["__nvrtcCreateProgram"] = __nvrtcCreateProgram - {{else}} - data["__nvrtcCreateProgram"] = 0 - {{endif}} - - {{if 'nvrtcDestroyProgram' in found_functions}} - global __nvrtcDestroyProgram - data["__nvrtcDestroyProgram"] = __nvrtcDestroyProgram - {{else}} - data["__nvrtcDestroyProgram"] = 0 - {{endif}} - - {{if 'nvrtcCompileProgram' in found_functions}} - global __nvrtcCompileProgram - data["__nvrtcCompileProgram"] = __nvrtcCompileProgram - {{else}} - data["__nvrtcCompileProgram"] = 0 - {{endif}} - - {{if 'nvrtcGetPTXSize' in found_functions}} - global __nvrtcGetPTXSize - data["__nvrtcGetPTXSize"] = __nvrtcGetPTXSize - {{else}} - data["__nvrtcGetPTXSize"] = 0 - {{endif}} - - {{if 'nvrtcGetPTX' in found_functions}} - global __nvrtcGetPTX - data["__nvrtcGetPTX"] = __nvrtcGetPTX - {{else}} - data["__nvrtcGetPTX"] = 0 - {{endif}} - - {{if 'nvrtcGetCUBINSize' in found_functions}} - global __nvrtcGetCUBINSize - data["__nvrtcGetCUBINSize"] = __nvrtcGetCUBINSize - {{else}} - data["__nvrtcGetCUBINSize"] = 0 - {{endif}} - - {{if 'nvrtcGetCUBIN' in found_functions}} - global __nvrtcGetCUBIN - data["__nvrtcGetCUBIN"] = __nvrtcGetCUBIN - {{else}} - data["__nvrtcGetCUBIN"] = 0 - {{endif}} - - {{if 'nvrtcGetLTOIRSize' in found_functions}} - global __nvrtcGetLTOIRSize - data["__nvrtcGetLTOIRSize"] = __nvrtcGetLTOIRSize - {{else}} - data["__nvrtcGetLTOIRSize"] = 0 - {{endif}} - - {{if 'nvrtcGetLTOIR' in found_functions}} - global __nvrtcGetLTOIR - data["__nvrtcGetLTOIR"] = __nvrtcGetLTOIR - {{else}} - data["__nvrtcGetLTOIR"] = 0 - {{endif}} - - {{if 'nvrtcGetOptiXIRSize' in found_functions}} - global __nvrtcGetOptiXIRSize - data["__nvrtcGetOptiXIRSize"] = __nvrtcGetOptiXIRSize - {{else}} - data["__nvrtcGetOptiXIRSize"] = 0 - {{endif}} - - {{if 'nvrtcGetOptiXIR' in found_functions}} - global __nvrtcGetOptiXIR - data["__nvrtcGetOptiXIR"] = __nvrtcGetOptiXIR - {{else}} - data["__nvrtcGetOptiXIR"] = 0 - {{endif}} - - {{if 'nvrtcGetProgramLogSize' in found_functions}} - global __nvrtcGetProgramLogSize - data["__nvrtcGetProgramLogSize"] = __nvrtcGetProgramLogSize - {{else}} - data["__nvrtcGetProgramLogSize"] = 0 - {{endif}} - - {{if 'nvrtcGetProgramLog' in found_functions}} - global __nvrtcGetProgramLog - data["__nvrtcGetProgramLog"] = __nvrtcGetProgramLog - {{else}} - data["__nvrtcGetProgramLog"] = 0 - {{endif}} - - {{if 'nvrtcAddNameExpression' in found_functions}} - global __nvrtcAddNameExpression - data["__nvrtcAddNameExpression"] = __nvrtcAddNameExpression - {{else}} - data["__nvrtcAddNameExpression"] = 0 - {{endif}} - - {{if 'nvrtcGetLoweredName' in found_functions}} - global __nvrtcGetLoweredName - data["__nvrtcGetLoweredName"] = __nvrtcGetLoweredName - {{else}} - data["__nvrtcGetLoweredName"] = 0 - {{endif}} - - {{if 'nvrtcGetPCHHeapSize' in found_functions}} - global __nvrtcGetPCHHeapSize - data["__nvrtcGetPCHHeapSize"] = __nvrtcGetPCHHeapSize - {{else}} - data["__nvrtcGetPCHHeapSize"] = 0 - {{endif}} - - {{if 'nvrtcSetPCHHeapSize' in found_functions}} - global __nvrtcSetPCHHeapSize - data["__nvrtcSetPCHHeapSize"] = __nvrtcSetPCHHeapSize - {{else}} - data["__nvrtcSetPCHHeapSize"] = 0 - {{endif}} - - {{if 'nvrtcGetPCHCreateStatus' in found_functions}} - global __nvrtcGetPCHCreateStatus - data["__nvrtcGetPCHCreateStatus"] = __nvrtcGetPCHCreateStatus - {{else}} - data["__nvrtcGetPCHCreateStatus"] = 0 - {{endif}} - - {{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}} - global __nvrtcGetPCHHeapSizeRequired - data["__nvrtcGetPCHHeapSizeRequired"] = __nvrtcGetPCHHeapSizeRequired - {{else}} - data["__nvrtcGetPCHHeapSizeRequired"] = 0 - {{endif}} - - {{if 'nvrtcSetFlowCallback' in found_functions}} - global __nvrtcSetFlowCallback - data["__nvrtcSetFlowCallback"] = __nvrtcSetFlowCallback - {{else}} - data["__nvrtcSetFlowCallback"] = 0 - {{endif}} - - {{if 'nvrtcGetTileIRSize' in found_functions}} - global __nvrtcGetTileIRSize - data["__nvrtcGetTileIRSize"] = __nvrtcGetTileIRSize - {{else}} - data["__nvrtcGetTileIRSize"] = 0 - {{endif}} - - {{if 'nvrtcGetTileIR' in found_functions}} - global __nvrtcGetTileIR - data["__nvrtcGetTileIR"] = __nvrtcGetTileIR - {{else}} - data["__nvrtcGetTileIR"] = 0 - {{endif}} - - func_ptrs = data - return data - -cpdef _inspect_function_pointer(str name): - global func_ptrs - if func_ptrs is None: - func_ptrs = _inspect_function_pointers() - return func_ptrs[name] diff --git a/cuda_bindings/cuda/bindings/_bindings/cynvrtc.pxd.in b/cuda_bindings/cuda/bindings/_internal/nvrtc.pxd similarity index 66% rename from cuda_bindings/cuda/bindings/_bindings/cynvrtc.pxd.in rename to cuda_bindings/cuda/bindings/_internal/nvrtc.pxd index 5a53b926d17..1964af1f16b 100644 --- a/cuda_bindings/cuda/bindings/_bindings/cynvrtc.pxd.in +++ b/cuda_bindings/cuda/bindings/_internal/nvrtc.pxd @@ -1,136 +1,63 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1364+ged01d643e. Do not modify it directly. -# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. -from cuda.bindings.cynvrtc cimport * +from ..cynvrtc cimport * -{{if 'nvrtcGetErrorString' in found_functions}} +############################################################################### +# Wrapper functions +############################################################################### cdef const char* _nvrtcGetErrorString(nvrtcResult result) except ?NULL nogil -{{endif}} - -{{if 'nvrtcVersion' in found_functions}} cdef nvrtcResult _nvrtcVersion(int* major, int* minor) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetNumSupportedArchs' in found_functions}} cdef nvrtcResult _nvrtcGetNumSupportedArchs(int* numArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetSupportedArchs' in found_functions}} cdef nvrtcResult _nvrtcGetSupportedArchs(int* supportedArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcCreateProgram' in found_functions}} cdef nvrtcResult _nvrtcCreateProgram(nvrtcProgram* prog, const char* src, const char* name, int numHeaders, const char** headers, const char** includeNames) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcDestroyProgram' in found_functions}} cdef nvrtcResult _nvrtcDestroyProgram(nvrtcProgram* prog) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcCompileProgram' in found_functions}} cdef nvrtcResult _nvrtcCompileProgram(nvrtcProgram prog, int numOptions, const char** options) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPTXSize' in found_functions}} cdef nvrtcResult _nvrtcGetPTXSize(nvrtcProgram prog, size_t* ptxSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPTX' in found_functions}} cdef nvrtcResult _nvrtcGetPTX(nvrtcProgram prog, char* ptx) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetCUBINSize' in found_functions}} cdef nvrtcResult _nvrtcGetCUBINSize(nvrtcProgram prog, size_t* cubinSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetCUBIN' in found_functions}} cdef nvrtcResult _nvrtcGetCUBIN(nvrtcProgram prog, char* cubin) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetLTOIRSize' in found_functions}} cdef nvrtcResult _nvrtcGetLTOIRSize(nvrtcProgram prog, size_t* LTOIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetLTOIR' in found_functions}} cdef nvrtcResult _nvrtcGetLTOIR(nvrtcProgram prog, char* LTOIR) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetOptiXIRSize' in found_functions}} cdef nvrtcResult _nvrtcGetOptiXIRSize(nvrtcProgram prog, size_t* optixirSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetOptiXIR' in found_functions}} cdef nvrtcResult _nvrtcGetOptiXIR(nvrtcProgram prog, char* optixir) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetProgramLogSize' in found_functions}} cdef nvrtcResult _nvrtcGetProgramLogSize(nvrtcProgram prog, size_t* logSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetProgramLog' in found_functions}} cdef nvrtcResult _nvrtcGetProgramLog(nvrtcProgram prog, char* log) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcAddNameExpression' in found_functions}} cdef nvrtcResult _nvrtcAddNameExpression(nvrtcProgram prog, const char* name_expression) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetLoweredName' in found_functions}} cdef nvrtcResult _nvrtcGetLoweredName(nvrtcProgram prog, const char* name_expression, const char** lowered_name) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPCHHeapSize' in found_functions}} cdef nvrtcResult _nvrtcGetPCHHeapSize(size_t* ret) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcSetPCHHeapSize' in found_functions}} cdef nvrtcResult _nvrtcSetPCHHeapSize(size_t size) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPCHCreateStatus' in found_functions}} cdef nvrtcResult _nvrtcGetPCHCreateStatus(nvrtcProgram prog) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}} cdef nvrtcResult _nvrtcGetPCHHeapSizeRequired(nvrtcProgram prog, size_t* size) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcSetFlowCallback' in found_functions}} cdef nvrtcResult _nvrtcSetFlowCallback(nvrtcProgram prog, void* callback, void* payload) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetTileIRSize' in found_functions}} cdef nvrtcResult _nvrtcGetTileIRSize(nvrtcProgram prog, size_t* TileIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetTileIR' in found_functions}} cdef nvrtcResult _nvrtcGetTileIR(nvrtcProgram prog, char* TileIR) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - diff --git a/cuda_bindings/cuda/bindings/_internal/nvrtc_linux.pyx b/cuda_bindings/cuda/bindings/_internal/nvrtc_linux.pyx new file mode 100644 index 00000000000..780042a8cdc --- /dev/null +++ b/cuda_bindings/cuda/bindings/_internal/nvrtc_linux.pyx @@ -0,0 +1,599 @@ +# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1364+ged01d643e. Do not modify it directly. + +from libc.stdint cimport intptr_t, uintptr_t + +import threading +from .utils import FunctionNotFoundError, NotSupportedError + +from cuda.pathfinder import load_nvidia_dynamic_lib + +############################################################################### +# Extern +############################################################################### + +# You must 'from .utils import NotSupportedError' before using this template + +cdef extern from "" nogil: + void* dlopen(const char*, int) + char* dlerror() + void* dlsym(void*, const char*) + int dlclose(void*) + + enum: + RTLD_LAZY + RTLD_NOW + RTLD_GLOBAL + RTLD_LOCAL + + const void* RTLD_DEFAULT 'RTLD_DEFAULT' + +cdef int get_cuda_version(): + cdef void* handle = NULL + cdef int err, driver_ver = 0 + + # Load driver to check version + handle = dlopen('libcuda.so.1', RTLD_NOW | RTLD_GLOBAL) + if handle == NULL: + err_msg = dlerror() + raise NotSupportedError(f'CUDA driver is not found ({err_msg.decode()})') + cuDriverGetVersion = dlsym(handle, "cuDriverGetVersion") + if cuDriverGetVersion == NULL: + raise RuntimeError('Did not find cuDriverGetVersion symbol in libcuda.so.1') + err = (cuDriverGetVersion)(&driver_ver) + if err != 0: + raise RuntimeError(f'cuDriverGetVersion returned error code {err}') + + return driver_ver + +############################################################################### +# Wrapper init +############################################################################### + +cdef object __symbol_lock = threading.Lock() +cdef bint __py_nvrtc_init = False + +cdef void* __nvrtcGetErrorString = NULL +cdef void* __nvrtcVersion = NULL +cdef void* __nvrtcGetNumSupportedArchs = NULL +cdef void* __nvrtcGetSupportedArchs = NULL +cdef void* __nvrtcCreateProgram = NULL +cdef void* __nvrtcDestroyProgram = NULL +cdef void* __nvrtcCompileProgram = NULL +cdef void* __nvrtcGetPTXSize = NULL +cdef void* __nvrtcGetPTX = NULL +cdef void* __nvrtcGetCUBINSize = NULL +cdef void* __nvrtcGetCUBIN = NULL +cdef void* __nvrtcGetLTOIRSize = NULL +cdef void* __nvrtcGetLTOIR = NULL +cdef void* __nvrtcGetOptiXIRSize = NULL +cdef void* __nvrtcGetOptiXIR = NULL +cdef void* __nvrtcGetProgramLogSize = NULL +cdef void* __nvrtcGetProgramLog = NULL +cdef void* __nvrtcAddNameExpression = NULL +cdef void* __nvrtcGetLoweredName = NULL +cdef void* __nvrtcGetPCHHeapSize = NULL +cdef void* __nvrtcSetPCHHeapSize = NULL +cdef void* __nvrtcGetPCHCreateStatus = NULL +cdef void* __nvrtcGetPCHHeapSizeRequired = NULL +cdef void* __nvrtcSetFlowCallback = NULL +cdef void* __nvrtcGetTileIRSize = NULL +cdef void* __nvrtcGetTileIR = NULL + +cdef void* load_library() except* with gil: + cdef uintptr_t handle = load_nvidia_dynamic_lib("nvrtc")._handle_uint + return handle + +cdef int _init_nvrtc() except -1 nogil: + global __py_nvrtc_init + + cdef void* handle = NULL + + with gil, __symbol_lock: + # Recheck the flag after obtaining the locks + if __py_nvrtc_init: + return 0 + + # Load function + global __nvrtcGetErrorString + __nvrtcGetErrorString = dlsym(RTLD_DEFAULT, 'nvrtcGetErrorString') + if __nvrtcGetErrorString == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetErrorString = dlsym(handle, 'nvrtcGetErrorString') + + global __nvrtcVersion + __nvrtcVersion = dlsym(RTLD_DEFAULT, 'nvrtcVersion') + if __nvrtcVersion == NULL: + if handle == NULL: + handle = load_library() + __nvrtcVersion = dlsym(handle, 'nvrtcVersion') + + global __nvrtcGetNumSupportedArchs + __nvrtcGetNumSupportedArchs = dlsym(RTLD_DEFAULT, 'nvrtcGetNumSupportedArchs') + if __nvrtcGetNumSupportedArchs == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetNumSupportedArchs = dlsym(handle, 'nvrtcGetNumSupportedArchs') + + global __nvrtcGetSupportedArchs + __nvrtcGetSupportedArchs = dlsym(RTLD_DEFAULT, 'nvrtcGetSupportedArchs') + if __nvrtcGetSupportedArchs == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetSupportedArchs = dlsym(handle, 'nvrtcGetSupportedArchs') + + global __nvrtcCreateProgram + __nvrtcCreateProgram = dlsym(RTLD_DEFAULT, 'nvrtcCreateProgram') + if __nvrtcCreateProgram == NULL: + if handle == NULL: + handle = load_library() + __nvrtcCreateProgram = dlsym(handle, 'nvrtcCreateProgram') + + global __nvrtcDestroyProgram + __nvrtcDestroyProgram = dlsym(RTLD_DEFAULT, 'nvrtcDestroyProgram') + if __nvrtcDestroyProgram == NULL: + if handle == NULL: + handle = load_library() + __nvrtcDestroyProgram = dlsym(handle, 'nvrtcDestroyProgram') + + global __nvrtcCompileProgram + __nvrtcCompileProgram = dlsym(RTLD_DEFAULT, 'nvrtcCompileProgram') + if __nvrtcCompileProgram == NULL: + if handle == NULL: + handle = load_library() + __nvrtcCompileProgram = dlsym(handle, 'nvrtcCompileProgram') + + global __nvrtcGetPTXSize + __nvrtcGetPTXSize = dlsym(RTLD_DEFAULT, 'nvrtcGetPTXSize') + if __nvrtcGetPTXSize == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetPTXSize = dlsym(handle, 'nvrtcGetPTXSize') + + global __nvrtcGetPTX + __nvrtcGetPTX = dlsym(RTLD_DEFAULT, 'nvrtcGetPTX') + if __nvrtcGetPTX == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetPTX = dlsym(handle, 'nvrtcGetPTX') + + global __nvrtcGetCUBINSize + __nvrtcGetCUBINSize = dlsym(RTLD_DEFAULT, 'nvrtcGetCUBINSize') + if __nvrtcGetCUBINSize == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetCUBINSize = dlsym(handle, 'nvrtcGetCUBINSize') + + global __nvrtcGetCUBIN + __nvrtcGetCUBIN = dlsym(RTLD_DEFAULT, 'nvrtcGetCUBIN') + if __nvrtcGetCUBIN == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetCUBIN = dlsym(handle, 'nvrtcGetCUBIN') + + global __nvrtcGetLTOIRSize + __nvrtcGetLTOIRSize = dlsym(RTLD_DEFAULT, 'nvrtcGetLTOIRSize') + if __nvrtcGetLTOIRSize == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetLTOIRSize = dlsym(handle, 'nvrtcGetLTOIRSize') + + global __nvrtcGetLTOIR + __nvrtcGetLTOIR = dlsym(RTLD_DEFAULT, 'nvrtcGetLTOIR') + if __nvrtcGetLTOIR == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetLTOIR = dlsym(handle, 'nvrtcGetLTOIR') + + global __nvrtcGetOptiXIRSize + __nvrtcGetOptiXIRSize = dlsym(RTLD_DEFAULT, 'nvrtcGetOptiXIRSize') + if __nvrtcGetOptiXIRSize == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetOptiXIRSize = dlsym(handle, 'nvrtcGetOptiXIRSize') + + global __nvrtcGetOptiXIR + __nvrtcGetOptiXIR = dlsym(RTLD_DEFAULT, 'nvrtcGetOptiXIR') + if __nvrtcGetOptiXIR == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetOptiXIR = dlsym(handle, 'nvrtcGetOptiXIR') + + global __nvrtcGetProgramLogSize + __nvrtcGetProgramLogSize = dlsym(RTLD_DEFAULT, 'nvrtcGetProgramLogSize') + if __nvrtcGetProgramLogSize == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetProgramLogSize = dlsym(handle, 'nvrtcGetProgramLogSize') + + global __nvrtcGetProgramLog + __nvrtcGetProgramLog = dlsym(RTLD_DEFAULT, 'nvrtcGetProgramLog') + if __nvrtcGetProgramLog == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetProgramLog = dlsym(handle, 'nvrtcGetProgramLog') + + global __nvrtcAddNameExpression + __nvrtcAddNameExpression = dlsym(RTLD_DEFAULT, 'nvrtcAddNameExpression') + if __nvrtcAddNameExpression == NULL: + if handle == NULL: + handle = load_library() + __nvrtcAddNameExpression = dlsym(handle, 'nvrtcAddNameExpression') + + global __nvrtcGetLoweredName + __nvrtcGetLoweredName = dlsym(RTLD_DEFAULT, 'nvrtcGetLoweredName') + if __nvrtcGetLoweredName == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetLoweredName = dlsym(handle, 'nvrtcGetLoweredName') + + global __nvrtcGetPCHHeapSize + __nvrtcGetPCHHeapSize = dlsym(RTLD_DEFAULT, 'nvrtcGetPCHHeapSize') + if __nvrtcGetPCHHeapSize == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetPCHHeapSize = dlsym(handle, 'nvrtcGetPCHHeapSize') + + global __nvrtcSetPCHHeapSize + __nvrtcSetPCHHeapSize = dlsym(RTLD_DEFAULT, 'nvrtcSetPCHHeapSize') + if __nvrtcSetPCHHeapSize == NULL: + if handle == NULL: + handle = load_library() + __nvrtcSetPCHHeapSize = dlsym(handle, 'nvrtcSetPCHHeapSize') + + global __nvrtcGetPCHCreateStatus + __nvrtcGetPCHCreateStatus = dlsym(RTLD_DEFAULT, 'nvrtcGetPCHCreateStatus') + if __nvrtcGetPCHCreateStatus == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetPCHCreateStatus = dlsym(handle, 'nvrtcGetPCHCreateStatus') + + global __nvrtcGetPCHHeapSizeRequired + __nvrtcGetPCHHeapSizeRequired = dlsym(RTLD_DEFAULT, 'nvrtcGetPCHHeapSizeRequired') + if __nvrtcGetPCHHeapSizeRequired == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetPCHHeapSizeRequired = dlsym(handle, 'nvrtcGetPCHHeapSizeRequired') + + global __nvrtcSetFlowCallback + __nvrtcSetFlowCallback = dlsym(RTLD_DEFAULT, 'nvrtcSetFlowCallback') + if __nvrtcSetFlowCallback == NULL: + if handle == NULL: + handle = load_library() + __nvrtcSetFlowCallback = dlsym(handle, 'nvrtcSetFlowCallback') + + global __nvrtcGetTileIRSize + __nvrtcGetTileIRSize = dlsym(RTLD_DEFAULT, 'nvrtcGetTileIRSize') + if __nvrtcGetTileIRSize == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetTileIRSize = dlsym(handle, 'nvrtcGetTileIRSize') + + global __nvrtcGetTileIR + __nvrtcGetTileIR = dlsym(RTLD_DEFAULT, 'nvrtcGetTileIR') + if __nvrtcGetTileIR == NULL: + if handle == NULL: + handle = load_library() + __nvrtcGetTileIR = dlsym(handle, 'nvrtcGetTileIR') + + __py_nvrtc_init = True + return 0 + +cdef inline int _check_or_init_nvrtc() except -1 nogil: + if __py_nvrtc_init: + return 0 + + return _init_nvrtc() + +cdef dict func_ptrs = None + +cpdef dict _inspect_function_pointers(): + global func_ptrs + if func_ptrs is not None: + return func_ptrs + + _check_or_init_nvrtc() + cdef dict data = {} + + global __nvrtcGetErrorString + data["__nvrtcGetErrorString"] = __nvrtcGetErrorString + + global __nvrtcVersion + data["__nvrtcVersion"] = __nvrtcVersion + + global __nvrtcGetNumSupportedArchs + data["__nvrtcGetNumSupportedArchs"] = __nvrtcGetNumSupportedArchs + + global __nvrtcGetSupportedArchs + data["__nvrtcGetSupportedArchs"] = __nvrtcGetSupportedArchs + + global __nvrtcCreateProgram + data["__nvrtcCreateProgram"] = __nvrtcCreateProgram + + global __nvrtcDestroyProgram + data["__nvrtcDestroyProgram"] = __nvrtcDestroyProgram + + global __nvrtcCompileProgram + data["__nvrtcCompileProgram"] = __nvrtcCompileProgram + + global __nvrtcGetPTXSize + data["__nvrtcGetPTXSize"] = __nvrtcGetPTXSize + + global __nvrtcGetPTX + data["__nvrtcGetPTX"] = __nvrtcGetPTX + + global __nvrtcGetCUBINSize + data["__nvrtcGetCUBINSize"] = __nvrtcGetCUBINSize + + global __nvrtcGetCUBIN + data["__nvrtcGetCUBIN"] = __nvrtcGetCUBIN + + global __nvrtcGetLTOIRSize + data["__nvrtcGetLTOIRSize"] = __nvrtcGetLTOIRSize + + global __nvrtcGetLTOIR + data["__nvrtcGetLTOIR"] = __nvrtcGetLTOIR + + global __nvrtcGetOptiXIRSize + data["__nvrtcGetOptiXIRSize"] = __nvrtcGetOptiXIRSize + + global __nvrtcGetOptiXIR + data["__nvrtcGetOptiXIR"] = __nvrtcGetOptiXIR + + global __nvrtcGetProgramLogSize + data["__nvrtcGetProgramLogSize"] = __nvrtcGetProgramLogSize + + global __nvrtcGetProgramLog + data["__nvrtcGetProgramLog"] = __nvrtcGetProgramLog + + global __nvrtcAddNameExpression + data["__nvrtcAddNameExpression"] = __nvrtcAddNameExpression + + global __nvrtcGetLoweredName + data["__nvrtcGetLoweredName"] = __nvrtcGetLoweredName + + global __nvrtcGetPCHHeapSize + data["__nvrtcGetPCHHeapSize"] = __nvrtcGetPCHHeapSize + + global __nvrtcSetPCHHeapSize + data["__nvrtcSetPCHHeapSize"] = __nvrtcSetPCHHeapSize + + global __nvrtcGetPCHCreateStatus + data["__nvrtcGetPCHCreateStatus"] = __nvrtcGetPCHCreateStatus + + global __nvrtcGetPCHHeapSizeRequired + data["__nvrtcGetPCHHeapSizeRequired"] = __nvrtcGetPCHHeapSizeRequired + + global __nvrtcSetFlowCallback + data["__nvrtcSetFlowCallback"] = __nvrtcSetFlowCallback + + global __nvrtcGetTileIRSize + data["__nvrtcGetTileIRSize"] = __nvrtcGetTileIRSize + + global __nvrtcGetTileIR + data["__nvrtcGetTileIR"] = __nvrtcGetTileIR + + func_ptrs = data + return data + +cpdef _inspect_function_pointer(str name): + global func_ptrs + if func_ptrs is None: + func_ptrs = _inspect_function_pointers() + return func_ptrs[name] + +############################################################################### +# Wrapper functions +############################################################################### + +cdef const char* _nvrtcGetErrorString(nvrtcResult result) except ?NULL nogil: + global __nvrtcGetErrorString + _check_or_init_nvrtc() + if __nvrtcGetErrorString == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetErrorString is not found") + return (__nvrtcGetErrorString)(result) + +cdef nvrtcResult _nvrtcVersion(int* major, int* minor) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcVersion + _check_or_init_nvrtc() + if __nvrtcVersion == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcVersion is not found") + return (__nvrtcVersion)(major, minor) + +cdef nvrtcResult _nvrtcGetNumSupportedArchs(int* numArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetNumSupportedArchs + _check_or_init_nvrtc() + if __nvrtcGetNumSupportedArchs == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetNumSupportedArchs is not found") + return (__nvrtcGetNumSupportedArchs)(numArchs) + +cdef nvrtcResult _nvrtcGetSupportedArchs(int* supportedArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetSupportedArchs + _check_or_init_nvrtc() + if __nvrtcGetSupportedArchs == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetSupportedArchs is not found") + return (__nvrtcGetSupportedArchs)(supportedArchs) + +cdef nvrtcResult _nvrtcCreateProgram(nvrtcProgram* prog, const char* src, const char* name, int numHeaders, const char** headers, const char** includeNames) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcCreateProgram + _check_or_init_nvrtc() + if __nvrtcCreateProgram == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcCreateProgram is not found") + return (__nvrtcCreateProgram)(prog, src, name, numHeaders, headers, includeNames) + +cdef nvrtcResult _nvrtcDestroyProgram(nvrtcProgram* prog) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcDestroyProgram + _check_or_init_nvrtc() + if __nvrtcDestroyProgram == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcDestroyProgram is not found") + return (__nvrtcDestroyProgram)(prog) + +cdef nvrtcResult _nvrtcCompileProgram(nvrtcProgram prog, int numOptions, const char** options) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcCompileProgram + _check_or_init_nvrtc() + if __nvrtcCompileProgram == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcCompileProgram is not found") + return (__nvrtcCompileProgram)(prog, numOptions, options) + +cdef nvrtcResult _nvrtcGetPTXSize(nvrtcProgram prog, size_t* ptxSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPTXSize + _check_or_init_nvrtc() + if __nvrtcGetPTXSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPTXSize is not found") + return (__nvrtcGetPTXSize)(prog, ptxSizeRet) + +cdef nvrtcResult _nvrtcGetPTX(nvrtcProgram prog, char* ptx) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPTX + _check_or_init_nvrtc() + if __nvrtcGetPTX == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPTX is not found") + return (__nvrtcGetPTX)(prog, ptx) + +cdef nvrtcResult _nvrtcGetCUBINSize(nvrtcProgram prog, size_t* cubinSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetCUBINSize + _check_or_init_nvrtc() + if __nvrtcGetCUBINSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetCUBINSize is not found") + return (__nvrtcGetCUBINSize)(prog, cubinSizeRet) + +cdef nvrtcResult _nvrtcGetCUBIN(nvrtcProgram prog, char* cubin) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetCUBIN + _check_or_init_nvrtc() + if __nvrtcGetCUBIN == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetCUBIN is not found") + return (__nvrtcGetCUBIN)(prog, cubin) + +cdef nvrtcResult _nvrtcGetLTOIRSize(nvrtcProgram prog, size_t* LTOIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetLTOIRSize + _check_or_init_nvrtc() + if __nvrtcGetLTOIRSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetLTOIRSize is not found") + return (__nvrtcGetLTOIRSize)(prog, LTOIRSizeRet) + +cdef nvrtcResult _nvrtcGetLTOIR(nvrtcProgram prog, char* LTOIR) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetLTOIR + _check_or_init_nvrtc() + if __nvrtcGetLTOIR == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetLTOIR is not found") + return (__nvrtcGetLTOIR)(prog, LTOIR) + +cdef nvrtcResult _nvrtcGetOptiXIRSize(nvrtcProgram prog, size_t* optixirSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetOptiXIRSize + _check_or_init_nvrtc() + if __nvrtcGetOptiXIRSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetOptiXIRSize is not found") + return (__nvrtcGetOptiXIRSize)(prog, optixirSizeRet) + +cdef nvrtcResult _nvrtcGetOptiXIR(nvrtcProgram prog, char* optixir) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetOptiXIR + _check_or_init_nvrtc() + if __nvrtcGetOptiXIR == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetOptiXIR is not found") + return (__nvrtcGetOptiXIR)(prog, optixir) + +cdef nvrtcResult _nvrtcGetProgramLogSize(nvrtcProgram prog, size_t* logSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetProgramLogSize + _check_or_init_nvrtc() + if __nvrtcGetProgramLogSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetProgramLogSize is not found") + return (__nvrtcGetProgramLogSize)(prog, logSizeRet) + +cdef nvrtcResult _nvrtcGetProgramLog(nvrtcProgram prog, char* log) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetProgramLog + _check_or_init_nvrtc() + if __nvrtcGetProgramLog == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetProgramLog is not found") + return (__nvrtcGetProgramLog)(prog, log) + +cdef nvrtcResult _nvrtcAddNameExpression(nvrtcProgram prog, const char* name_expression) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcAddNameExpression + _check_or_init_nvrtc() + if __nvrtcAddNameExpression == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcAddNameExpression is not found") + return (__nvrtcAddNameExpression)(prog, name_expression) + +cdef nvrtcResult _nvrtcGetLoweredName(nvrtcProgram prog, const char* name_expression, const char** lowered_name) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetLoweredName + _check_or_init_nvrtc() + if __nvrtcGetLoweredName == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetLoweredName is not found") + return (__nvrtcGetLoweredName)(prog, name_expression, lowered_name) + +cdef nvrtcResult _nvrtcGetPCHHeapSize(size_t* ret) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPCHHeapSize + _check_or_init_nvrtc() + if __nvrtcGetPCHHeapSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPCHHeapSize is not found") + return (__nvrtcGetPCHHeapSize)(ret) + +cdef nvrtcResult _nvrtcSetPCHHeapSize(size_t size) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcSetPCHHeapSize + _check_or_init_nvrtc() + if __nvrtcSetPCHHeapSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcSetPCHHeapSize is not found") + return (__nvrtcSetPCHHeapSize)(size) + +cdef nvrtcResult _nvrtcGetPCHCreateStatus(nvrtcProgram prog) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPCHCreateStatus + _check_or_init_nvrtc() + if __nvrtcGetPCHCreateStatus == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPCHCreateStatus is not found") + return (__nvrtcGetPCHCreateStatus)(prog) + +cdef nvrtcResult _nvrtcGetPCHHeapSizeRequired(nvrtcProgram prog, size_t* size) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPCHHeapSizeRequired + _check_or_init_nvrtc() + if __nvrtcGetPCHHeapSizeRequired == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPCHHeapSizeRequired is not found") + return (__nvrtcGetPCHHeapSizeRequired)(prog, size) + +cdef nvrtcResult _nvrtcSetFlowCallback(nvrtcProgram prog, void* callback, void* payload) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcSetFlowCallback + _check_or_init_nvrtc() + if __nvrtcSetFlowCallback == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcSetFlowCallback is not found") + return (__nvrtcSetFlowCallback)(prog, callback, payload) + +cdef nvrtcResult _nvrtcGetTileIRSize(nvrtcProgram prog, size_t* TileIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetTileIRSize + _check_or_init_nvrtc() + if __nvrtcGetTileIRSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetTileIRSize is not found") + return (__nvrtcGetTileIRSize)(prog, TileIRSizeRet) + +cdef nvrtcResult _nvrtcGetTileIR(nvrtcProgram prog, char* TileIR) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetTileIR + _check_or_init_nvrtc() + if __nvrtcGetTileIR == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetTileIR is not found") + return (__nvrtcGetTileIR)(prog, TileIR) diff --git a/cuda_bindings/cuda/bindings/_internal/nvrtc_windows.pyx b/cuda_bindings/cuda/bindings/_internal/nvrtc_windows.pyx new file mode 100644 index 00000000000..1fb555644e1 --- /dev/null +++ b/cuda_bindings/cuda/bindings/_internal/nvrtc_windows.pyx @@ -0,0 +1,511 @@ +# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1364+ged01d643e. Do not modify it directly. + +from libc.stdint cimport intptr_t + +import threading +from .utils import FunctionNotFoundError, NotSupportedError + +from cuda.pathfinder import load_nvidia_dynamic_lib + +from libc.stddef cimport wchar_t +from libc.stdint cimport uintptr_t +from cpython cimport PyUnicode_AsWideCharString, PyMem_Free + +# You must 'from .utils import NotSupportedError' before using this template + +cdef extern from "windows.h" nogil: + ctypedef void* HMODULE + ctypedef void* HANDLE + ctypedef void* FARPROC + ctypedef unsigned long DWORD + ctypedef const wchar_t *LPCWSTR + ctypedef const char *LPCSTR + + cdef DWORD LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 + cdef DWORD LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000 + cdef DWORD LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100 + + HMODULE _LoadLibraryExW "LoadLibraryExW"( + LPCWSTR lpLibFileName, + HANDLE hFile, + DWORD dwFlags + ) + + FARPROC _GetProcAddress "GetProcAddress"(HMODULE hModule, LPCSTR lpProcName) + +cdef inline uintptr_t LoadLibraryExW(str path, HANDLE hFile, DWORD dwFlags): + cdef uintptr_t result + cdef wchar_t* wpath = PyUnicode_AsWideCharString(path, NULL) + with nogil: + result = _LoadLibraryExW( + wpath, + hFile, + dwFlags + ) + PyMem_Free(wpath) + return result + +cdef inline void *GetProcAddress(uintptr_t hModule, const char* lpProcName) nogil: + return _GetProcAddress(hModule, lpProcName) + +cdef int get_cuda_version(): + cdef int err, driver_ver = 0 + + # Load driver to check version + handle = LoadLibraryExW("nvcuda.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32) + if handle == 0: + raise NotSupportedError('CUDA driver is not found') + cuDriverGetVersion = GetProcAddress(handle, 'cuDriverGetVersion') + if cuDriverGetVersion == NULL: + raise RuntimeError('Did not find cuDriverGetVersion symbol in nvcuda.dll') + err = (cuDriverGetVersion)(&driver_ver) + if err != 0: + raise RuntimeError(f'cuDriverGetVersion returned error code {err}') + + return driver_ver + +############################################################################### +# Wrapper init +############################################################################### + +cdef object __symbol_lock = threading.Lock() +cdef bint __py_nvrtc_init = False + +cdef void* __nvrtcGetErrorString = NULL +cdef void* __nvrtcVersion = NULL +cdef void* __nvrtcGetNumSupportedArchs = NULL +cdef void* __nvrtcGetSupportedArchs = NULL +cdef void* __nvrtcCreateProgram = NULL +cdef void* __nvrtcDestroyProgram = NULL +cdef void* __nvrtcCompileProgram = NULL +cdef void* __nvrtcGetPTXSize = NULL +cdef void* __nvrtcGetPTX = NULL +cdef void* __nvrtcGetCUBINSize = NULL +cdef void* __nvrtcGetCUBIN = NULL +cdef void* __nvrtcGetLTOIRSize = NULL +cdef void* __nvrtcGetLTOIR = NULL +cdef void* __nvrtcGetOptiXIRSize = NULL +cdef void* __nvrtcGetOptiXIR = NULL +cdef void* __nvrtcGetProgramLogSize = NULL +cdef void* __nvrtcGetProgramLog = NULL +cdef void* __nvrtcAddNameExpression = NULL +cdef void* __nvrtcGetLoweredName = NULL +cdef void* __nvrtcGetPCHHeapSize = NULL +cdef void* __nvrtcSetPCHHeapSize = NULL +cdef void* __nvrtcGetPCHCreateStatus = NULL +cdef void* __nvrtcGetPCHHeapSizeRequired = NULL +cdef void* __nvrtcSetFlowCallback = NULL +cdef void* __nvrtcGetTileIRSize = NULL +cdef void* __nvrtcGetTileIR = NULL + +cdef int _init_nvrtc() except -1 nogil: + global __py_nvrtc_init + + with gil, __symbol_lock: + # Recheck the flag after obtaining the locks + if __py_nvrtc_init: + return 0 + + # Load library + handle = load_nvidia_dynamic_lib("nvrtc")._handle_uint + + # Load function + global __nvrtcGetErrorString + __nvrtcGetErrorString = GetProcAddress(handle, 'nvrtcGetErrorString') + + global __nvrtcVersion + __nvrtcVersion = GetProcAddress(handle, 'nvrtcVersion') + + global __nvrtcGetNumSupportedArchs + __nvrtcGetNumSupportedArchs = GetProcAddress(handle, 'nvrtcGetNumSupportedArchs') + + global __nvrtcGetSupportedArchs + __nvrtcGetSupportedArchs = GetProcAddress(handle, 'nvrtcGetSupportedArchs') + + global __nvrtcCreateProgram + __nvrtcCreateProgram = GetProcAddress(handle, 'nvrtcCreateProgram') + + global __nvrtcDestroyProgram + __nvrtcDestroyProgram = GetProcAddress(handle, 'nvrtcDestroyProgram') + + global __nvrtcCompileProgram + __nvrtcCompileProgram = GetProcAddress(handle, 'nvrtcCompileProgram') + + global __nvrtcGetPTXSize + __nvrtcGetPTXSize = GetProcAddress(handle, 'nvrtcGetPTXSize') + + global __nvrtcGetPTX + __nvrtcGetPTX = GetProcAddress(handle, 'nvrtcGetPTX') + + global __nvrtcGetCUBINSize + __nvrtcGetCUBINSize = GetProcAddress(handle, 'nvrtcGetCUBINSize') + + global __nvrtcGetCUBIN + __nvrtcGetCUBIN = GetProcAddress(handle, 'nvrtcGetCUBIN') + + global __nvrtcGetLTOIRSize + __nvrtcGetLTOIRSize = GetProcAddress(handle, 'nvrtcGetLTOIRSize') + + global __nvrtcGetLTOIR + __nvrtcGetLTOIR = GetProcAddress(handle, 'nvrtcGetLTOIR') + + global __nvrtcGetOptiXIRSize + __nvrtcGetOptiXIRSize = GetProcAddress(handle, 'nvrtcGetOptiXIRSize') + + global __nvrtcGetOptiXIR + __nvrtcGetOptiXIR = GetProcAddress(handle, 'nvrtcGetOptiXIR') + + global __nvrtcGetProgramLogSize + __nvrtcGetProgramLogSize = GetProcAddress(handle, 'nvrtcGetProgramLogSize') + + global __nvrtcGetProgramLog + __nvrtcGetProgramLog = GetProcAddress(handle, 'nvrtcGetProgramLog') + + global __nvrtcAddNameExpression + __nvrtcAddNameExpression = GetProcAddress(handle, 'nvrtcAddNameExpression') + + global __nvrtcGetLoweredName + __nvrtcGetLoweredName = GetProcAddress(handle, 'nvrtcGetLoweredName') + + global __nvrtcGetPCHHeapSize + __nvrtcGetPCHHeapSize = GetProcAddress(handle, 'nvrtcGetPCHHeapSize') + + global __nvrtcSetPCHHeapSize + __nvrtcSetPCHHeapSize = GetProcAddress(handle, 'nvrtcSetPCHHeapSize') + + global __nvrtcGetPCHCreateStatus + __nvrtcGetPCHCreateStatus = GetProcAddress(handle, 'nvrtcGetPCHCreateStatus') + + global __nvrtcGetPCHHeapSizeRequired + __nvrtcGetPCHHeapSizeRequired = GetProcAddress(handle, 'nvrtcGetPCHHeapSizeRequired') + + global __nvrtcSetFlowCallback + __nvrtcSetFlowCallback = GetProcAddress(handle, 'nvrtcSetFlowCallback') + + global __nvrtcGetTileIRSize + __nvrtcGetTileIRSize = GetProcAddress(handle, 'nvrtcGetTileIRSize') + + global __nvrtcGetTileIR + __nvrtcGetTileIR = GetProcAddress(handle, 'nvrtcGetTileIR') + + __py_nvrtc_init = True + return 0 + +cdef inline int _check_or_init_nvrtc() except -1 nogil: + if __py_nvrtc_init: + return 0 + + return _init_nvrtc() + +cdef dict func_ptrs = None + +cpdef dict _inspect_function_pointers(): + global func_ptrs + if func_ptrs is not None: + return func_ptrs + + _check_or_init_nvrtc() + cdef dict data = {} + + global __nvrtcGetErrorString + data["__nvrtcGetErrorString"] = __nvrtcGetErrorString + + global __nvrtcVersion + data["__nvrtcVersion"] = __nvrtcVersion + + global __nvrtcGetNumSupportedArchs + data["__nvrtcGetNumSupportedArchs"] = __nvrtcGetNumSupportedArchs + + global __nvrtcGetSupportedArchs + data["__nvrtcGetSupportedArchs"] = __nvrtcGetSupportedArchs + + global __nvrtcCreateProgram + data["__nvrtcCreateProgram"] = __nvrtcCreateProgram + + global __nvrtcDestroyProgram + data["__nvrtcDestroyProgram"] = __nvrtcDestroyProgram + + global __nvrtcCompileProgram + data["__nvrtcCompileProgram"] = __nvrtcCompileProgram + + global __nvrtcGetPTXSize + data["__nvrtcGetPTXSize"] = __nvrtcGetPTXSize + + global __nvrtcGetPTX + data["__nvrtcGetPTX"] = __nvrtcGetPTX + + global __nvrtcGetCUBINSize + data["__nvrtcGetCUBINSize"] = __nvrtcGetCUBINSize + + global __nvrtcGetCUBIN + data["__nvrtcGetCUBIN"] = __nvrtcGetCUBIN + + global __nvrtcGetLTOIRSize + data["__nvrtcGetLTOIRSize"] = __nvrtcGetLTOIRSize + + global __nvrtcGetLTOIR + data["__nvrtcGetLTOIR"] = __nvrtcGetLTOIR + + global __nvrtcGetOptiXIRSize + data["__nvrtcGetOptiXIRSize"] = __nvrtcGetOptiXIRSize + + global __nvrtcGetOptiXIR + data["__nvrtcGetOptiXIR"] = __nvrtcGetOptiXIR + + global __nvrtcGetProgramLogSize + data["__nvrtcGetProgramLogSize"] = __nvrtcGetProgramLogSize + + global __nvrtcGetProgramLog + data["__nvrtcGetProgramLog"] = __nvrtcGetProgramLog + + global __nvrtcAddNameExpression + data["__nvrtcAddNameExpression"] = __nvrtcAddNameExpression + + global __nvrtcGetLoweredName + data["__nvrtcGetLoweredName"] = __nvrtcGetLoweredName + + global __nvrtcGetPCHHeapSize + data["__nvrtcGetPCHHeapSize"] = __nvrtcGetPCHHeapSize + + global __nvrtcSetPCHHeapSize + data["__nvrtcSetPCHHeapSize"] = __nvrtcSetPCHHeapSize + + global __nvrtcGetPCHCreateStatus + data["__nvrtcGetPCHCreateStatus"] = __nvrtcGetPCHCreateStatus + + global __nvrtcGetPCHHeapSizeRequired + data["__nvrtcGetPCHHeapSizeRequired"] = __nvrtcGetPCHHeapSizeRequired + + global __nvrtcSetFlowCallback + data["__nvrtcSetFlowCallback"] = __nvrtcSetFlowCallback + + global __nvrtcGetTileIRSize + data["__nvrtcGetTileIRSize"] = __nvrtcGetTileIRSize + + global __nvrtcGetTileIR + data["__nvrtcGetTileIR"] = __nvrtcGetTileIR + + func_ptrs = data + return data + +cpdef _inspect_function_pointer(str name): + global func_ptrs + if func_ptrs is None: + func_ptrs = _inspect_function_pointers() + return func_ptrs[name] + +############################################################################### +# Wrapper functions +############################################################################### + +cdef const char* _nvrtcGetErrorString(nvrtcResult result) except ?NULL nogil: + global __nvrtcGetErrorString + _check_or_init_nvrtc() + if __nvrtcGetErrorString == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetErrorString is not found") + return (__nvrtcGetErrorString)(result) + +cdef nvrtcResult _nvrtcVersion(int* major, int* minor) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcVersion + _check_or_init_nvrtc() + if __nvrtcVersion == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcVersion is not found") + return (__nvrtcVersion)(major, minor) + +cdef nvrtcResult _nvrtcGetNumSupportedArchs(int* numArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetNumSupportedArchs + _check_or_init_nvrtc() + if __nvrtcGetNumSupportedArchs == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetNumSupportedArchs is not found") + return (__nvrtcGetNumSupportedArchs)(numArchs) + +cdef nvrtcResult _nvrtcGetSupportedArchs(int* supportedArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetSupportedArchs + _check_or_init_nvrtc() + if __nvrtcGetSupportedArchs == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetSupportedArchs is not found") + return (__nvrtcGetSupportedArchs)(supportedArchs) + +cdef nvrtcResult _nvrtcCreateProgram(nvrtcProgram* prog, const char* src, const char* name, int numHeaders, const char** headers, const char** includeNames) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcCreateProgram + _check_or_init_nvrtc() + if __nvrtcCreateProgram == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcCreateProgram is not found") + return (__nvrtcCreateProgram)(prog, src, name, numHeaders, headers, includeNames) + +cdef nvrtcResult _nvrtcDestroyProgram(nvrtcProgram* prog) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcDestroyProgram + _check_or_init_nvrtc() + if __nvrtcDestroyProgram == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcDestroyProgram is not found") + return (__nvrtcDestroyProgram)(prog) + +cdef nvrtcResult _nvrtcCompileProgram(nvrtcProgram prog, int numOptions, const char** options) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcCompileProgram + _check_or_init_nvrtc() + if __nvrtcCompileProgram == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcCompileProgram is not found") + return (__nvrtcCompileProgram)(prog, numOptions, options) + +cdef nvrtcResult _nvrtcGetPTXSize(nvrtcProgram prog, size_t* ptxSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPTXSize + _check_or_init_nvrtc() + if __nvrtcGetPTXSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPTXSize is not found") + return (__nvrtcGetPTXSize)(prog, ptxSizeRet) + +cdef nvrtcResult _nvrtcGetPTX(nvrtcProgram prog, char* ptx) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPTX + _check_or_init_nvrtc() + if __nvrtcGetPTX == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPTX is not found") + return (__nvrtcGetPTX)(prog, ptx) + +cdef nvrtcResult _nvrtcGetCUBINSize(nvrtcProgram prog, size_t* cubinSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetCUBINSize + _check_or_init_nvrtc() + if __nvrtcGetCUBINSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetCUBINSize is not found") + return (__nvrtcGetCUBINSize)(prog, cubinSizeRet) + +cdef nvrtcResult _nvrtcGetCUBIN(nvrtcProgram prog, char* cubin) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetCUBIN + _check_or_init_nvrtc() + if __nvrtcGetCUBIN == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetCUBIN is not found") + return (__nvrtcGetCUBIN)(prog, cubin) + +cdef nvrtcResult _nvrtcGetLTOIRSize(nvrtcProgram prog, size_t* LTOIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetLTOIRSize + _check_or_init_nvrtc() + if __nvrtcGetLTOIRSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetLTOIRSize is not found") + return (__nvrtcGetLTOIRSize)(prog, LTOIRSizeRet) + +cdef nvrtcResult _nvrtcGetLTOIR(nvrtcProgram prog, char* LTOIR) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetLTOIR + _check_or_init_nvrtc() + if __nvrtcGetLTOIR == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetLTOIR is not found") + return (__nvrtcGetLTOIR)(prog, LTOIR) + +cdef nvrtcResult _nvrtcGetOptiXIRSize(nvrtcProgram prog, size_t* optixirSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetOptiXIRSize + _check_or_init_nvrtc() + if __nvrtcGetOptiXIRSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetOptiXIRSize is not found") + return (__nvrtcGetOptiXIRSize)(prog, optixirSizeRet) + +cdef nvrtcResult _nvrtcGetOptiXIR(nvrtcProgram prog, char* optixir) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetOptiXIR + _check_or_init_nvrtc() + if __nvrtcGetOptiXIR == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetOptiXIR is not found") + return (__nvrtcGetOptiXIR)(prog, optixir) + +cdef nvrtcResult _nvrtcGetProgramLogSize(nvrtcProgram prog, size_t* logSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetProgramLogSize + _check_or_init_nvrtc() + if __nvrtcGetProgramLogSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetProgramLogSize is not found") + return (__nvrtcGetProgramLogSize)(prog, logSizeRet) + +cdef nvrtcResult _nvrtcGetProgramLog(nvrtcProgram prog, char* log) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetProgramLog + _check_or_init_nvrtc() + if __nvrtcGetProgramLog == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetProgramLog is not found") + return (__nvrtcGetProgramLog)(prog, log) + +cdef nvrtcResult _nvrtcAddNameExpression(nvrtcProgram prog, const char* name_expression) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcAddNameExpression + _check_or_init_nvrtc() + if __nvrtcAddNameExpression == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcAddNameExpression is not found") + return (__nvrtcAddNameExpression)(prog, name_expression) + +cdef nvrtcResult _nvrtcGetLoweredName(nvrtcProgram prog, const char* name_expression, const char** lowered_name) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetLoweredName + _check_or_init_nvrtc() + if __nvrtcGetLoweredName == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetLoweredName is not found") + return (__nvrtcGetLoweredName)(prog, name_expression, lowered_name) + +cdef nvrtcResult _nvrtcGetPCHHeapSize(size_t* ret) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPCHHeapSize + _check_or_init_nvrtc() + if __nvrtcGetPCHHeapSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPCHHeapSize is not found") + return (__nvrtcGetPCHHeapSize)(ret) + +cdef nvrtcResult _nvrtcSetPCHHeapSize(size_t size) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcSetPCHHeapSize + _check_or_init_nvrtc() + if __nvrtcSetPCHHeapSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcSetPCHHeapSize is not found") + return (__nvrtcSetPCHHeapSize)(size) + +cdef nvrtcResult _nvrtcGetPCHCreateStatus(nvrtcProgram prog) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPCHCreateStatus + _check_or_init_nvrtc() + if __nvrtcGetPCHCreateStatus == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPCHCreateStatus is not found") + return (__nvrtcGetPCHCreateStatus)(prog) + +cdef nvrtcResult _nvrtcGetPCHHeapSizeRequired(nvrtcProgram prog, size_t* size) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetPCHHeapSizeRequired + _check_or_init_nvrtc() + if __nvrtcGetPCHHeapSizeRequired == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetPCHHeapSizeRequired is not found") + return (__nvrtcGetPCHHeapSizeRequired)(prog, size) + +cdef nvrtcResult _nvrtcSetFlowCallback(nvrtcProgram prog, void* callback, void* payload) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcSetFlowCallback + _check_or_init_nvrtc() + if __nvrtcSetFlowCallback == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcSetFlowCallback is not found") + return (__nvrtcSetFlowCallback)(prog, callback, payload) + +cdef nvrtcResult _nvrtcGetTileIRSize(nvrtcProgram prog, size_t* TileIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetTileIRSize + _check_or_init_nvrtc() + if __nvrtcGetTileIRSize == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetTileIRSize is not found") + return (__nvrtcGetTileIRSize)(prog, TileIRSizeRet) + +cdef nvrtcResult _nvrtcGetTileIR(nvrtcProgram prog, char* TileIR) except ?NVRTC_ERROR_INVALID_INPUT nogil: + global __nvrtcGetTileIR + _check_or_init_nvrtc() + if __nvrtcGetTileIR == NULL: + with gil: + raise FunctionNotFoundError("function nvrtcGetTileIR is not found") + return (__nvrtcGetTileIR)(prog, TileIR) diff --git a/cuda_bindings/cuda/bindings/cynvrtc.pxd.in b/cuda_bindings/cuda/bindings/cynvrtc.pxd similarity index 72% rename from cuda_bindings/cuda/bindings/cynvrtc.pxd.in rename to cuda_bindings/cuda/bindings/cynvrtc.pxd index 209b361c053..6bf1bda94e1 100644 --- a/cuda_bindings/cuda/bindings/cynvrtc.pxd.in +++ b/cuda_bindings/cuda/bindings/cynvrtc.pxd @@ -1,7 +1,8 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1364+ged01d643e. Do not modify it directly. from libc.stdint cimport uint32_t, uint64_t @@ -31,133 +32,54 @@ cdef extern from "nvrtc.h": pass ctypedef _nvrtcProgram* nvrtcProgram -{{if 'nvrtcGetErrorString' in found_functions}} - cdef const char* nvrtcGetErrorString(nvrtcResult result) except ?NULL nogil -{{endif}} - -{{if 'nvrtcVersion' in found_functions}} cdef nvrtcResult nvrtcVersion(int* major, int* minor) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetNumSupportedArchs' in found_functions}} cdef nvrtcResult nvrtcGetNumSupportedArchs(int* numArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetSupportedArchs' in found_functions}} cdef nvrtcResult nvrtcGetSupportedArchs(int* supportedArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcCreateProgram' in found_functions}} cdef nvrtcResult nvrtcCreateProgram(nvrtcProgram* prog, const char* src, const char* name, int numHeaders, const char** headers, const char** includeNames) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcDestroyProgram' in found_functions}} cdef nvrtcResult nvrtcDestroyProgram(nvrtcProgram* prog) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcCompileProgram' in found_functions}} cdef nvrtcResult nvrtcCompileProgram(nvrtcProgram prog, int numOptions, const char** options) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPTXSize' in found_functions}} cdef nvrtcResult nvrtcGetPTXSize(nvrtcProgram prog, size_t* ptxSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPTX' in found_functions}} cdef nvrtcResult nvrtcGetPTX(nvrtcProgram prog, char* ptx) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetCUBINSize' in found_functions}} cdef nvrtcResult nvrtcGetCUBINSize(nvrtcProgram prog, size_t* cubinSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetCUBIN' in found_functions}} cdef nvrtcResult nvrtcGetCUBIN(nvrtcProgram prog, char* cubin) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetLTOIRSize' in found_functions}} cdef nvrtcResult nvrtcGetLTOIRSize(nvrtcProgram prog, size_t* LTOIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetLTOIR' in found_functions}} cdef nvrtcResult nvrtcGetLTOIR(nvrtcProgram prog, char* LTOIR) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetOptiXIRSize' in found_functions}} cdef nvrtcResult nvrtcGetOptiXIRSize(nvrtcProgram prog, size_t* optixirSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetOptiXIR' in found_functions}} cdef nvrtcResult nvrtcGetOptiXIR(nvrtcProgram prog, char* optixir) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetProgramLogSize' in found_functions}} cdef nvrtcResult nvrtcGetProgramLogSize(nvrtcProgram prog, size_t* logSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetProgramLog' in found_functions}} cdef nvrtcResult nvrtcGetProgramLog(nvrtcProgram prog, char* log) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcAddNameExpression' in found_functions}} cdef nvrtcResult nvrtcAddNameExpression(nvrtcProgram prog, const char* name_expression) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetLoweredName' in found_functions}} cdef nvrtcResult nvrtcGetLoweredName(nvrtcProgram prog, const char* name_expression, const char** lowered_name) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPCHHeapSize' in found_functions}} cdef nvrtcResult nvrtcGetPCHHeapSize(size_t* ret) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcSetPCHHeapSize' in found_functions}} cdef nvrtcResult nvrtcSetPCHHeapSize(size_t size) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPCHCreateStatus' in found_functions}} cdef nvrtcResult nvrtcGetPCHCreateStatus(nvrtcProgram prog) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}} cdef nvrtcResult nvrtcGetPCHHeapSizeRequired(nvrtcProgram prog, size_t* size) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcSetFlowCallback' in found_functions}} cdef nvrtcResult nvrtcSetFlowCallback(nvrtcProgram prog, void* callback, void* payload) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetTileIRSize' in found_functions}} cdef nvrtcResult nvrtcGetTileIRSize(nvrtcProgram prog, size_t* TileIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - -{{if 'nvrtcGetTileIR' in found_functions}} cdef nvrtcResult nvrtcGetTileIR(nvrtcProgram prog, char* TileIR) except ?NVRTC_ERROR_INVALID_INPUT nogil -{{endif}} - diff --git a/cuda_bindings/cuda/bindings/cynvrtc.pyx.in b/cuda_bindings/cuda/bindings/cynvrtc.pyx similarity index 50% rename from cuda_bindings/cuda/bindings/cynvrtc.pyx.in rename to cuda_bindings/cuda/bindings/cynvrtc.pyx index 9e64cbf9c15..2bf71611d72 100644 --- a/cuda_bindings/cuda/bindings/cynvrtc.pyx.in +++ b/cuda_bindings/cuda/bindings/cynvrtc.pyx @@ -1,161 +1,85 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1364+ged01d643e. Do not modify it directly. -# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. -cimport cuda.bindings._bindings.cynvrtc as cynvrtc - -{{if 'nvrtcGetErrorString' in found_functions}} +from ._internal cimport nvrtc as _nvrtc cdef const char* nvrtcGetErrorString(nvrtcResult result) except ?NULL nogil: - return cynvrtc._nvrtcGetErrorString(result) -{{endif}} - -{{if 'nvrtcVersion' in found_functions}} + return _nvrtc._nvrtcGetErrorString(result) cdef nvrtcResult nvrtcVersion(int* major, int* minor) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcVersion(major, minor) -{{endif}} - -{{if 'nvrtcGetNumSupportedArchs' in found_functions}} + return _nvrtc._nvrtcVersion(major, minor) cdef nvrtcResult nvrtcGetNumSupportedArchs(int* numArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetNumSupportedArchs(numArchs) -{{endif}} - -{{if 'nvrtcGetSupportedArchs' in found_functions}} + return _nvrtc._nvrtcGetNumSupportedArchs(numArchs) cdef nvrtcResult nvrtcGetSupportedArchs(int* supportedArchs) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetSupportedArchs(supportedArchs) -{{endif}} - -{{if 'nvrtcCreateProgram' in found_functions}} + return _nvrtc._nvrtcGetSupportedArchs(supportedArchs) cdef nvrtcResult nvrtcCreateProgram(nvrtcProgram* prog, const char* src, const char* name, int numHeaders, const char** headers, const char** includeNames) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcCreateProgram(prog, src, name, numHeaders, headers, includeNames) -{{endif}} - -{{if 'nvrtcDestroyProgram' in found_functions}} + return _nvrtc._nvrtcCreateProgram(prog, src, name, numHeaders, headers, includeNames) cdef nvrtcResult nvrtcDestroyProgram(nvrtcProgram* prog) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcDestroyProgram(prog) -{{endif}} - -{{if 'nvrtcCompileProgram' in found_functions}} + return _nvrtc._nvrtcDestroyProgram(prog) cdef nvrtcResult nvrtcCompileProgram(nvrtcProgram prog, int numOptions, const char** options) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcCompileProgram(prog, numOptions, options) -{{endif}} - -{{if 'nvrtcGetPTXSize' in found_functions}} + return _nvrtc._nvrtcCompileProgram(prog, numOptions, options) cdef nvrtcResult nvrtcGetPTXSize(nvrtcProgram prog, size_t* ptxSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetPTXSize(prog, ptxSizeRet) -{{endif}} - -{{if 'nvrtcGetPTX' in found_functions}} + return _nvrtc._nvrtcGetPTXSize(prog, ptxSizeRet) cdef nvrtcResult nvrtcGetPTX(nvrtcProgram prog, char* ptx) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetPTX(prog, ptx) -{{endif}} - -{{if 'nvrtcGetCUBINSize' in found_functions}} + return _nvrtc._nvrtcGetPTX(prog, ptx) cdef nvrtcResult nvrtcGetCUBINSize(nvrtcProgram prog, size_t* cubinSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetCUBINSize(prog, cubinSizeRet) -{{endif}} - -{{if 'nvrtcGetCUBIN' in found_functions}} + return _nvrtc._nvrtcGetCUBINSize(prog, cubinSizeRet) cdef nvrtcResult nvrtcGetCUBIN(nvrtcProgram prog, char* cubin) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetCUBIN(prog, cubin) -{{endif}} - -{{if 'nvrtcGetLTOIRSize' in found_functions}} + return _nvrtc._nvrtcGetCUBIN(prog, cubin) cdef nvrtcResult nvrtcGetLTOIRSize(nvrtcProgram prog, size_t* LTOIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetLTOIRSize(prog, LTOIRSizeRet) -{{endif}} - -{{if 'nvrtcGetLTOIR' in found_functions}} + return _nvrtc._nvrtcGetLTOIRSize(prog, LTOIRSizeRet) cdef nvrtcResult nvrtcGetLTOIR(nvrtcProgram prog, char* LTOIR) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetLTOIR(prog, LTOIR) -{{endif}} - -{{if 'nvrtcGetOptiXIRSize' in found_functions}} + return _nvrtc._nvrtcGetLTOIR(prog, LTOIR) cdef nvrtcResult nvrtcGetOptiXIRSize(nvrtcProgram prog, size_t* optixirSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetOptiXIRSize(prog, optixirSizeRet) -{{endif}} - -{{if 'nvrtcGetOptiXIR' in found_functions}} + return _nvrtc._nvrtcGetOptiXIRSize(prog, optixirSizeRet) cdef nvrtcResult nvrtcGetOptiXIR(nvrtcProgram prog, char* optixir) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetOptiXIR(prog, optixir) -{{endif}} - -{{if 'nvrtcGetProgramLogSize' in found_functions}} + return _nvrtc._nvrtcGetOptiXIR(prog, optixir) cdef nvrtcResult nvrtcGetProgramLogSize(nvrtcProgram prog, size_t* logSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetProgramLogSize(prog, logSizeRet) -{{endif}} - -{{if 'nvrtcGetProgramLog' in found_functions}} + return _nvrtc._nvrtcGetProgramLogSize(prog, logSizeRet) cdef nvrtcResult nvrtcGetProgramLog(nvrtcProgram prog, char* log) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetProgramLog(prog, log) -{{endif}} - -{{if 'nvrtcAddNameExpression' in found_functions}} + return _nvrtc._nvrtcGetProgramLog(prog, log) cdef nvrtcResult nvrtcAddNameExpression(nvrtcProgram prog, const char* name_expression) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcAddNameExpression(prog, name_expression) -{{endif}} - -{{if 'nvrtcGetLoweredName' in found_functions}} + return _nvrtc._nvrtcAddNameExpression(prog, name_expression) cdef nvrtcResult nvrtcGetLoweredName(nvrtcProgram prog, const char* name_expression, const char** lowered_name) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetLoweredName(prog, name_expression, lowered_name) -{{endif}} - -{{if 'nvrtcGetPCHHeapSize' in found_functions}} + return _nvrtc._nvrtcGetLoweredName(prog, name_expression, lowered_name) cdef nvrtcResult nvrtcGetPCHHeapSize(size_t* ret) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetPCHHeapSize(ret) -{{endif}} - -{{if 'nvrtcSetPCHHeapSize' in found_functions}} + return _nvrtc._nvrtcGetPCHHeapSize(ret) cdef nvrtcResult nvrtcSetPCHHeapSize(size_t size) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcSetPCHHeapSize(size) -{{endif}} - -{{if 'nvrtcGetPCHCreateStatus' in found_functions}} + return _nvrtc._nvrtcSetPCHHeapSize(size) cdef nvrtcResult nvrtcGetPCHCreateStatus(nvrtcProgram prog) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetPCHCreateStatus(prog) -{{endif}} - -{{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}} + return _nvrtc._nvrtcGetPCHCreateStatus(prog) cdef nvrtcResult nvrtcGetPCHHeapSizeRequired(nvrtcProgram prog, size_t* size) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetPCHHeapSizeRequired(prog, size) -{{endif}} - -{{if 'nvrtcSetFlowCallback' in found_functions}} + return _nvrtc._nvrtcGetPCHHeapSizeRequired(prog, size) cdef nvrtcResult nvrtcSetFlowCallback(nvrtcProgram prog, void* callback, void* payload) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcSetFlowCallback(prog, callback, payload) -{{endif}} - -{{if 'nvrtcGetTileIRSize' in found_functions}} + return _nvrtc._nvrtcSetFlowCallback(prog, callback, payload) cdef nvrtcResult nvrtcGetTileIRSize(nvrtcProgram prog, size_t* TileIRSizeRet) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetTileIRSize(prog, TileIRSizeRet) -{{endif}} - -{{if 'nvrtcGetTileIR' in found_functions}} + return _nvrtc._nvrtcGetTileIRSize(prog, TileIRSizeRet) cdef nvrtcResult nvrtcGetTileIR(nvrtcProgram prog, char* TileIR) except ?NVRTC_ERROR_INVALID_INPUT nogil: - return cynvrtc._nvrtcGetTileIR(prog, TileIR) -{{endif}} + return _nvrtc._nvrtcGetTileIR(prog, TileIR) diff --git a/cuda_bindings/cuda/bindings/nvrtc.pxd.in b/cuda_bindings/cuda/bindings/nvrtc.pxd similarity index 85% rename from cuda_bindings/cuda/bindings/nvrtc.pxd.in rename to cuda_bindings/cuda/bindings/nvrtc.pxd index 394bcccff0e..5cb372430f9 100644 --- a/cuda_bindings/cuda/bindings/nvrtc.pxd.in +++ b/cuda_bindings/cuda/bindings/nvrtc.pxd @@ -1,13 +1,11 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE -# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1364+ged01d643e. Do not modify it directly. cimport cuda.bindings.cynvrtc as cynvrtc include "_lib/utils.pxd" -{{if 'nvrtcProgram' in found_types}} - cdef class nvrtcProgram: """ nvrtcProgram is the unit of compilation, and an opaque handle for a program. @@ -21,4 +19,3 @@ cdef class nvrtcProgram: """ cdef cynvrtc.nvrtcProgram _pvt_val cdef cynvrtc.nvrtcProgram* _pvt_ptr -{{endif}} diff --git a/cuda_bindings/cuda/bindings/nvrtc.pyx.in b/cuda_bindings/cuda/bindings/nvrtc.pyx similarity index 90% rename from cuda_bindings/cuda/bindings/nvrtc.pyx.in rename to cuda_bindings/cuda/bindings/nvrtc.pyx index 2f50afa7265..aca1ead365f 100644 --- a/cuda_bindings/cuda/bindings/nvrtc.pyx.in +++ b/cuda_bindings/cuda/bindings/nvrtc.pyx @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE -# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1422+gf4812259e.d20260318. Do not modify it directly. +# This code was automatically generated with version 13.2.0, generator version 0.3.1.dev1364+ged01d643e. Do not modify it directly. from typing import Any, Optional import cython import ctypes @@ -43,59 +43,50 @@ ctypedef unsigned long long float_ptr ctypedef unsigned long long double_ptr ctypedef unsigned long long void_ptr - -{{if 'nvrtcResult' in found_types}} - class nvrtcResult(_FastEnum): """ The enumerated type nvrtcResult defines API call result codes. NVRTC API functions return nvrtcResult to indicate the call result. """ - {{if 'NVRTC_SUCCESS' in found_values}} - NVRTC_SUCCESS = cynvrtc.nvrtcResult.NVRTC_SUCCESS{{endif}} - {{if 'NVRTC_ERROR_OUT_OF_MEMORY' in found_values}} - NVRTC_ERROR_OUT_OF_MEMORY = cynvrtc.nvrtcResult.NVRTC_ERROR_OUT_OF_MEMORY{{endif}} - {{if 'NVRTC_ERROR_PROGRAM_CREATION_FAILURE' in found_values}} - NVRTC_ERROR_PROGRAM_CREATION_FAILURE = cynvrtc.nvrtcResult.NVRTC_ERROR_PROGRAM_CREATION_FAILURE{{endif}} - {{if 'NVRTC_ERROR_INVALID_INPUT' in found_values}} - NVRTC_ERROR_INVALID_INPUT = cynvrtc.nvrtcResult.NVRTC_ERROR_INVALID_INPUT{{endif}} - {{if 'NVRTC_ERROR_INVALID_PROGRAM' in found_values}} - NVRTC_ERROR_INVALID_PROGRAM = cynvrtc.nvrtcResult.NVRTC_ERROR_INVALID_PROGRAM{{endif}} - {{if 'NVRTC_ERROR_INVALID_OPTION' in found_values}} - NVRTC_ERROR_INVALID_OPTION = cynvrtc.nvrtcResult.NVRTC_ERROR_INVALID_OPTION{{endif}} - {{if 'NVRTC_ERROR_COMPILATION' in found_values}} - NVRTC_ERROR_COMPILATION = cynvrtc.nvrtcResult.NVRTC_ERROR_COMPILATION{{endif}} - {{if 'NVRTC_ERROR_BUILTIN_OPERATION_FAILURE' in found_values}} - NVRTC_ERROR_BUILTIN_OPERATION_FAILURE = cynvrtc.nvrtcResult.NVRTC_ERROR_BUILTIN_OPERATION_FAILURE{{endif}} - {{if 'NVRTC_ERROR_NO_NAME_EXPRESSIONS_AFTER_COMPILATION' in found_values}} - NVRTC_ERROR_NO_NAME_EXPRESSIONS_AFTER_COMPILATION = cynvrtc.nvrtcResult.NVRTC_ERROR_NO_NAME_EXPRESSIONS_AFTER_COMPILATION{{endif}} - {{if 'NVRTC_ERROR_NO_LOWERED_NAMES_BEFORE_COMPILATION' in found_values}} - NVRTC_ERROR_NO_LOWERED_NAMES_BEFORE_COMPILATION = cynvrtc.nvrtcResult.NVRTC_ERROR_NO_LOWERED_NAMES_BEFORE_COMPILATION{{endif}} - {{if 'NVRTC_ERROR_NAME_EXPRESSION_NOT_VALID' in found_values}} - NVRTC_ERROR_NAME_EXPRESSION_NOT_VALID = cynvrtc.nvrtcResult.NVRTC_ERROR_NAME_EXPRESSION_NOT_VALID{{endif}} - {{if 'NVRTC_ERROR_INTERNAL_ERROR' in found_values}} - NVRTC_ERROR_INTERNAL_ERROR = cynvrtc.nvrtcResult.NVRTC_ERROR_INTERNAL_ERROR{{endif}} - {{if 'NVRTC_ERROR_TIME_FILE_WRITE_FAILED' in found_values}} - NVRTC_ERROR_TIME_FILE_WRITE_FAILED = cynvrtc.nvrtcResult.NVRTC_ERROR_TIME_FILE_WRITE_FAILED{{endif}} - {{if 'NVRTC_ERROR_NO_PCH_CREATE_ATTEMPTED' in found_values}} - NVRTC_ERROR_NO_PCH_CREATE_ATTEMPTED = cynvrtc.nvrtcResult.NVRTC_ERROR_NO_PCH_CREATE_ATTEMPTED{{endif}} - {{if 'NVRTC_ERROR_PCH_CREATE_HEAP_EXHAUSTED' in found_values}} - NVRTC_ERROR_PCH_CREATE_HEAP_EXHAUSTED = cynvrtc.nvrtcResult.NVRTC_ERROR_PCH_CREATE_HEAP_EXHAUSTED{{endif}} - {{if 'NVRTC_ERROR_PCH_CREATE' in found_values}} - NVRTC_ERROR_PCH_CREATE = cynvrtc.nvrtcResult.NVRTC_ERROR_PCH_CREATE{{endif}} - {{if 'NVRTC_ERROR_CANCELLED' in found_values}} - NVRTC_ERROR_CANCELLED = cynvrtc.nvrtcResult.NVRTC_ERROR_CANCELLED{{endif}} - {{if 'NVRTC_ERROR_TIME_TRACE_FILE_WRITE_FAILED' in found_values}} - NVRTC_ERROR_TIME_TRACE_FILE_WRITE_FAILED = cynvrtc.nvrtcResult.NVRTC_ERROR_TIME_TRACE_FILE_WRITE_FAILED{{endif}} - -{{endif}} -cdef object _nvrtcResult = nvrtcResult -cdef object _nvrtcResult_SUCCESS = nvrtcResult.NVRTC_SUCCESS + NVRTC_SUCCESS = cynvrtc.nvrtcResult.NVRTC_SUCCESS + + NVRTC_ERROR_OUT_OF_MEMORY = cynvrtc.nvrtcResult.NVRTC_ERROR_OUT_OF_MEMORY + + NVRTC_ERROR_PROGRAM_CREATION_FAILURE = cynvrtc.nvrtcResult.NVRTC_ERROR_PROGRAM_CREATION_FAILURE + + NVRTC_ERROR_INVALID_INPUT = cynvrtc.nvrtcResult.NVRTC_ERROR_INVALID_INPUT + + NVRTC_ERROR_INVALID_PROGRAM = cynvrtc.nvrtcResult.NVRTC_ERROR_INVALID_PROGRAM + + NVRTC_ERROR_INVALID_OPTION = cynvrtc.nvrtcResult.NVRTC_ERROR_INVALID_OPTION + + NVRTC_ERROR_COMPILATION = cynvrtc.nvrtcResult.NVRTC_ERROR_COMPILATION + + NVRTC_ERROR_BUILTIN_OPERATION_FAILURE = cynvrtc.nvrtcResult.NVRTC_ERROR_BUILTIN_OPERATION_FAILURE + + NVRTC_ERROR_NO_NAME_EXPRESSIONS_AFTER_COMPILATION = cynvrtc.nvrtcResult.NVRTC_ERROR_NO_NAME_EXPRESSIONS_AFTER_COMPILATION + + NVRTC_ERROR_NO_LOWERED_NAMES_BEFORE_COMPILATION = cynvrtc.nvrtcResult.NVRTC_ERROR_NO_LOWERED_NAMES_BEFORE_COMPILATION + NVRTC_ERROR_NAME_EXPRESSION_NOT_VALID = cynvrtc.nvrtcResult.NVRTC_ERROR_NAME_EXPRESSION_NOT_VALID + NVRTC_ERROR_INTERNAL_ERROR = cynvrtc.nvrtcResult.NVRTC_ERROR_INTERNAL_ERROR -{{if 'nvrtcProgram' in found_types}} + NVRTC_ERROR_TIME_FILE_WRITE_FAILED = cynvrtc.nvrtcResult.NVRTC_ERROR_TIME_FILE_WRITE_FAILED + + NVRTC_ERROR_NO_PCH_CREATE_ATTEMPTED = cynvrtc.nvrtcResult.NVRTC_ERROR_NO_PCH_CREATE_ATTEMPTED + + NVRTC_ERROR_PCH_CREATE_HEAP_EXHAUSTED = cynvrtc.nvrtcResult.NVRTC_ERROR_PCH_CREATE_HEAP_EXHAUSTED + + NVRTC_ERROR_PCH_CREATE = cynvrtc.nvrtcResult.NVRTC_ERROR_PCH_CREATE + + NVRTC_ERROR_CANCELLED = cynvrtc.nvrtcResult.NVRTC_ERROR_CANCELLED + + NVRTC_ERROR_TIME_TRACE_FILE_WRITE_FAILED = cynvrtc.nvrtcResult.NVRTC_ERROR_TIME_TRACE_FILE_WRITE_FAILED + +cdef object _nvrtcResult = nvrtcResult +cdef object _nvrtcResult_SUCCESS = nvrtcResult.NVRTC_SUCCESS cdef class nvrtcProgram: """ nvrtcProgram is the unit of compilation, and an opaque handle for a program. @@ -130,9 +121,6 @@ cdef class nvrtcProgram: return self._pvt_ptr[0] def getPtr(self): return self._pvt_ptr -{{endif}} - -{{if 'nvrtcGetErrorString' in found_functions}} @cython.embedsignature(True) def nvrtcGetErrorString(result not None : nvrtcResult): @@ -154,9 +142,6 @@ def nvrtcGetErrorString(result not None : nvrtcResult): with nogil: err = cynvrtc.nvrtcGetErrorString(cyresult) return (nvrtcResult.NVRTC_SUCCESS, err) -{{endif}} - -{{if 'nvrtcVersion' in found_functions}} @cython.embedsignature(True) def nvrtcVersion(): @@ -179,9 +164,6 @@ def nvrtcVersion(): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None, None) return (_nvrtcResult_SUCCESS, major, minor) -{{endif}} - -{{if 'nvrtcGetNumSupportedArchs' in found_functions}} @cython.embedsignature(True) def nvrtcGetNumSupportedArchs(): @@ -203,9 +185,6 @@ def nvrtcGetNumSupportedArchs(): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, numArchs) -{{endif}} - -{{if 'nvrtcGetSupportedArchs' in found_functions}} @cython.embedsignature(True) def nvrtcGetSupportedArchs(): @@ -230,9 +209,6 @@ def nvrtcGetSupportedArchs(): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, supportedArchs) -{{endif}} - -{{if 'nvrtcCreateProgram' in found_functions}} @cython.embedsignature(True) def nvrtcCreateProgram(char* src, char* name, int numHeaders, headers : Optional[tuple[bytes] | list[bytes]], includeNames : Optional[tuple[bytes] | list[bytes]]): @@ -288,9 +264,6 @@ def nvrtcCreateProgram(char* src, char* name, int numHeaders, headers : Optional if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, prog) -{{endif}} - -{{if 'nvrtcDestroyProgram' in found_functions}} @cython.embedsignature(True) def nvrtcDestroyProgram(prog): @@ -324,9 +297,6 @@ def nvrtcDestroyProgram(prog): with nogil: err = cynvrtc.nvrtcDestroyProgram(cyprog) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcCompileProgram' in found_functions}} @cython.embedsignature(True) def nvrtcCompileProgram(prog, int numOptions, options : Optional[tuple[bytes] | list[bytes]]): @@ -374,9 +344,6 @@ def nvrtcCompileProgram(prog, int numOptions, options : Optional[tuple[bytes] | with nogil: err = cynvrtc.nvrtcCompileProgram(cyprog, numOptions, cyoptions.data()) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetPTXSize' in found_functions}} @cython.embedsignature(True) def nvrtcGetPTXSize(prog): @@ -414,9 +381,6 @@ def nvrtcGetPTXSize(prog): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, ptxSizeRet) -{{endif}} - -{{if 'nvrtcGetPTX' in found_functions}} @cython.embedsignature(True) def nvrtcGetPTX(prog, char* ptx): @@ -451,9 +415,6 @@ def nvrtcGetPTX(prog, char* ptx): with nogil: err = cynvrtc.nvrtcGetPTX(cyprog, ptx) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetCUBINSize' in found_functions}} @cython.embedsignature(True) def nvrtcGetCUBINSize(prog): @@ -491,9 +452,6 @@ def nvrtcGetCUBINSize(prog): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, cubinSizeRet) -{{endif}} - -{{if 'nvrtcGetCUBIN' in found_functions}} @cython.embedsignature(True) def nvrtcGetCUBIN(prog, char* cubin): @@ -528,9 +486,6 @@ def nvrtcGetCUBIN(prog, char* cubin): with nogil: err = cynvrtc.nvrtcGetCUBIN(cyprog, cubin) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetLTOIRSize' in found_functions}} @cython.embedsignature(True) def nvrtcGetLTOIRSize(prog): @@ -568,9 +523,6 @@ def nvrtcGetLTOIRSize(prog): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, LTOIRSizeRet) -{{endif}} - -{{if 'nvrtcGetLTOIR' in found_functions}} @cython.embedsignature(True) def nvrtcGetLTOIR(prog, char* LTOIR): @@ -605,9 +557,6 @@ def nvrtcGetLTOIR(prog, char* LTOIR): with nogil: err = cynvrtc.nvrtcGetLTOIR(cyprog, LTOIR) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetOptiXIRSize' in found_functions}} @cython.embedsignature(True) def nvrtcGetOptiXIRSize(prog): @@ -645,9 +594,6 @@ def nvrtcGetOptiXIRSize(prog): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, optixirSizeRet) -{{endif}} - -{{if 'nvrtcGetOptiXIR' in found_functions}} @cython.embedsignature(True) def nvrtcGetOptiXIR(prog, char* optixir): @@ -682,9 +628,6 @@ def nvrtcGetOptiXIR(prog, char* optixir): with nogil: err = cynvrtc.nvrtcGetOptiXIR(cyprog, optixir) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetProgramLogSize' in found_functions}} @cython.embedsignature(True) def nvrtcGetProgramLogSize(prog): @@ -725,9 +668,6 @@ def nvrtcGetProgramLogSize(prog): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, logSizeRet) -{{endif}} - -{{if 'nvrtcGetProgramLog' in found_functions}} @cython.embedsignature(True) def nvrtcGetProgramLog(prog, char* log): @@ -762,9 +702,6 @@ def nvrtcGetProgramLog(prog, char* log): with nogil: err = cynvrtc.nvrtcGetProgramLog(cyprog, log) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcAddNameExpression' in found_functions}} @cython.embedsignature(True) def nvrtcAddNameExpression(prog, char* name_expression): @@ -804,9 +741,6 @@ def nvrtcAddNameExpression(prog, char* name_expression): with nogil: err = cynvrtc.nvrtcAddNameExpression(cyprog, name_expression) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetLoweredName' in found_functions}} @cython.embedsignature(True) def nvrtcGetLoweredName(prog, char* name_expression): @@ -849,9 +783,6 @@ def nvrtcGetLoweredName(prog, char* name_expression): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, lowered_name if lowered_name != NULL else None) -{{endif}} - -{{if 'nvrtcGetPCHHeapSize' in found_functions}} @cython.embedsignature(True) def nvrtcGetPCHHeapSize(): @@ -871,9 +802,6 @@ def nvrtcGetPCHHeapSize(): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, ret) -{{endif}} - -{{if 'nvrtcSetPCHHeapSize' in found_functions}} @cython.embedsignature(True) def nvrtcSetPCHHeapSize(size_t size): @@ -896,9 +824,6 @@ def nvrtcSetPCHHeapSize(size_t size): with nogil: err = cynvrtc.nvrtcSetPCHHeapSize(size) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetPCHCreateStatus' in found_functions}} @cython.embedsignature(True) def nvrtcGetPCHCreateStatus(prog): @@ -944,9 +869,6 @@ def nvrtcGetPCHCreateStatus(prog): with nogil: err = cynvrtc.nvrtcGetPCHCreateStatus(cyprog) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetPCHHeapSizeRequired' in found_functions}} @cython.embedsignature(True) def nvrtcGetPCHHeapSizeRequired(prog): @@ -981,9 +903,6 @@ def nvrtcGetPCHHeapSizeRequired(prog): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, size) -{{endif}} - -{{if 'nvrtcSetFlowCallback' in found_functions}} @cython.embedsignature(True) def nvrtcSetFlowCallback(prog, callback, payload): @@ -1044,13 +963,10 @@ def nvrtcSetFlowCallback(prog, callback, payload): _helper_input_void_ptr_free(&cycallbackHelper) _helper_input_void_ptr_free(&cypayloadHelper) return (_nvrtcResult(err),) -{{endif}} - -{{if 'nvrtcGetTileIRSize' in found_functions}} @cython.embedsignature(True) def nvrtcGetTileIRSize(prog): - """ + """ Parameters ---------- @@ -1078,13 +994,10 @@ def nvrtcGetTileIRSize(prog): if err != cynvrtc.NVRTC_SUCCESS: return (_nvrtcResult(err), None) return (_nvrtcResult_SUCCESS, TileIRSizeRet) -{{endif}} - -{{if 'nvrtcGetTileIR' in found_functions}} @cython.embedsignature(True) def nvrtcGetTileIR(prog, char* TileIR): - """ + """ Parameters ---------- @@ -1109,7 +1022,6 @@ def nvrtcGetTileIR(prog, char* TileIR): with nogil: err = cynvrtc.nvrtcGetTileIR(cyprog, TileIR) return (_nvrtcResult(err),) -{{endif}} @cython.embedsignature(True) def sizeof(objType): @@ -1125,7 +1037,7 @@ def sizeof(objType): lowered_name : int The size of `objType` in bytes """ - {{if 'nvrtcProgram' in found_types}} + if objType == nvrtcProgram: - return sizeof(cynvrtc.nvrtcProgram){{endif}} + return sizeof(cynvrtc.nvrtcProgram) raise TypeError("Unknown type: " + str(objType)) From b4279192d4d56498555f514b0ebbaa527bdbc7f4 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Mon, 20 Apr 2026 19:16:27 -0400 Subject: [PATCH 022/244] Privatize system.Device helper classes (#1942) Remove helper/data-container classes from cuda.core.system's public API (__all__) since they are not intended to be directly instantiated by users. These classes are returned by Device properties/methods and serve as nested data containers. Classes removed from __all__: BAR1MemoryInfo, ClockInfo, ClockOffsets, CoolerInfo, DeviceAttributes, DeviceEvents, EventData, FanInfo, FieldValue, FieldValues, GpuDynamicPstatesInfo, GpuDynamicPstatesUtilization, InforomInfo, PciInfo, RepairStatus, Temperature, ThermalSensor, ThermalSettings. Enums, exceptions, Device itself, and free functions remain public. Also: - Update api.rst to document helper classes via their private module path (system._device.ClassName) - Update Device property/method docstrings to link to helper classes using :obj:`~ClassName` role - Update tests to import helper classes from _device module --- cuda_core/cuda/core/system/_clock.pxi | 2 +- cuda_core/cuda/core/system/_device.pyx | 74 ++++++++----------- cuda_core/cuda/core/system/_system_events.pyx | 20 +++-- cuda_core/cuda/core/system/_temperature.pxi | 2 +- cuda_core/docs/source/api.rst | 27 +------ cuda_core/docs/source/api_private.rst | 33 +++++++++ cuda_core/tests/system/test_system_device.py | 28 +++---- 7 files changed, 93 insertions(+), 93 deletions(-) diff --git a/cuda_core/cuda/core/system/_clock.pxi b/cuda_core/cuda/core/system/_clock.pxi index 89e9020f4c9..5d08d2a5676 100644 --- a/cuda_core/cuda/core/system/_clock.pxi +++ b/cuda_core/cuda/core/system/_clock.pxi @@ -129,7 +129,7 @@ cdef class ClockInfo: Returns ------- - ClockOffsets + :obj:`~_device.ClockOffsets` An object with the min, max and current clock offset. """ return ClockOffsets(nvml.device_get_clock_offsets(self._handle, self._clock_type, pstate)) diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index f661c4e685b..23fcf81e92c 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -159,7 +159,7 @@ cdef class Device: @property def arch(self) -> DeviceArch: """ - Device architecture. + :obj:`~DeviceArch` device architecture. For example, a Tesla V100 will report ``DeviceArchitecture.name == "VOLTA"``, and RTX A6000 will report ``DeviceArchitecture.name == @@ -177,7 +177,7 @@ cdef class Device: @property def brand(self) -> BrandType: """ - Brand of the device + :obj:`~BrandType` brand of the device """ return BrandType(nvml.device_get_brand(self._handle)) @@ -289,7 +289,7 @@ cdef class Device: Returns ------- - Iterator of Device + Iterator over :obj:`~Device` An iterator over available devices. """ for device_id in range(nvml.device_get_count_v2()): @@ -301,7 +301,7 @@ cdef class Device: @property def addressing_mode(self) -> AddressingMode: """ - Get the addressing mode of the device. + Get the :obj:`~AddressingMode` of the device. Addressing modes can be one of: @@ -334,7 +334,7 @@ cdef class Device: Returns ------- - Iterator of Device + Iterator of :obj:`~Device` An iterator over available devices. """ cdef Device device @@ -411,7 +411,7 @@ cdef class Device: def clock(self, clock_type: ClockType) -> ClockInfo: """ - Get information about and manage a specific clock on a device. + :obj:`~_device.ClockInfo` object to get information about and manage a specific clock on a device. """ return ClockInfo(self._handle, clock_type) @@ -442,7 +442,7 @@ cdef class Device: def get_current_clock_event_reasons(self) -> list[ClocksEventReasons]: """ - Retrieves the current clocks event reasons. + Retrieves the current :obj:`~ClocksEventReasons`. For all fully supported products. """ @@ -452,7 +452,7 @@ cdef class Device: def get_supported_clock_event_reasons(self) -> list[ClocksEventReasons]: """ - Retrieves supported clocks event reasons that can be returned by + Retrieves supported :obj:`~ClocksEventReasons` that can be returned by :meth:`get_current_clock_event_reasons`. For all fully supported products. @@ -470,7 +470,7 @@ cdef class Device: @property def cooler(self) -> CoolerInfo: """ - Get information about cooler on a device. + :obj:`~_device.CoolerInfo` object with cooler information for the device. """ return CoolerInfo(nvml.device_get_cooler_info(self._handle)) @@ -481,7 +481,7 @@ cdef class Device: @property def attributes(self) -> DeviceAttributes: """ - Get various device attributes. + :obj:`~_device.DeviceAttributes` object with various device attributes. For Ampere™ or newer fully supported devices. Only available on Linux systems. @@ -549,9 +549,9 @@ cdef class Device: Returns ------- - :class:`DeviceEvents` + :obj:`~_device.DeviceEvents` An object representing the registered events. Call - :meth:`DeviceEvents.wait` on this object to wait for events. + :meth:`~_device.DeviceEvents.wait` on this object to wait for events. Raises ------ @@ -582,7 +582,7 @@ cdef class Device: def fan(self, fan: int = 0) -> FanInfo: """ - Get information and manage a specific fan on a device. + :obj:`~_device.FanInfo` object to get information and manage a specific fan on a device. """ if fan < 0 or fan >= self.num_fans: raise ValueError(f"Fan index {fan} is out of range [0, {self.num_fans})") @@ -605,14 +605,14 @@ cdef class Device: Each value specified can raise its own exception. That exception will be raised when attempting to access the corresponding ``value`` from the - returned :class:`FieldValues` container. + returned :obj:`~_device.FieldValues` container. To confirm that there are no exceptions in the entire container, call - :meth:`FieldValues.validate`. + :meth:`~_device.FieldValues.validate`. Parameters ---------- - field_ids: list of int or tuple of (int, int) + field_ids: list[int | tuple[int, int]] List of field IDs to query. Each item may be either a single value from the :class:`FieldId` @@ -620,7 +620,7 @@ cdef class Device: Returns ------- - :class:`FieldValues` + :obj:`~_device.FieldValues` Container of field values corresponding to the requested field IDs. """ return FieldValues(nvml.device_get_field_values(self._handle, field_ids)) @@ -631,7 +631,7 @@ cdef class Device: Parameters ---------- - field_ids: list of int or tuple of (int, int) + field_ids: list[int | tuple[int, int]] List of field IDs to clear. Each item may be either a single value from the :class:`FieldId` @@ -646,7 +646,7 @@ cdef class Device: @property def inforom(self) -> InforomInfo: """ - Accessor for InfoROM information. + :obj:`~_device.InforomInfo` object with InfoROM information. For all products with an InfoROM. """ @@ -659,7 +659,7 @@ cdef class Device: @property def bar1_memory_info(self) -> BAR1MemoryInfo: """ - Get information about BAR1 memory. + :obj:`~_device.BAR1MemoryInfo` object with BAR1 memory information. BAR1 is used to map the FB (device memory) so that it can be directly accessed by the CPU or by 3rd party devices (peer-to-peer on the PCIE @@ -670,7 +670,7 @@ cdef class Device: @property def memory_info(self) -> MemoryInfo: """ - Object with memory information. + :obj:`~_device.MemoryInfo` object with memory information. """ return MemoryInfo(nvml.device_get_memory_info_v2(self._handle)) @@ -681,7 +681,7 @@ cdef class Device: @property def pci_info(self) -> PciInfo: """ - The PCI attributes of this device. + :obj:`~_device.PciInfo` object with the PCI attributes of this device. """ return PciInfo(nvml.device_get_pci_info_ext(self._handle), self._handle) @@ -703,7 +703,7 @@ cdef class Device: @property def dynamic_pstates_info(self) -> GpuDynamicPstatesInfo: """ - Retrieve performance monitor samples from the associated subdevice. + :obj:`~_device.GpuDynamicPstatesInfo` object with performance monitor samples from the associated subdevice. """ return GpuDynamicPstatesInfo(nvml.device_get_dynamic_pstates_info(self._handle)) @@ -713,6 +713,11 @@ cdef class Device: The returned list contains a contiguous list of valid P-States supported by the device. + + Return + ------ + list[Pstates] + A list of supported P-States for the device. """ return [Pstates(x) for x in nvml.device_get_supported_performance_states(self._handle)] @@ -723,7 +728,7 @@ cdef class Device: @property def repair_status(self) -> RepairStatus: """ - Get the repair status for TPC/Channel repair. + :obj:`~_device.RepairStatus` object with TPC/Channel repair status. For Ampere™ or newer fully supported devices. """ @@ -736,7 +741,7 @@ cdef class Device: @property def temperature(self) -> Temperature: """ - Get information about temperatures on a device. + :obj:`~_device.Temperature` object with temperature information for the device. """ return Temperature(self._handle) @@ -822,46 +827,27 @@ def get_p2p_status(device1: Device, device2: Device, index: GpuP2PCapsIndex) -> __all__ = [ "AddressingMode", "AffinityScope", - "BAR1MemoryInfo", "BrandType", "ClockId", - "ClockInfo", - "ClockOffsets", "ClocksEventReasons", "ClockType", "CoolerControl", - "CoolerInfo", "CoolerTarget", "Device", "DeviceArch", - "DeviceAttributes", - "DeviceEvents", - "EventData", "EventType", "FanControlPolicy", - "FanInfo", "FieldId", - "FieldValue", - "FieldValues", "get_p2p_status", "get_topology_common_ancestor", - "GpuDynamicPstatesInfo", - "GpuDynamicPstatesUtilization", "GpuP2PCapsIndex", "GpuP2PStatus", "GpuTopologyLevel", - "InforomInfo", "InforomObject", - "MemoryInfo", "PcieUtilCounter", - "PciInfo", "Pstates", - "RepairStatus", - "Temperature", "TemperatureSensors", "TemperatureThresholds", "ThermalController", - "ThermalSensor", - "ThermalSettings", "ThermalTarget", ] diff --git a/cuda_core/cuda/core/system/_system_events.pyx b/cuda_core/cuda/core/system/_system_events.pyx index d8a64b619b7..81f69d872af 100644 --- a/cuda_core/cuda/core/system/_system_events.pyx +++ b/cuda_core/cuda/core/system/_system_events.pyx @@ -26,7 +26,7 @@ cdef class SystemEvent: @property def event_type(self) -> SystemEventType: """ - The type of event that was triggered. + The :obj:`~SystemEventType` that was triggered. """ return SystemEventType(self._event_data.event_type) @@ -40,7 +40,7 @@ cdef class SystemEvent: @property def device(self) -> _device.Device: """ - The device associated with this event. + The :obj:`~_device.Device` associated with this event. """ return _device.Device(pci_bus_id=self.gpu_id) @@ -56,6 +56,9 @@ cdef class SystemEvents: return len(self._event_data) def __getitem__(self, idx: int) -> SystemEvent: + """ + Get the :obj:`~_system_events.SystemEvent` at the specified index. + """ return SystemEvent(self._event_data[idx]) @@ -107,6 +110,12 @@ cdef class RegisteredSystemEvents: buffer_size: int The maximum number of events to retrieve. Must be at least 1. + Returns + ------- + :obj:`~_system_events.SystemEvents` + A set of events that were received. The number of events returned may + be less than the specified buffer size if fewer events were available. + Raises ------ :class:`cuda.core.system.TimeoutError` @@ -142,9 +151,9 @@ def register_events(events: SystemEventType | int | list[SystemEventType | int]) Returns ------- - :class:`RegisteredSystemEvents` + :obj:`~_system_events.RegisteredSystemEvents` An object representing the registered events. Call - :meth:`RegisteredSystemEvents.wait` on this object to wait for events. + :meth:`~_system_events.RegisteredSystemEvents.wait` on this object to wait for events. Raises ------ @@ -156,8 +165,5 @@ def register_events(events: SystemEventType | int | list[SystemEventType | int]) __all__ = [ "register_events", - "RegisteredSystemEvents", - "SystemEvent", - "SystemEvents", "SystemEventType", ] diff --git a/cuda_core/cuda/core/system/_temperature.pxi b/cuda_core/cuda/core/system/_temperature.pxi index c56eb719d18..8f8e10a570b 100644 --- a/cuda_core/cuda/core/system/_temperature.pxi +++ b/cuda_core/cuda/core/system/_temperature.pxi @@ -140,7 +140,7 @@ cdef class Temperature: Returns ------- - :class:`ThermalSettings` + :obj:`~_device.ThermalSettings` The thermal settings for the specified sensor. """ return ThermalSettings(nvml.device_get_thermal_settings(self._handle, sensor_index)) diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 005866ddb2d..8bd3638da0e 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -199,9 +199,6 @@ Events :toctree: generated/ system.register_events - system.RegisteredSystemEvents - system.SystemEvent - system.SystemEvents system.SystemEventType Enums @@ -215,6 +212,7 @@ Enums system.BrandType system.ClockId system.ClocksEventReasons + system.ClockType system.CoolerControl system.CoolerTarget system.DeviceArch @@ -238,29 +236,6 @@ Types :template: autosummary/cyclass.rst system.Device - system.BAR1MemoryInfo - system.ClockInfo - system.ClockOffsets - system.ClockType - system.CoolerInfo - system.DeviceAttributes - system.DeviceEvents - system.EventData - system.FanInfo - system.FieldValue - system.FieldValues - system.GpuDynamicPstatesInfo - system.GpuDynamicPstatesUtilization - system.GpuP2PCapsIndex - system.GpuP2PStatus - system.GpuTopologyLevel - system.InforomInfo - system.MemoryInfo - system.PciInfo - system.RepairStatus - system.Temperature - system.ThermalSensor - system.ThermalSettings .. module:: cuda.core.utils diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index becd1746ccb..de3e6bf77f7 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -51,3 +51,36 @@ CUDA protocols :template: protocol.rst typing.IsStreamT + +NVML +---- + +.. autosummary:: + :toctree: generated/ + :template: autosummary/cyclass.rst + + system._device.BAR1MemoryInfo + system._device.ClockInfo + system._device.ClockOffsets + system._device.CoolerInfo + system._device.DeviceAttributes + system._device.DeviceEvents + system._device.EventData + system._device.FanInfo + system._device.FieldValue + system._device.FieldValues + system._device.GpuDynamicPstatesInfo + system._device.GpuDynamicPstatesUtilization + system._device.GpuP2PCapsIndex + system._device.GpuP2PStatus + system._device.GpuTopologyLevel + system._device.InforomInfo + system._device.MemoryInfo + system._device.PciInfo + system._device.RepairStatus + system._device.Temperature + system._device.ThermalSensor + system._device.ThermalSettings + system._system_events.RegisteredSystemEvents + system._system_events.SystemEvent + system._system_events.SystemEvents diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index 2a094d82119..85a541018da 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -83,7 +83,7 @@ def test_device_bar1_memory(): bar1_memory_info.used, ) - assert isinstance(bar1_memory_info, system.BAR1MemoryInfo) + assert isinstance(bar1_memory_info, _device.BAR1MemoryInfo) assert isinstance(free, int) assert isinstance(total, int) assert isinstance(used, int) @@ -140,7 +140,7 @@ def test_device_memory(): memory_info = device.memory_info free, total, used, reserved = memory_info.free, memory_info.total, memory_info.used, memory_info.reserved - assert isinstance(memory_info, system.MemoryInfo) + assert isinstance(memory_info, _device.MemoryInfo) assert isinstance(free, int) assert isinstance(total, int) assert isinstance(used, int) @@ -163,7 +163,7 @@ def test_device_name(): def test_device_pci_info(): for device in system.Device.get_all_devices(): pci_info = device.pci_info - assert isinstance(pci_info, system.PciInfo) + assert isinstance(pci_info, _device.PciInfo) assert isinstance(pci_info.bus_id, str) assert re.match("[a-f0-9]{8}:[a-f0-9]{2}:[a-f0-9]{2}.[a-f0-9]", pci_info.bus_id.lower()) @@ -317,7 +317,7 @@ def test_device_attributes(): # that's not the case. with unsupported_before(device, None): attributes = device.attributes - assert isinstance(attributes, system.DeviceAttributes) + assert isinstance(attributes, _device.DeviceAttributes) assert isinstance(attributes.multiprocessor_count, int) assert attributes.multiprocessor_count > 0 @@ -371,7 +371,7 @@ def test_field_values(): with pytest.raises(TypeError): field_values["invalid_index"] - assert isinstance(field_values, system.FieldValues) + assert isinstance(field_values, _device.FieldValues) assert len(field_values) == len(field_ids) raw_values = field_values.get_all_values() @@ -453,7 +453,7 @@ def test_repair_status(): # this seems to also work on some TURING systems. with unsupported_before(device, None): repair_status = device.repair_status - assert isinstance(repair_status, system.RepairStatus) + assert isinstance(repair_status, _device.RepairStatus) assert isinstance(repair_status.channel_repair_pending, bool) assert isinstance(repair_status.tpc_repair_pending, bool) @@ -557,7 +557,7 @@ def test_clock(): for device in system.Device.get_all_devices(): for clock_type in system.ClockType: clock = device.clock(clock_type) - assert isinstance(clock, system.ClockInfo) + assert isinstance(clock, _device.ClockInfo) # These are ordered from oldest API to newest API so we test as much # as we can on each hardware architecture. @@ -589,7 +589,7 @@ def test_clock(): except (system.InvalidArgumentError, system.NotFoundError): pass else: - assert isinstance(offsets, system.ClockOffsets) + assert isinstance(offsets, _device.ClockOffsets) assert isinstance(offsets.clock_offset_mhz, int) assert isinstance(offsets.max_offset_mhz, int) assert isinstance(offsets.min_offset_mhz, int) @@ -622,7 +622,7 @@ def test_fan(): for fan_idx in range(device.num_fans): fan_info = device.fan(fan_idx) - assert isinstance(fan_info, system.FanInfo) + assert isinstance(fan_info, _device.FanInfo) speed = fan_info.speed assert isinstance(speed, int) @@ -663,7 +663,7 @@ def test_cooler(): with unsupported_before(device, DeviceArch.MAXWELL): cooler_info = device.cooler - assert isinstance(cooler_info, system.CoolerInfo) + assert isinstance(cooler_info, _device.CoolerInfo) signal_type = cooler_info.signal_type assert isinstance(signal_type, system.CoolerControl) @@ -675,7 +675,7 @@ def test_cooler(): def test_temperature(): for device in system.Device.get_all_devices(): temperature = device.temperature - assert isinstance(temperature, system.Temperature) + assert isinstance(temperature, _device.Temperature) sensor = temperature.sensor() assert isinstance(sensor, int) @@ -696,10 +696,10 @@ def test_temperature(): with unsupported_before(device, None): thermals = temperature.thermal_settings(system.ThermalTarget.ALL) - assert isinstance(thermals, system.ThermalSettings) + assert isinstance(thermals, _device.ThermalSettings) for i, sensor in enumerate(thermals): - assert isinstance(sensor, system.ThermalSensor) + assert isinstance(sensor, _device.ThermalSensor) assert isinstance(sensor.target, system.ThermalTarget) assert isinstance(sensor.controller, system.ThermalController) assert isinstance(sensor.default_min_temp, int) @@ -720,7 +720,7 @@ def test_pstates(): assert all(isinstance(p, system.Pstates) for p in pstates) dynamic_pstates_info = device.dynamic_pstates_info - assert isinstance(dynamic_pstates_info, system.GpuDynamicPstatesInfo) + assert isinstance(dynamic_pstates_info, _device.GpuDynamicPstatesInfo) assert len(dynamic_pstates_info) == nvml.MAX_GPU_UTILIZATIONS From b3ae845cf4ee0a983f90a2a3decfff5267dcc836 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:00:35 -0400 Subject: [PATCH 023/244] CI: Test sdist completeness for all user-facing packages on Linux and Windows (#1909) * CI: Test sdist builds for all user-facing packages Add a new test-sdist workflow that builds an sdist and then a wheel-from-sdist for each of the 4 user-facing packages (cuda_pathfinder, cuda_python, cuda_bindings, cuda_core). This catches regressions in MANIFEST.in or package-data configuration that could silently break sdist-based builds. Closes #1599 Co-Authored-By: Claude Opus 4.6 (1M context) * fix: skip test-sdist job on doc-only PRs Match existing test jobs' doc-only guard so docs-only PRs don't run source-build validation. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: point cuda_bindings sdist build at local cuda_pathfinder wheel cuda_bindings build backend imports cuda.pathfinder, so pip's build isolation needs to find the locally-built cuda_pathfinder wheel instead of pulling from PyPI. Set PIP_FIND_LINKS to the pathfinder dist directory. Co-Authored-By: Claude Opus 4.6 (1M context) * Add actionlint config for self-hosted runner labels actionlint rejects non-GitHub-hosted runner labels unless declared in .github/actionlint.yaml. This was causing pre-commit.ci failures on test-sdist.yml which uses linux-amd64-cpu8. Co-Authored-By: Claude Opus 4.6 (1M context) * ci: add Windows sdist completeness test alongside Linux Split test-sdist.yml into test-sdist-linux.yml (renamed) and a new test-sdist-windows.yml sibling, mirroring the test-wheel-linux/-windows pattern. cuda_bindings/build_hooks.py selects platform-specific *.pyx variants (_linux.pyx on Linux, _windows.pyx on Windows) at build time, so a Linux-only sdist test cannot prove the sdist ships the Windows sources required to build a wheel-from-sdist on Windows. The Windows variant uses the repo's windows-2022 + ilammy/msvc-dev-cmd setup already used by build-wheel.yml, omits sccache and nv-gha-runners/setup-proxy-cache (both limited to Linux in build-wheel by convention), and otherwise mirrors the Linux steps. ci.yml now invokes both workflows (test-sdist-linux, test-sdist-windows) and the checks aggregator fails if either is cancelled or failed. Addresses review feedback from PR #1909. * ci: normalize Windows PIP_FIND_LINKS via cygpath Convert the find-links paths to native Windows style with cygpath -w so they match the convention used in build-wheel.yml's Windows CIBW environment. This avoids ambiguity when pip splits PIP_FIND_LINKS on spaces and is asked to resolve a mix of path styles. --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .github/actionlint.yaml | 9 ++ .github/workflows/ci.yml | 39 +++++++++ .github/workflows/test-sdist-linux.yml | 106 +++++++++++++++++++++++ .github/workflows/test-sdist-windows.yml | 92 ++++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/test-sdist-linux.yml create mode 100644 .github/workflows/test-sdist-windows.yml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000000..af378f65b53 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Self-hosted runner labels used in CI workflows. +# Without this config, actionlint rejects non-GitHub-hosted labels. +self-hosted-runner: + labels: + - linux-amd64-cpu8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f575d2f851e..292263f0c44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -286,6 +286,37 @@ jobs: cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + # NOTE: test-sdist jobs are split by platform (mirroring build-* and test-wheel-*) + # so platform-specific sources (e.g. cuda_bindings/*_windows.pyx selected by + # build_hooks.py) are exercised on their target OS. Keep these job definitions + # textually identical except for: + # - host-platform value + # - uses: (test-sdist-linux.yml vs test-sdist-windows.yml) + test-sdist-linux: + needs: + - ci-vars + - should-skip + name: Test sdist linux-64 + if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }} + secrets: inherit + uses: ./.github/workflows/test-sdist-linux.yml + with: + host-platform: linux-64 + cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + + # See test-sdist-linux for why sdist test jobs are split by platform. + test-sdist-windows: + needs: + - ci-vars + - should-skip + name: Test sdist win-64 + if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }} + secrets: inherit + uses: ./.github/workflows/test-sdist-windows.yml + with: + host-platform: win-64 + cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + # NOTE: Test jobs are split by platform for the same reason as build jobs (see # build-linux-64). Keep these job definitions textually identical except for: # - host-platform value @@ -388,6 +419,8 @@ jobs: needs: - should-skip - detect-changes + - test-sdist-linux + - test-sdist-windows - test-linux-64 - test-linux-aarch64 - test-windows @@ -427,6 +460,12 @@ jobs: if ${{ needs.doc.result == 'cancelled' || needs.doc.result == 'failure' }}; then exit 1 fi + if ${{ needs.test-sdist-linux.result == 'cancelled' || + needs.test-sdist-linux.result == 'failure' || + needs.test-sdist-windows.result == 'cancelled' || + needs.test-sdist-windows.result == 'failure' }}; then + exit 1 + fi if [[ "${doc_only}" != "true" ]]; then if ${{ needs.test-linux-64.result == 'cancelled' || needs.test-linux-64.result == 'failure' || diff --git a/.github/workflows/test-sdist-linux.yml b/.github/workflows/test-sdist-linux.yml new file mode 100644 index 00000000000..49a68877cc0 --- /dev/null +++ b/.github/workflows/test-sdist-linux.yml @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +on: + workflow_call: + inputs: + host-platform: + required: true + type: string + cuda-version: + required: true + type: string + +defaults: + run: + shell: bash --noprofile --norc -xeuo pipefail {0} + +permissions: + contents: read # This is required for actions/checkout + +jobs: + test-sdist: + name: Test sdist builds + runs-on: linux-amd64-cpu8 + steps: + - name: Checkout ${{ github.event.repository.name }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Install build tools + run: pip install build + + # Pure Python packages -- no CTK needed. + - name: Build cuda.pathfinder sdist and wheel-from-sdist + run: | + python -m build --sdist cuda_pathfinder/ + pip wheel --no-deps --wheel-dir cuda_pathfinder/dist cuda_pathfinder/dist/*.tar.gz + + - name: Build cuda-python sdist and wheel-from-sdist + run: | + python -m build --sdist cuda_python/ + pip wheel --no-deps --wheel-dir cuda_python/dist cuda_python/dist/*.tar.gz + + # Cython packages need CTK + sccache. + # The env vars ACTIONS_CACHE_SERVICE_V2, ACTIONS_RESULTS_URL, and ACTIONS_RUNTIME_TOKEN + # are exposed by this action. + - name: Enable sccache + uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # 0.0.9 + with: + disable_annotations: 'true' + + # xref: https://github.com/orgs/community/discussions/42856#discussioncomment-7678867 + - name: Adding additional GHA cache-related env vars + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL']) + core.exportVariable('ACTIONS_RUNTIME_URL', process.env['ACTIONS_RUNTIME_URL']) + + - name: Setup proxy cache + uses: nv-gha-runners/setup-proxy-cache@main + continue-on-error: true + with: + enable-apt: true + + - name: Set up mini CTK + uses: ./.github/actions/fetch_ctk + continue-on-error: false + with: + host-platform: ${{ inputs.host-platform }} + cuda-version: ${{ inputs.cuda-version }} + + # cuda_bindings/setup.py parses CUDA headers at import time, so CUDA_PATH + # (set by fetch_ctk) must be available for both sdist and wheel builds. + - name: Build cuda.bindings sdist and wheel-from-sdist + run: | + export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc) + export CC="sccache cc" + export CXX="sccache c++" + export PIP_FIND_LINKS="$(pwd)/cuda_pathfinder/dist" + python -m build --sdist cuda_bindings/ + pip wheel --no-deps --wheel-dir cuda_bindings/dist cuda_bindings/dist/*.tar.gz + + # cuda_core sdist delegates to setuptools (no CTK needed), but + # wheel-from-sdist needs CTK and cuda-bindings (dynamic build dep via + # get_requires_for_build_wheel in build_hooks.py). + - name: Build cuda.core sdist and wheel-from-sdist + run: | + export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc) + export CUDA_CORE_BUILD_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)" + export CC="sccache cc" + export CXX="sccache c++" + export PIP_FIND_LINKS="$(pwd)/cuda_bindings/dist $(pwd)/cuda_pathfinder/dist" + python -m build --sdist cuda_core/ + pip wheel --no-deps --wheel-dir cuda_core/dist cuda_core/dist/*.tar.gz + + - name: Show sccache stats + if: always() + run: sccache --show-stats diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml new file mode 100644 index 00000000000..9661793190c --- /dev/null +++ b/.github/workflows/test-sdist-windows.yml @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Windows counterpart of test-sdist-linux.yml. Exists so that platform-gated +# sources (e.g. cuda_bindings/*_windows.pyx selected by build_hooks.py) are +# exercised by the sdist completeness test. Keep the shape of this file in +# sync with test-sdist-linux.yml aside from Windows-specific setup (MSVC, +# no sccache, no proxy cache). + +on: + workflow_call: + inputs: + host-platform: + required: true + type: string + cuda-version: + required: true + type: string + +defaults: + run: + shell: bash --noprofile --norc -xeuo pipefail {0} + +permissions: + contents: read # This is required for actions/checkout + +jobs: + test-sdist: + name: Test sdist builds + runs-on: windows-2022 + steps: + - name: Checkout ${{ github.event.repository.name }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Set up MSVC + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 + + - name: Install build tools + run: pip install build + + # Pure Python packages -- no CTK needed. + - name: Build cuda.pathfinder sdist and wheel-from-sdist + run: | + python -m build --sdist cuda_pathfinder/ + pip wheel --no-deps --wheel-dir cuda_pathfinder/dist cuda_pathfinder/dist/*.tar.gz + + - name: Build cuda-python sdist and wheel-from-sdist + run: | + python -m build --sdist cuda_python/ + pip wheel --no-deps --wheel-dir cuda_python/dist cuda_python/dist/*.tar.gz + + # Cython packages need CTK. No sccache on Windows (this is a correctness + # smoke test, not a production build; see build-wheel.yml which also + # limits sccache to Linux). + - name: Set up mini CTK + uses: ./.github/actions/fetch_ctk + continue-on-error: false + with: + host-platform: ${{ inputs.host-platform }} + cuda-version: ${{ inputs.cuda-version }} + + # cuda_bindings/setup.py parses CUDA headers at import time, so CUDA_PATH + # (set by fetch_ctk) must be available for both sdist and wheel builds. + # PIP_FIND_LINKS is passed as a native Windows path via cygpath because + # pip on Windows treats space-separated entries as separators and is + # picky about mixed path styles (see build-wheel.yml for the same + # convention). + - name: Build cuda.bindings sdist and wheel-from-sdist + run: | + export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc) + export PIP_FIND_LINKS="$(cygpath -w "$(pwd)/cuda_pathfinder/dist")" + python -m build --sdist cuda_bindings/ + pip wheel --no-deps --wheel-dir cuda_bindings/dist cuda_bindings/dist/*.tar.gz + + # cuda_core sdist delegates to setuptools (no CTK needed), but + # wheel-from-sdist needs CTK and cuda-bindings (dynamic build dep via + # get_requires_for_build_wheel in build_hooks.py). + - name: Build cuda.core sdist and wheel-from-sdist + run: | + export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc) + export CUDA_CORE_BUILD_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)" + export PIP_FIND_LINKS="$(cygpath -w "$(pwd)/cuda_bindings/dist") $(cygpath -w "$(pwd)/cuda_pathfinder/dist")" + python -m build --sdist cuda_core/ + pip wheel --no-deps --wheel-dir cuda_core/dist cuda_core/dist/*.tar.gz From e15450091b084723d588345c542002872d5f0128 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Tue, 21 Apr 2026 10:00:53 -0400 Subject: [PATCH 024/244] [no-ci] Add auto-labeling workflow for PRs based on changed paths (#1818) * Add labeler config for auto-labeling PRs by path * Add auto-label workflow using actions/labeler v6 Co-Authored-By: Claude Opus 4.6 (1M context) * Add ci/ folder to CI/CD label mapping * Add SPDX header to labeler.yml to fix pre-commit check * Update labeler.yml --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .github/labeler.yml | 24 ++++++++++++++++++++++++ .github/workflows/pr-auto-label.yml | 23 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/pr-auto-label.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000000..62b46533816 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Configuration for https://github.com/actions/labeler +# Auto-applies labels based on which files a PR changes. + +cuda.bindings: + - changed-files: + - any-glob-to-any-file: 'cuda_bindings/**' + +cuda.core: + - changed-files: + - any-glob-to-any-file: 'cuda_core/**' + +cuda.pathfinder: + - changed-files: + - any-glob-to-any-file: 'cuda_pathfinder/**' + +CI/CD: + - changed-files: + - any-glob-to-any-file: + - '.github/**' + - 'ci/**' diff --git a/.github/workflows/pr-auto-label.yml b/.github/workflows/pr-auto-label.yml new file mode 100644 index 00000000000..7b86455b2d1 --- /dev/null +++ b/.github/workflows/pr-auto-label.yml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: "CI: Auto-label PRs by path" + +on: + pull_request_target: + types: + - opened + - synchronize + +jobs: + auto-label: + name: Auto-label by changed paths + if: github.repository_owner == 'NVIDIA' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Apply labels + uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 From 46818dcd9be6b59849571d48c3b92c5b0e4fcd1a Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Tue, 21 Apr 2026 10:55:26 -0400 Subject: [PATCH 025/244] Alternate half of linux arm64 CI runners from a100 to l4 (#1956) Change every other arm64 entry in the pull-request test matrix to use l4 GPU instead of a100, giving a 50/50 split (9 a100, 9 l4) in an alternating pattern. This reduces pressure on a100 runners while maintaining arm64 coverage across all Python and CUDA version combinations. Co-authored-by: Claude Opus 4.6 (1M context) --- ci/test-matrix.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ci/test-matrix.yml b/ci/test-matrix.yml index 4bcfcf4b232..a402e3e4cf7 100644 --- a/ci/test-matrix.yml +++ b/ci/test-matrix.yml @@ -1,10 +1,10 @@ -# 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 # # Test matrix configurations for CUDA Python CI workflows. This file consolidates # the test matrices that were previously hardcoded in the workflow files. All GPU -# and ARCH values are hard-coded for each architecture: l4 GPU for amd64, a100 GPU -# for arm64. +# and ARCH values are hard-coded for each architecture: various GPUs for amd64, +# alternating a100/l4 for arm64. # # Please keep the matrices sorted in ascending order by the following: # @@ -38,23 +38,23 @@ linux: - { ARCH: 'amd64', PY_VER: '3.14t', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } # linux-aarch64 - { ARCH: 'arm64', PY_VER: '3.10', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.10', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.10', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.10', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.11', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.12', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.12', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.12', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.12', CUDA_VER: '13.2.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.13', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.14', CUDA_VER: '12.9.1', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.14', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.14', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.14', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '12.9.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '13.0.2', LOCAL_CTK: '0', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } - - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'a100', GPU_COUNT: '1', DRIVER: 'latest' } + - { ARCH: 'arm64', PY_VER: '3.14t', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'l4', GPU_COUNT: '1', DRIVER: 'latest' } # special runners - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.0.2', LOCAL_CTK: '1', GPU: 'h100', GPU_COUNT: '1', DRIVER: 'latest' } - { ARCH: 'amd64', PY_VER: '3.13', CUDA_VER: '13.2.1', LOCAL_CTK: '1', GPU: 'h100', GPU_COUNT: '1', DRIVER: 'latest' } From 515b513e718733a336b4d3bf9643ac324b6adc36 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Tue, 21 Apr 2026 14:24:43 -0400 Subject: [PATCH 026/244] Update .gitignore after #1900 (#1958) --- .gitignore | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitignore b/.gitignore index da6748ddbe3..26f13b1d174 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,6 @@ cuda_bindings/cuda/bindings/_bindings/cyruntime.pxd cuda_bindings/cuda/bindings/_bindings/cyruntime.pyx cuda_bindings/cuda/bindings/_bindings/cyruntime_ptds.pxd cuda_bindings/cuda/bindings/_bindings/cyruntime_ptds.pyx -cuda_bindings/cuda/bindings/_bindings/cynvrtc.pxd -cuda_bindings/cuda/bindings/_bindings/cynvrtc.pyx cuda_bindings/cuda/bindings/_internal/_nvml.pyx cuda_bindings/cuda/bindings/_internal/cufile.pyx cuda_bindings/cuda/bindings/_internal/nvfatbin.pyx @@ -42,14 +40,10 @@ cuda_bindings/cuda/bindings/cyruntime.pxd cuda_bindings/cuda/bindings/cyruntime.pyx cuda_bindings/cuda/bindings/cyruntime_functions.pxi cuda_bindings/cuda/bindings/cyruntime_types.pxi -cuda_bindings/cuda/bindings/cynvrtc.pxd -cuda_bindings/cuda/bindings/cynvrtc.pyx cuda_bindings/cuda/bindings/driver.pxd cuda_bindings/cuda/bindings/driver.pyx cuda_bindings/cuda/bindings/runtime.pxd cuda_bindings/cuda/bindings/runtime.pyx -cuda_bindings/cuda/bindings/nvrtc.pxd -cuda_bindings/cuda/bindings/nvrtc.pyx cuda_bindings/cuda/bindings/utils/_get_handle.pyx # Version files from setuptools_scm From b162f6428ebdd0eded864a60b21f303d87be9a0e Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 22 Apr 2026 07:35:31 -0400 Subject: [PATCH 027/244] cuda.core.system: Add MIG-related APIs (#1916) * cuda.core.system: Add MIG-related APIs * Add "need" * Add missing file * Make properties * Fix test * Fix test * Elaborate in the docstring * Address comments in PR * Address comments in PR --- cuda_core/cuda/core/system/_device.pyx | 38 ++++- cuda_core/cuda/core/system/_mig.pxi | 166 +++++++++++++++++++ cuda_core/docs/source/api_private.rst | 3 +- cuda_core/tests/system/test_system_device.py | 32 +++- cuda_core/tests/test_device.py | 2 +- 5 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 cuda_core/cuda/core/system/_mig.pxi diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index 23fcf81e92c..27a487bc3f7 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -32,6 +32,7 @@ include "_fan.pxi" include "_field_values.pxi" include "_inforom.pxi" include "_memory.pxi" +include "_mig.pxi" include "_pci_info.pxi" include "_performance.pxi" include "_repair_status.pxi" @@ -132,12 +133,23 @@ cdef class Device: board serial identifier. In the upstream NVML C++ API, the UUID includes a ``gpu-`` or ``mig-`` - prefix. That is not included in ``cuda.core.system``. + prefix. If you need a `uuid` without that prefix (for example, to + interact with CUDA), use the `uuid_without_prefix` property. """ - # NVML UUIDs have a `GPU-` or `MIG-` prefix. We remove that here. + return nvml.device_get_uuid(self._handle) - # TODO: If the user cares about the prefix, we will expose that in the - # future using the MIG-related APIs in NVML. + @property + def uuid_without_prefix(self) -> str: + """ + Retrieves the globally unique immutable UUID associated with this + device, as a 5 part hexadecimal string, that augments the immutable, + board serial identifier. + + In the upstream NVML C++ API, the UUID includes a ``gpu-`` or ``mig-`` + prefix. This property returns it without the prefix, to match the UUIDs + used in CUDA. If you need the prefix, use the `uuid` property. + """ + # NVML UUIDs have a `gpu-` or `mig-` prefix. We remove that here. return nvml.device_get_uuid(self._handle)[4:] @property @@ -265,7 +277,7 @@ cdef class Device: # search all the devices for one with a matching UUID. for cuda_device in CudaDevice.get_all_devices(): - if cuda_device.uuid == self.uuid: + if cuda_device.uuid == self.uuid_without_prefix: return cuda_device raise RuntimeError("No corresponding CUDA device found for this NVML device.") @@ -280,6 +292,8 @@ cdef class Device: int The number of available devices. """ + initialize() + return nvml.device_get_count_v2() @classmethod @@ -292,6 +306,8 @@ cdef class Device: Iterator over :obj:`~Device` An iterator over available devices. """ + initialize() + for device_id in range(nvml.device_get_count_v2()): yield cls(index=device_id) @@ -317,6 +333,18 @@ cdef class Device: """ return AddressingMode(nvml.device_get_addressing_mode(self._handle).value) + ######################################################################### + # MIG (MULTI-INSTANCE GPU) DEVICES + + @property + def mig(self) -> MigInfo: + """ + Get :obj:`~MigInfo` accessor for MIG (Multi-Instance GPU) information. + + For Ampere™ or newer fully supported devices. + """ + return MigInfo(self) + ######################################################################### # AFFINITY diff --git a/cuda_core/cuda/core/system/_mig.pxi b/cuda_core/cuda/core/system/_mig.pxi new file mode 100644 index 00000000000..8fa6b9d780d --- /dev/null +++ b/cuda_core/cuda/core/system/_mig.pxi @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +from typing import Iterable + + +cdef class MigInfo: + cdef Device _device + + def __init__(self, device: Device): + self._device = device + + @property + def is_mig_device(self) -> bool: + """ + Whether this device is a MIG (Multi-Instance GPU) device. + + A MIG device handle is an NVML abstraction which maps to a MIG compute + instance. These overloaded references can be used (with some + restrictions) interchangeably with a GPU device handle to execute + queries at a per-compute instance granularity. + + For Ampere™ or newer fully supported devices. + """ + return bool(nvml.device_is_mig_device_handle(self._device._handle)) + + @property + def mode(self) -> bool: + """ + Get current MIG mode for the device. + + For Ampere™ or newer fully supported devices. + + Changing MIG modes may require device unbind or reset. The "pending" MIG + mode refers to the target mode following the next activation trigger. + + Returns + ------- + bool + `True` if current MIG mode is enabled. + """ + current, _ = nvml.device_get_mig_mode(self._device._handle) + return current == nvml.EnableState.FEATURE_ENABLED + + @mode.setter + def mode(self, mode: bool): + """ + Set the MIG mode for the device. + + For Ampere™ or newer fully supported devices. + + Changing MIG modes may require device unbind or reset. The "pending" MIG + mode refers to the target mode following the next activation trigger. + + Parameters + ---------- + mode: bool + `True` to enable MIG mode, `False` to disable MIG mode. + """ + nvml.device_set_mig_mode( + self._device._handle, + nvml.EnableState.FEATURE_ENABLED if mode else nvml.EnableState.FEATURE_DISABLED + ) + + @property + def pending_mode(self) -> bool: + """ + Get pending MIG mode for the device. + + For Ampere™ or newer fully supported devices. + + Changing MIG modes may require device unbind or reset. The "pending" MIG + mode refers to the target mode following the next activation trigger. + + If the device is not a MIG device, returns `False`. + + Returns + ------- + bool + `True` if pending MIG mode is enabled. + """ + _, pending = nvml.device_get_mig_mode(self._device._handle) + return pending == nvml.EnableState.FEATURE_ENABLED + + @property + def device_count(self) -> int: + """ + Get the maximum number of MIG devices that can exist under this device. + + Returns zero if MIG is not supported or enabled. + + For Ampere™ or newer fully supported devices. + + Returns + ------- + int + The number of MIG devices (compute instances) on this GPU. + """ + return nvml.device_get_max_mig_device_count(self._device._handle) + + @property + def parent(self) -> Device: + """ + For MIG devices, get the parent GPU device. + + For Ampere™ or newer fully supported devices. + + Returns + ------- + Device + The parent GPU device for this MIG device. + """ + parent_handle = nvml.device_get_device_handle_from_mig_device_handle(self._device._handle) + parent_device = Device.__new__(Device) + parent_device._handle = parent_handle + return parent_device + + def get_device_by_index(self, index: int) -> Device: + """ + Get MIG device for the given index under its parent device. + + If the compute instance is destroyed either explicitly or by destroying, + resetting or unbinding the parent GPU instance or the GPU device itself + the MIG device handle would remain invalid and must be requested again + using this API. Handles may be reused and their properties can change in + the process. + + For Ampere™ or newer fully supported devices. + + Parameters + ---------- + index: int + The index of the MIG device (compute instance) to retrieve. Must be + between 0 and the value returned by `device_count - 1`. + + Returns + ------- + Device + The MIG device corresponding to the given index. + """ + mig_device_handle = nvml.device_get_mig_device_handle_by_index(self._device._handle, index) + mig_device = Device.__new__(Device) + mig_device._handle = mig_device_handle + return mig_device + + def get_all_devices(self) -> Iterable[Device]: + """ + Get all MIG devices under its parent device. + + If the compute instance is destroyed either explicitly or by destroying, + resetting or unbinding the parent GPU instance or the GPU device itself + the MIG device handle would remain invalid and must be requested again + using this API. Handles may be reused and their properties can change in + the process. + + For Ampere™ or newer fully supported devices. + + Returns + ------- + list[Device] + A list of all MIG devices corresponding to this GPU. + """ + for i in range(self.device_count): + yield self.get_device_by_index(i) diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index de3e6bf77f7..604ff9120f3 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -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 :orphan: @@ -76,6 +76,7 @@ NVML system._device.GpuTopologyLevel system._device.InforomInfo system._device.MemoryInfo + system._device.MigInfo system._device.PciInfo system._device.RepairStatus system._device.Temperature diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index 85a541018da..118b09fb9d6 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -57,7 +57,7 @@ def test_to_cuda_device(): cuda_device = device.to_cuda_device() assert isinstance(cuda_device, CudaDevice) - assert cuda_device.uuid == device.uuid + assert cuda_device.uuid == device.uuid_without_prefix # Technically, this test will only work with PCI devices, but are there # non-PCI devices we need to support? @@ -227,9 +227,9 @@ def test_device_serial(): assert len(serial) > 0 -def test_device_uuid(): +def test_device_uuid_without_prefix(): for device in system.Device.get_all_devices(): - uuid = device.uuid + uuid = device.uuid_without_prefix assert isinstance(uuid, str) # Expands to GPU-8hex-4hex-4hex-4hex-12hex, where 8hex means 8 consecutive @@ -729,3 +729,29 @@ def test_pstates(): assert isinstance(utilization.percentage, int) assert isinstance(utilization.inc_threshold, int) assert isinstance(utilization.dec_threshold, int) + + +@pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="MIG not supported on WSL or Windows") +def test_mig(): + for device in system.Device.get_all_devices(): + with unsupported_before(device, None): + mig = device.mig + + assert isinstance(mig.is_mig_device, bool) + assert isinstance(mig.mode, bool) + assert isinstance(mig.pending_mode, bool) + + device_count = mig.device_count + assert isinstance(device_count, int) + assert device_count >= 0 + + for mig_device in mig.get_all_devices(): + assert isinstance(mig_device, system.Device) + + +def test_uuid(): + for device in system.Device.get_all_devices(): + uuid = device.uuid + assert isinstance(uuid, str) + assert uuid.startswith(("GPU-", "MIG-")) + assert uuid == device.uuid diff --git a/cuda_core/tests/test_device.py b/cuda_core/tests/test_device.py index c4e1e9931f2..56a97f5185c 100644 --- a/cuda_core/tests/test_device.py +++ b/cuda_core/tests/test_device.py @@ -38,7 +38,7 @@ def test_to_system_device(deinit_cuda): system_device = device.to_system_device() assert isinstance(system_device, SystemDevice) - assert system_device.uuid == device.uuid + assert system_device.uuid_without_prefix == device.uuid # Technically, this test will only work with PCI devices, but are there # non-PCI devices we need to support? From 9863ea80aafcf63c0b825de9a8001e19f6e90003 Mon Sep 17 00:00:00 2001 From: Daniel Rodriguez Date: Wed, 22 Apr 2026 09:04:23 -0500 Subject: [PATCH 028/244] cuda.bindings latency benchmarks part 4 (#1959) --- .../cuda_bindings/benchmarks/bench_launch.py | 313 ++++++++++++++++-- .../cuda_bindings/benchmarks/bench_module.py | 57 ++++ .../cuda_bindings/benchmarks/bench_nvrtc.py | 83 +++++ .../benchmarks/cpp/CMakeLists.txt | 4 +- .../benchmarks/cpp/bench_launch.cpp | 262 ++++++++++++--- .../benchmarks/cpp/bench_module.cpp | 130 ++++++++ .../benchmarks/cpp/bench_nvrtc.cpp | 115 +++++++ .../benchmarks/cpp/bench_support.hpp | 10 + benchmarks/cuda_bindings/runner/runtime.py | 20 +- 9 files changed, 914 insertions(+), 80 deletions(-) create mode 100644 benchmarks/cuda_bindings/benchmarks/bench_module.py create mode 100644 benchmarks/cuda_bindings/benchmarks/bench_nvrtc.py create mode 100644 benchmarks/cuda_bindings/benchmarks/cpp/bench_module.cpp create mode 100644 benchmarks/cuda_bindings/benchmarks/cpp/bench_nvrtc.cpp diff --git a/benchmarks/cuda_bindings/benchmarks/bench_launch.py b/benchmarks/cuda_bindings/benchmarks/bench_launch.py index abf3f946ccc..eb3392b09d1 100644 --- a/benchmarks/cuda_bindings/benchmarks/bench_launch.py +++ b/benchmarks/cuda_bindings/benchmarks/bench_launch.py @@ -20,6 +20,15 @@ #define REP4(x, T) REP2(x##0, T) REP2(x##1, T) #define REP8(x, T) REP4(x##0, T) REP4(x##1, T) #define REP16(x, T) REP8(x##0, T) REP8(x##1, T) +#define REP32(x, T) REP16(x##0, T) REP16(x##1, T) +#define REP64(x, T) REP32(x##0, T) REP32(x##1, T) +#define REP128(x, T) REP64(x##0, T) REP64(x##1, T) +#define REP256(x, T) REP128(x##0, T) REP128(x##1, T) + +template +struct KernelFunctionParam { + unsigned char p[maxBytes]; +}; extern "C" __global__ void small_kernel_16_args( @@ -29,55 +38,194 @@ REP4(A, int*) REP8(A, int*)) { *F = 0; } + +extern "C" __global__ +void small_kernel_256_args( + ITEM_PARAM(F, int*) + REP1(A, int*) + REP2(A, int*) + REP4(A, int*) + REP8(A, int*) + REP16(A, int*) + REP32(A, int*) + REP64(A, int*) + REP128(A, int*)) +{ *F = 0; } + +extern "C" __global__ +void small_kernel_512_args( + ITEM_PARAM(F, int*) + REP1(A, int*) + REP2(A, int*) + REP4(A, int*) + REP8(A, int*) + REP16(A, int*) + REP32(A, int*) + REP64(A, int*) + REP128(A, int*) + REP256(A, int*)) +{ *F = 0; } + +extern "C" __global__ +void small_kernel_512_bools( + ITEM_PARAM(F, bool) + REP1(A, bool) + REP2(A, bool) + REP4(A, bool) + REP8(A, bool) + REP16(A, bool) + REP32(A, bool) + REP64(A, bool) + REP128(A, bool) + REP256(A, bool)) +{ return; } + +extern "C" __global__ +void small_kernel_512_ints( + ITEM_PARAM(F, int) + REP1(A, int) + REP2(A, int) + REP4(A, int) + REP8(A, int) + REP16(A, int) + REP32(A, int) + REP64(A, int) + REP128(A, int) + REP256(A, int)) +{ return; } + +extern "C" __global__ +void small_kernel_512_doubles( + ITEM_PARAM(F, double) + REP1(A, double) + REP2(A, double) + REP4(A, double) + REP8(A, double) + REP16(A, double) + REP32(A, double) + REP64(A, double) + REP128(A, double) + REP256(A, double)) +{ return; } + +extern "C" __global__ +void small_kernel_512_chars( + ITEM_PARAM(F, char) + REP1(A, char) + REP2(A, char) + REP4(A, char) + REP8(A, char) + REP16(A, char) + REP32(A, char) + REP64(A, char) + REP128(A, char) + REP256(A, char)) +{ return; } + +extern "C" __global__ +void small_kernel_512_longlongs( + ITEM_PARAM(F, long long) + REP1(A, long long) + REP2(A, long long) + REP4(A, long long) + REP8(A, long long) + REP16(A, long long) + REP32(A, long long) + REP64(A, long long) + REP128(A, long long) + REP256(A, long long)) +{ return; } + +extern "C" __global__ +void small_kernel_2048B(KernelFunctionParam<2048> param) { + // Do not touch param to prevent compiler from copying + // the whole structure from const bank to lmem. +} """ MODULE = None EMPTY_KERNEL = None SMALL_KERNEL = None KERNEL_16_ARGS = None +KERNEL_256_ARGS = None +KERNEL_512_ARGS = None +KERNEL_512_BOOLS = None +KERNEL_512_INTS = None +KERNEL_512_DOUBLES = None +KERNEL_512_CHARS = None +KERNEL_512_LONGLONGS = None +KERNEL_2048B = None STREAM = None FLOAT_PTR = None -INT_PTRS = None -_VAL_PS = None +INT_PTRS_512 = None +_VAL_PS_16 = None +_VAL_PS_512 = None PACKED_16 = None +PACKED_512 = None + + +class _Struct2048B(ctypes.Structure): + _fields_ = [("values", ctypes.c_uint8 * 2048)] + + +STRUCT_2048B = _Struct2048B() def _ensure_launch_state() -> None: - global MODULE, EMPTY_KERNEL, SMALL_KERNEL, KERNEL_16_ARGS, STREAM - global FLOAT_PTR, INT_PTRS, _VAL_PS, PACKED_16 + global MODULE, EMPTY_KERNEL, SMALL_KERNEL + global KERNEL_16_ARGS, KERNEL_256_ARGS, KERNEL_512_ARGS + global KERNEL_512_BOOLS, KERNEL_512_INTS, KERNEL_512_DOUBLES + global KERNEL_512_CHARS, KERNEL_512_LONGLONGS, KERNEL_2048B + global STREAM, FLOAT_PTR, INT_PTRS_512 + global _VAL_PS_16, _VAL_PS_512, PACKED_16, PACKED_512 if EMPTY_KERNEL is not None: return module = compile_and_load(KERNEL_SOURCE) - err, empty_kernel = cuda.cuModuleGetFunction(module, b"empty_kernel") - assert_drv(err) - err, small_kernel = cuda.cuModuleGetFunction(module, b"small_kernel") - assert_drv(err) - err, kernel_16_args = cuda.cuModuleGetFunction(module, b"small_kernel_16_args") - assert_drv(err) + def get_func(name): + err, func = cuda.cuModuleGetFunction(module, name.encode()) + assert_drv(err) + return func err, stream = cuda.cuStreamCreate(cuda.CUstream_flags.CU_STREAM_NON_BLOCKING.value) assert_drv(err) float_ptr = alloc_persistent(ctypes.sizeof(ctypes.c_float)) - int_ptrs = tuple(alloc_persistent(ctypes.sizeof(ctypes.c_int)) for _ in range(16)) + int_ptrs_512 = tuple(alloc_persistent(ctypes.sizeof(ctypes.c_int)) for _ in range(512)) - val_ps = [ctypes.c_void_p(int(ptr)) for ptr in int_ptrs] + # Pre-pack 16 args + val_ps_16 = [ctypes.c_void_p(int(ptr)) for ptr in int_ptrs_512[:16]] packed_16 = (ctypes.c_void_p * 16)() - for index, value_ptr in enumerate(val_ps): - packed_16[index] = ctypes.addressof(value_ptr) + for i, vp in enumerate(val_ps_16): + packed_16[i] = ctypes.addressof(vp) + + # Pre-pack 512 args + val_ps_512 = [ctypes.c_void_p(int(ptr)) for ptr in int_ptrs_512] + packed_512 = (ctypes.c_void_p * 512)() + for i, vp in enumerate(val_ps_512): + packed_512[i] = ctypes.addressof(vp) MODULE = module - EMPTY_KERNEL = empty_kernel - SMALL_KERNEL = small_kernel - KERNEL_16_ARGS = kernel_16_args + EMPTY_KERNEL = get_func("empty_kernel") + SMALL_KERNEL = get_func("small_kernel") + KERNEL_16_ARGS = get_func("small_kernel_16_args") + KERNEL_256_ARGS = get_func("small_kernel_256_args") + KERNEL_512_ARGS = get_func("small_kernel_512_args") + KERNEL_512_BOOLS = get_func("small_kernel_512_bools") + KERNEL_512_INTS = get_func("small_kernel_512_ints") + KERNEL_512_DOUBLES = get_func("small_kernel_512_doubles") + KERNEL_512_CHARS = get_func("small_kernel_512_chars") + KERNEL_512_LONGLONGS = get_func("small_kernel_512_longlongs") + KERNEL_2048B = get_func("small_kernel_2048B") STREAM = stream FLOAT_PTR = float_ptr - INT_PTRS = int_ptrs - _VAL_PS = val_ps + INT_PTRS_512 = int_ptrs_512 + _VAL_PS_16 = val_ps_16 + _VAL_PS_512 = val_ps_512 PACKED_16 = packed_16 + PACKED_512 = packed_512 def bench_launch_empty_kernel(loops: int) -> float: @@ -111,7 +259,7 @@ def bench_launch_16_args(loops: int) -> float: _fn = cuda.cuLaunchKernel _kernel = KERNEL_16_ARGS _stream = STREAM - _args = INT_PTRS + _args = INT_PTRS_512[:16] _arg_types = (None,) * 16 t0 = time.perf_counter() @@ -131,3 +279,128 @@ def bench_launch_16_args_pre_packed(loops: int) -> float: for _ in range(loops): _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, _packed, 0) return time.perf_counter() - t0 + + +def bench_launch_256_args(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_256_ARGS + _stream = STREAM + _args = INT_PTRS_512[:256] + _arg_types = (None,) * 256 + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + return time.perf_counter() - t0 + + +def bench_launch_512_args(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_512_ARGS + _stream = STREAM + _args = INT_PTRS_512 + _arg_types = (None,) * 512 + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + return time.perf_counter() - t0 + + +def bench_launch_512_args_pre_packed(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_512_ARGS + _stream = STREAM + _packed = PACKED_512 + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, _packed, 0) + return time.perf_counter() - t0 + + +def bench_launch_512_bools(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_512_BOOLS + _stream = STREAM + _args = (True,) * 512 + _arg_types = (ctypes.c_bool,) * 512 + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + return time.perf_counter() - t0 + + +def bench_launch_512_ints(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_512_INTS + _stream = STREAM + _args = (123,) * 512 + _arg_types = (ctypes.c_int,) * 512 + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + return time.perf_counter() - t0 + + +def bench_launch_512_doubles(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_512_DOUBLES + _stream = STREAM + _args = (1.2345,) * 512 + _arg_types = (ctypes.c_double,) * 512 + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + return time.perf_counter() - t0 + + +def bench_launch_512_bytes(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_512_CHARS + _stream = STREAM + _args = (127,) * 512 + _arg_types = (ctypes.c_byte,) * 512 + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + return time.perf_counter() - t0 + + +def bench_launch_512_longlongs(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_512_LONGLONGS + _stream = STREAM + _args = (9223372036854775806,) * 512 + _arg_types = (ctypes.c_longlong,) * 512 + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + return time.perf_counter() - t0 + + +def bench_launch_2048b(loops: int) -> float: + _ensure_launch_state() + _fn = cuda.cuLaunchKernel + _kernel = KERNEL_2048B + _stream = STREAM + _args = (STRUCT_2048B,) + _arg_types = (None,) + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_kernel, 1, 1, 1, 1, 1, 1, 0, _stream, (_args, _arg_types), 0) + return time.perf_counter() - t0 diff --git a/benchmarks/cuda_bindings/benchmarks/bench_module.py b/benchmarks/cuda_bindings/benchmarks/bench_module.py new file mode 100644 index 00000000000..0c2b317c3a1 --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/bench_module.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import time + +from runner.runtime import assert_drv, compile_cubin, ensure_context, register_module + +from cuda.bindings import driver as cuda + +ensure_context() + +# Compile a trivial kernel to cubin once; reuse for all benchmarks +KERNEL_SOURCE = 'extern "C" __global__ void empty_kernel() { return; }' +CUBIN = compile_cubin(KERNEL_SOURCE) + +# Load a persistent module + function for the get_function / get_attribute benchmarks +_err, MODULE = cuda.cuModuleLoadData(CUBIN) +assert_drv(_err) +register_module(MODULE) +_err, FUNCTION = cuda.cuModuleGetFunction(MODULE, b"empty_kernel") +assert_drv(_err) + +FUNC_ATTRIBUTE = cuda.CUfunction_attribute.CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK + + +def bench_module_load_unload(loops: int) -> float: + _load = cuda.cuModuleLoadData + _unload = cuda.cuModuleUnload + _cubin = CUBIN + + t0 = time.perf_counter() + for _ in range(loops): + _, m = _load(_cubin) + _unload(m) + return time.perf_counter() - t0 + + +def bench_module_get_function(loops: int) -> float: + _fn = cuda.cuModuleGetFunction + _module = MODULE + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_module, b"empty_kernel") + return time.perf_counter() - t0 + + +def bench_func_get_attribute(loops: int) -> float: + _fn = cuda.cuFuncGetAttribute + _attr = FUNC_ATTRIBUTE + _func = FUNCTION + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_attr, _func) + return time.perf_counter() - t0 diff --git a/benchmarks/cuda_bindings/benchmarks/bench_nvrtc.py b/benchmarks/cuda_bindings/benchmarks/bench_nvrtc.py new file mode 100644 index 00000000000..0b2598aaa4e --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/bench_nvrtc.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import time + +from runner.runtime import assert_drv, ensure_context + +from cuda.bindings import driver as cuda +from cuda.bindings import nvrtc + +ensure_context() + +KERNEL_SOURCE = b'extern "C" __global__ void empty_kernel() { return; }' +PROGRAM_NAME = b"benchmark_kernel.cu" + +# Compute the arch flag once for compile benchmarks +_err, _device = cuda.cuDeviceGet(0) +assert_drv(_err) +_err, _major = cuda.cuDeviceGetAttribute(cuda.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, _device) +assert_drv(_err) +_err, _minor = cuda.cuDeviceGetAttribute(cuda.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, _device) +assert_drv(_err) +ARCH_FLAG = f"--gpu-architecture=sm_{_major}{_minor}".encode() +COMPILE_OPTIONS = [b"--fmad=false", ARCH_FLAG] + +# Pre-build 100 empty headers for the headers benchmark +HEADER_NAMES = [f"header_{i}.cuh".encode() for i in range(100)] +HEADER_SOURCES = [b"// empty" for _ in range(100)] + + +def bench_nvrtc_create_program(loops: int) -> float: + _create = nvrtc.nvrtcCreateProgram + _destroy = nvrtc.nvrtcDestroyProgram + _assert = assert_drv + _src = KERNEL_SOURCE + _name = PROGRAM_NAME + + t0 = time.perf_counter() + for _ in range(loops): + err, prog = _create(_src, _name, 0, [], []) + _assert(err) + (err,) = _destroy(prog) + _assert(err) + return time.perf_counter() - t0 + + +def bench_nvrtc_create_program_100_headers(loops: int) -> float: + _create = nvrtc.nvrtcCreateProgram + _destroy = nvrtc.nvrtcDestroyProgram + _assert = assert_drv + _src = KERNEL_SOURCE + _name = PROGRAM_NAME + _headers = HEADER_SOURCES + _header_names = HEADER_NAMES + + t0 = time.perf_counter() + for _ in range(loops): + err, prog = _create(_src, _name, 100, _headers, _header_names) + _assert(err) + (err,) = _destroy(prog) + _assert(err) + return time.perf_counter() - t0 + + +def bench_nvrtc_compile_program(loops: int) -> float: + _create = nvrtc.nvrtcCreateProgram + _compile = nvrtc.nvrtcCompileProgram + _destroy = nvrtc.nvrtcDestroyProgram + _assert = assert_drv + _src = KERNEL_SOURCE + _name = PROGRAM_NAME + _options = COMPILE_OPTIONS + + t0 = time.perf_counter() + for _ in range(loops): + err, prog = _create(_src, _name, 0, [], []) + _assert(err) + (err,) = _compile(prog, 2, _options) + _assert(err) + (err,) = _destroy(prog) + _assert(err) + return time.perf_counter() - t0 diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt b/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt index 83326911af5..bc6541e5bb7 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt +++ b/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt @@ -87,6 +87,8 @@ add_driver_benchmark(bench_memory) # NVRTC benchmarks (require nvrtc for kernel compilation) if(NVRTC_INCLUDE_DIR AND NVRTC_LIBRARY) add_nvrtc_benchmark(bench_launch) + add_nvrtc_benchmark(bench_module) + add_nvrtc_benchmark(bench_nvrtc) else() - message(WARNING "NVRTC not found — skipping bench_launch. Install cuda-nvrtc-dev.") + message(WARNING "NVRTC not found — skipping bench_launch, bench_module, bench_nvrtc. Install cuda-nvrtc-dev.") endif() diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp index a2494269639..984c82fcf32 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp @@ -7,6 +7,7 @@ #include "bench_support.hpp" +#include #include #include #include @@ -45,7 +46,6 @@ static CUmodule compile_and_load(const char* source, CUdevice device) { const char* opts[] = {"--fmad=false", arch.c_str()}; nvrtcResult compile_result = nvrtcCompileProgram(prog, 2, opts); - // Print log on failure if (compile_result != NVRTC_SUCCESS) { size_t log_size = 0; nvrtcGetProgramLogSize(prog, &log_size); @@ -71,13 +71,85 @@ static const char* KERNEL_SOURCE = R"( extern "C" __global__ void empty_kernel() { return; } extern "C" __global__ void small_kernel(float *f) { *f = 0.0f; } +#define ITEM_PARAM(x, T) T x +#define REP1(x, T) , ITEM_PARAM(x, T) +#define REP2(x, T) REP1(x##0, T) REP1(x##1, T) +#define REP4(x, T) REP2(x##0, T) REP2(x##1, T) +#define REP8(x, T) REP4(x##0, T) REP4(x##1, T) +#define REP16(x, T) REP8(x##0, T) REP8(x##1, T) +#define REP32(x, T) REP16(x##0, T) REP16(x##1, T) +#define REP64(x, T) REP32(x##0, T) REP32(x##1, T) +#define REP128(x, T) REP64(x##0, T) REP64(x##1, T) +#define REP256(x, T) REP128(x##0, T) REP128(x##1, T) + +template +struct KernelFunctionParam { + unsigned char p[maxBytes]; +}; + extern "C" __global__ void small_kernel_16_args( - int* a0, int* a1, int* a2, int* a3, - int* a4, int* a5, int* a6, int* a7, - int* a8, int* a9, int* a10, int* a11, - int* a12, int* a13, int* a14, int* a15) -{ *a0 = 0; } + ITEM_PARAM(F, int*) + REP1(A, int*) REP2(A, int*) REP4(A, int*) REP8(A, int*)) +{ *F = 0; } + +extern "C" __global__ +void small_kernel_256_args( + ITEM_PARAM(F, int*) + REP1(A, int*) REP2(A, int*) REP4(A, int*) REP8(A, int*) + REP16(A, int*) REP32(A, int*) REP64(A, int*) REP128(A, int*)) +{ *F = 0; } + +extern "C" __global__ +void small_kernel_512_args( + ITEM_PARAM(F, int*) + REP1(A, int*) REP2(A, int*) REP4(A, int*) REP8(A, int*) + REP16(A, int*) REP32(A, int*) REP64(A, int*) REP128(A, int*) + REP256(A, int*)) +{ *F = 0; } + +extern "C" __global__ +void small_kernel_512_bools( + ITEM_PARAM(F, bool) + REP1(A, bool) REP2(A, bool) REP4(A, bool) REP8(A, bool) + REP16(A, bool) REP32(A, bool) REP64(A, bool) REP128(A, bool) + REP256(A, bool)) +{ return; } + +extern "C" __global__ +void small_kernel_512_ints( + ITEM_PARAM(F, int) + REP1(A, int) REP2(A, int) REP4(A, int) REP8(A, int) + REP16(A, int) REP32(A, int) REP64(A, int) REP128(A, int) + REP256(A, int)) +{ return; } + +extern "C" __global__ +void small_kernel_512_doubles( + ITEM_PARAM(F, double) + REP1(A, double) REP2(A, double) REP4(A, double) REP8(A, double) + REP16(A, double) REP32(A, double) REP64(A, double) REP128(A, double) + REP256(A, double)) +{ return; } + +extern "C" __global__ +void small_kernel_512_chars( + ITEM_PARAM(F, char) + REP1(A, char) REP2(A, char) REP4(A, char) REP8(A, char) + REP16(A, char) REP32(A, char) REP64(A, char) REP128(A, char) + REP256(A, char)) +{ return; } + +extern "C" __global__ +void small_kernel_512_longlongs( + ITEM_PARAM(F, long long) + REP1(A, long long) REP2(A, long long) REP4(A, long long) REP8(A, long long) + REP16(A, long long) REP32(A, long long) REP64(A, long long) REP128(A, long long) + REP256(A, long long)) +{ return; } + +extern "C" __global__ +void small_kernel_2048B(KernelFunctionParam<2048> param) {} )"; @@ -96,80 +168,160 @@ int main(int argc, char** argv) { CUmodule module = compile_and_load(KERNEL_SOURCE, device); - CUfunction empty_kernel, small_kernel, kernel_16_args; - check_cu(cuModuleGetFunction(&empty_kernel, module, "empty_kernel"), "GetFunction failed"); - check_cu(cuModuleGetFunction(&small_kernel, module, "small_kernel"), "GetFunction failed"); - check_cu(cuModuleGetFunction(&kernel_16_args, module, "small_kernel_16_args"), "GetFunction failed"); + // Get all kernel handles + auto get_func = [&](const char* name) { + CUfunction f; + check_cu(cuModuleGetFunction(&f, module, name), "GetFunction failed"); + return f; + }; + + CUfunction empty_kernel = get_func("empty_kernel"); + CUfunction small_kernel = get_func("small_kernel"); + CUfunction kernel_16_args = get_func("small_kernel_16_args"); + CUfunction kernel_256_args = get_func("small_kernel_256_args"); + CUfunction kernel_512_args = get_func("small_kernel_512_args"); + CUfunction kernel_512_bools = get_func("small_kernel_512_bools"); + CUfunction kernel_512_ints = get_func("small_kernel_512_ints"); + CUfunction kernel_512_doubles = get_func("small_kernel_512_doubles"); + CUfunction kernel_512_chars = get_func("small_kernel_512_chars"); + CUfunction kernel_512_longlongs = get_func("small_kernel_512_longlongs"); + CUfunction kernel_2048B = get_func("small_kernel_2048B"); CUstream stream; check_cu(cuStreamCreate(&stream, CU_STREAM_NON_BLOCKING), "cuStreamCreate failed"); - // Allocate device memory for arguments + // Allocate device memory CUdeviceptr float_ptr; check_cu(cuMemAlloc(&float_ptr, sizeof(float)), "cuMemAlloc failed"); - CUdeviceptr int_ptrs[16]; - for (int i = 0; i < 16; ++i) { + CUdeviceptr int_ptrs[512]; + for (int i = 0; i < 512; ++i) { check_cu(cuMemAlloc(&int_ptrs[i], sizeof(int)), "cuMemAlloc failed"); } - // Pre-pack kernel params for the pre-packed benchmark + // Pre-pack pointer params void* packed_16[16]; - for (int i = 0; i < 16; ++i) { + for (int i = 0; i < 16; ++i) packed_16[i] = &int_ptrs[i]; - } - bench::BenchmarkSuite suite(options); + void* packed_256[256]; + for (int i = 0; i < 256; ++i) + packed_256[i] = &int_ptrs[i]; - // --- launch_empty_kernel --- - { - suite.run("launch.launch_empty_kernel", [&]() { - check_cu( - cuLaunchKernel(empty_kernel, 1, 1, 1, 1, 1, 1, 0, stream, nullptr, nullptr), - "cuLaunchKernel failed" - ); - }); - } + void* packed_512[512]; + for (int i = 0; i < 512; ++i) + packed_512[i] = &int_ptrs[i]; - // Drain the stream between benchmarks so each starts with a clean queue - check_cu(cuStreamSynchronize(stream), "cuStreamSynchronize failed"); + // Typed args for 512-arg benchmarks + bool bool_args[512]; + void* bool_params[512]; + for (int i = 0; i < 512; ++i) { bool_args[i] = true; bool_params[i] = &bool_args[i]; } - { - void* params[] = {&float_ptr}; - suite.run("launch.launch_small_kernel", [&]() { - check_cu( - cuLaunchKernel(small_kernel, 1, 1, 1, 1, 1, 1, 0, stream, params, nullptr), - "cuLaunchKernel failed" - ); - }); - } + int int_args[512]; + void* int_params[512]; + for (int i = 0; i < 512; ++i) { int_args[i] = 123; int_params[i] = &int_args[i]; } - check_cu(cuStreamSynchronize(stream), "cuStreamSynchronize failed"); + double double_args[512]; + void* double_params[512]; + for (int i = 0; i < 512; ++i) { double_args[i] = 1.2345; double_params[i] = &double_args[i]; } - { - suite.run("launch.launch_16_args", [&]() { - check_cu( - cuLaunchKernel(kernel_16_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_16, nullptr), - "cuLaunchKernel failed" - ); - }); - } + char char_args[512]; + void* char_params[512]; + for (int i = 0; i < 512; ++i) { char_args[i] = 127; char_params[i] = &char_args[i]; } - check_cu(cuStreamSynchronize(stream), "cuStreamSynchronize failed"); + long long ll_args[512]; + void* ll_params[512]; + for (int i = 0; i < 512; ++i) { ll_args[i] = 9223372036854775806LL; ll_params[i] = &ll_args[i]; } + + // 2048-byte struct + struct alignas(8) Struct2048B { unsigned char p[2048]; } struct_2048B = {}; + void* struct_params[] = {&struct_2048B}; + + bench::BenchmarkSuite suite(options); + + suite.run("launch.launch_empty_kernel", [&]() { + check_cu(cuLaunchKernel(empty_kernel, 1, 1, 1, 1, 1, 1, 0, stream, nullptr, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); - // In C++ the params are always pre-packed, so this is identical to launch_16_args. - // We include it for naming parity with the Python benchmark. { - suite.run("launch.launch_16_args_pre_packed", [&]() { - check_cu( - cuLaunchKernel(kernel_16_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_16, nullptr), - "cuLaunchKernel failed" - ); + void* params[] = {&float_ptr}; + suite.run("launch.launch_small_kernel", [&]() { + check_cu(cuLaunchKernel(small_kernel, 1, 1, 1, 1, 1, 1, 0, stream, params, nullptr), + "cuLaunchKernel failed"); }); } + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_16_args", [&]() { + check_cu(cuLaunchKernel(kernel_16_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_16, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_16_args_pre_packed", [&]() { + check_cu(cuLaunchKernel(kernel_16_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_16, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_256_args", [&]() { + check_cu(cuLaunchKernel(kernel_256_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_256, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_512_args", [&]() { + check_cu(cuLaunchKernel(kernel_512_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_512, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_512_args_pre_packed", [&]() { + check_cu(cuLaunchKernel(kernel_512_args, 1, 1, 1, 1, 1, 1, 0, stream, packed_512, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_512_bools", [&]() { + check_cu(cuLaunchKernel(kernel_512_bools, 1, 1, 1, 1, 1, 1, 0, stream, bool_params, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_512_ints", [&]() { + check_cu(cuLaunchKernel(kernel_512_ints, 1, 1, 1, 1, 1, 1, 0, stream, int_params, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_512_doubles", [&]() { + check_cu(cuLaunchKernel(kernel_512_doubles, 1, 1, 1, 1, 1, 1, 0, stream, double_params, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_512_bytes", [&]() { + check_cu(cuLaunchKernel(kernel_512_chars, 1, 1, 1, 1, 1, 1, 0, stream, char_params, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_512_longlongs", [&]() { + check_cu(cuLaunchKernel(kernel_512_longlongs, 1, 1, 1, 1, 1, 1, 0, stream, ll_params, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); + + suite.run("launch.launch_2048b", [&]() { + check_cu(cuLaunchKernel(kernel_2048B, 1, 1, 1, 1, 1, 1, 0, stream, struct_params, nullptr), + "cuLaunchKernel failed"); + }); + check_cu(cuStreamSynchronize(stream), "sync"); // Cleanup - for (int i = 0; i < 16; ++i) { + for (int i = 0; i < 512; ++i) { check_cu(cuMemFree(int_ptrs[i]), "cuMemFree failed"); } check_cu(cuMemFree(float_ptr), "cuMemFree failed"); diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_module.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_module.cpp new file mode 100644 index 00000000000..802025a6506 --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_module.cpp @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "bench_support.hpp" + +#include +#include +#include +#include + + +static void check_cu(CUresult status, const char* message) { + if (status != CUDA_SUCCESS) { + const char* error_name = nullptr; + cuGetErrorName(status, &error_name); + std::cerr << message << ": " << (error_name ? error_name : "unknown") << '\n'; + std::exit(1); + } +} + +static void check_nvrtc(nvrtcResult status, const char* message) { + if (status != NVRTC_SUCCESS) { + std::cerr << message << ": " << nvrtcGetErrorString(status) << '\n'; + std::exit(1); + } +} + +static std::vector compile_cubin(const char* source, CUdevice device) { + int major = 0, minor = 0; + check_cu(cuDeviceGetAttribute(&major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, device), + "cuDeviceGetAttribute failed"); + check_cu(cuDeviceGetAttribute(&minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, device), + "cuDeviceGetAttribute failed"); + + nvrtcProgram prog; + check_nvrtc(nvrtcCreateProgram(&prog, source, "benchmark_kernel.cu", 0, nullptr, nullptr), + "nvrtcCreateProgram failed"); + + std::string arch = "--gpu-architecture=sm_" + std::to_string(major) + std::to_string(minor); + const char* opts[] = {"--fmad=false", arch.c_str()}; + nvrtcResult compile_result = nvrtcCompileProgram(prog, 2, opts); + + if (compile_result != NVRTC_SUCCESS) { + size_t log_size = 0; + nvrtcGetProgramLogSize(prog, &log_size); + std::vector log(log_size); + nvrtcGetProgramLog(prog, log.data()); + std::cerr << "NVRTC compile failed:\n" << log.data() << '\n'; + std::exit(1); + } + + size_t cubin_size = 0; + check_nvrtc(nvrtcGetCUBINSize(prog, &cubin_size), "nvrtcGetCUBINSize failed"); + std::vector cubin(cubin_size); + check_nvrtc(nvrtcGetCUBIN(prog, cubin.data()), "nvrtcGetCUBIN failed"); + nvrtcDestroyProgram(&prog); + + return cubin; +} + + +static const char* KERNEL_SOURCE = R"( +extern "C" __global__ void empty_kernel() { return; } +)"; + + +int main(int argc, char** argv) { + bench::Options options = bench::parse_args(argc, argv); + + // Setup + check_cu(cuInit(0), "cuInit failed"); + + CUdevice device; + check_cu(cuDeviceGet(&device, 0), "cuDeviceGet failed"); + + CUcontext ctx; + CUctxCreateParams ctxParams = {}; + check_cu(cuCtxCreate(&ctx, &ctxParams, 0, device), "cuCtxCreate failed"); + + std::vector cubin = compile_cubin(KERNEL_SOURCE, device); + + // Load a persistent module + function for get_function / get_attribute benchmarks + CUmodule persistent_module; + check_cu(cuModuleLoadData(&persistent_module, cubin.data()), "cuModuleLoadData failed"); + + CUfunction function; + check_cu(cuModuleGetFunction(&function, persistent_module, "empty_kernel"), + "cuModuleGetFunction failed"); + + bench::BenchmarkSuite suite(options); + + // --- module_load_unload --- + { + CUmodule m; + suite.run("module.module_load_unload", [&]() { + check_cu(cuModuleLoadData(&m, cubin.data()), "cuModuleLoadData failed"); + check_cu(cuModuleUnload(m), "cuModuleUnload failed"); + }); + } + + // --- module_get_function --- + { + CUfunction f; + suite.run("module.module_get_function", [&]() { + check_cu(cuModuleGetFunction(&f, persistent_module, "empty_kernel"), + "cuModuleGetFunction failed"); + }); + } + + // --- func_get_attribute --- + { + int value = 0; + suite.run("module.func_get_attribute", [&]() { + check_cu(cuFuncGetAttribute(&value, CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK, function), + "cuFuncGetAttribute failed"); + }); + } + + // Cleanup + check_cu(cuModuleUnload(persistent_module), "cuModuleUnload failed"); + check_cu(cuCtxDestroy(ctx), "cuCtxDestroy failed"); + + suite.write(); + + return 0; +} diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_nvrtc.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_nvrtc.cpp new file mode 100644 index 00000000000..842b7353cf4 --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_nvrtc.cpp @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "bench_support.hpp" + +#include +#include +#include +#include + + +static void check_cu(CUresult status, const char* message) { + if (status != CUDA_SUCCESS) { + const char* error_name = nullptr; + cuGetErrorName(status, &error_name); + std::cerr << message << ": " << (error_name ? error_name : "unknown") << '\n'; + std::exit(1); + } +} + +static void check_nvrtc(nvrtcResult status, const char* message) { + if (status != NVRTC_SUCCESS) { + std::cerr << message << ": " << nvrtcGetErrorString(status) << '\n'; + std::exit(1); + } +} + + +static const char* KERNEL_SOURCE = R"(extern "C" __global__ void empty_kernel() { return; })"; +static const char* PROGRAM_NAME = "benchmark_kernel.cu"; + +static constexpr int NUM_HEADERS = 100; + + +int main(int argc, char** argv) { + bench::Options options = bench::parse_args(argc, argv); + + // Setup: need CUDA init to query compute capability for compile options + check_cu(cuInit(0), "cuInit failed"); + + CUdevice device; + check_cu(cuDeviceGet(&device, 0), "cuDeviceGet failed"); + + int major = 0, minor = 0; + check_cu(cuDeviceGetAttribute(&major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, device), + "cuDeviceGetAttribute failed"); + check_cu(cuDeviceGetAttribute(&minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, device), + "cuDeviceGetAttribute failed"); + + std::string arch = "--gpu-architecture=sm_" + std::to_string(major) + std::to_string(minor); + const char* compile_opts[] = {"--fmad=false", arch.c_str()}; + + // Pre-build 100 empty headers + std::vector header_name_strs(NUM_HEADERS); + std::vector header_names(NUM_HEADERS); + std::vector header_sources(NUM_HEADERS); + static const char* empty_header = "// empty"; + for (int i = 0; i < NUM_HEADERS; ++i) { + header_name_strs[i] = "header_" + std::to_string(i) + ".cuh"; + header_names[i] = header_name_strs[i].c_str(); + header_sources[i] = empty_header; + } + + bench::BenchmarkSuite suite(options); + + // --- nvrtc_create_program (no headers) --- + { + nvrtcProgram prog; + suite.run("nvrtc.nvrtc_create_program", [&]() { + check_nvrtc( + nvrtcCreateProgram(&prog, KERNEL_SOURCE, PROGRAM_NAME, 0, nullptr, nullptr), + "nvrtcCreateProgram failed" + ); + check_nvrtc(nvrtcDestroyProgram(&prog), "nvrtcDestroyProgram failed"); + }); + } + + // --- nvrtc_create_program_100_headers --- + { + nvrtcProgram prog; + suite.run("nvrtc.nvrtc_create_program_100_headers", [&]() { + check_nvrtc( + nvrtcCreateProgram(&prog, KERNEL_SOURCE, PROGRAM_NAME, + NUM_HEADERS, header_sources.data(), header_names.data()), + "nvrtcCreateProgram failed" + ); + check_nvrtc(nvrtcDestroyProgram(&prog), "nvrtcDestroyProgram failed"); + }); + } + + // --- nvrtc_compile_program --- + { + nvrtcProgram prog; + std::uint64_t compile_loops = std::min(options.loops, static_cast(10)); + suite.run("nvrtc.nvrtc_compile_program", compile_loops, [&]() { + check_nvrtc( + nvrtcCreateProgram(&prog, KERNEL_SOURCE, PROGRAM_NAME, 0, nullptr, nullptr), + "nvrtcCreateProgram failed" + ); + check_nvrtc( + nvrtcCompileProgram(prog, 2, compile_opts), + "nvrtcCompileProgram failed" + ); + check_nvrtc(nvrtcDestroyProgram(&prog), "nvrtcDestroyProgram failed"); + }); + } + + suite.write(); + + return 0; +} diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp index 837c15a9d19..8b541228667 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp @@ -243,6 +243,16 @@ class BenchmarkSuite { entries_.push_back({name, options_.loops, std::move(results)}); } + // Run a benchmark with a custom loop count (for slow operations like compilation). + template + void run(const std::string& name, std::uint64_t loops_override, Fn&& fn) { + Options custom = options_; + custom.loops = loops_override; + auto results = run_benchmark(custom, std::forward(fn)); + print_summary(name, results); + entries_.push_back({name, loops_override, std::move(results)}); + } + // Write all collected benchmarks to the output file (if -o was given). void write() const { if (options_.output_path.empty() || entries_.empty()) diff --git a/benchmarks/cuda_bindings/runner/runtime.py b/benchmarks/cuda_bindings/runner/runtime.py index c985adb2e2b..153d92f259d 100644 --- a/benchmarks/cuda_bindings/runner/runtime.py +++ b/benchmarks/cuda_bindings/runner/runtime.py @@ -44,8 +44,13 @@ def alloc_persistent(size: int) -> int: return ptr -def compile_and_load(kernel_source: str) -> int: - """Compile CUDA C source and returns the CUmodule handle""" +def register_module(module) -> int: + _modules.append(module) + return module + + +def compile_cubin(kernel_source: str) -> bytes: + """Compile CUDA C source with NVRTC and return the raw cubin bytes.""" ensure_context() err, major = cuda.cuDeviceGetAttribute( @@ -76,11 +81,18 @@ def compile_and_load(kernel_source: str) -> int: cubin = b" " * cubin_size (err,) = nvrtc.nvrtcGetCUBIN(prog, cubin) assert_drv(err) + (err,) = nvrtc.nvrtcDestroyProgram(prog) + assert_drv(err) + return cubin + + +def compile_and_load(kernel_source: str) -> int: + """Compile CUDA C source and return the CUmodule handle.""" + cubin = compile_cubin(kernel_source) err, module = cuda.cuModuleLoadData(cubin) assert_drv(err) - _modules.append(module) - return module + return register_module(module) def cleanup() -> None: From 18bbe5ba11f7a5ca89abb10f3bd46f2809e174db Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 22 Apr 2026 10:51:10 -0400 Subject: [PATCH 029/244] Check struct and class changes in ABI checking tool (#1952) --- toolshed/check_cython_abi.py | 299 ++++++++++++++++++++++++++++++++--- 1 file changed, 275 insertions(+), 24 deletions(-) diff --git a/toolshed/check_cython_abi.py b/toolshed/check_cython_abi.py index 2199d526a3d..32b5e6be11e 100644 --- a/toolshed/check_cython_abi.py +++ b/toolshed/check_cython_abi.py @@ -6,12 +6,17 @@ """ Tool to check for Cython ABI changes in a given package. -There are different types of ABI changes, only one of which is covered by this tool: +Cython must be installed in your venv to run this script. + +There are different types of ABI changes, some of which are covered by this tool: - cdef function signatures (capsule strings) — covered here -- cdef class struct size (tp_basicsize) — not covered -- cdef class vtable layout / method reordering — not covered, and this one fails as silent UB rather than an import-time error -- Fused specialization ordering — partially covered (reorders manifest as capsule-name deltas, but the mapping is non-obvious) +- cdef class struct size (tp_basicsize) — covered here +- cdef struct / ctypedef struct field layout — covered here (via .pxd parsing) +- cdef class vtable layout / method reordering — not covered, and this one fails + as silent UB rather than an import-time error +- Fused specialization ordering — partially covered (reorders manifest as + capsule-name deltas, but the mapping is non-obvious) The workflow is basically: @@ -21,13 +26,13 @@ package is installed), where `package_name` is the import path to the package, e.g. `cuda.bindings`: - python check_cython_abi.py generate + python check_cython_abi.py generate 3) Checkout a version with the changes to be tested, and build and install. 4) Check the ABI against the previously generated files by running: - python check_cython_abi.py check + python check_cython_abi.py check """ import ctypes @@ -35,8 +40,14 @@ import json import sys import sysconfig +from io import StringIO from pathlib import Path +from Cython.Compiler import Parsing +from Cython.Compiler.Scanning import FileSourceDescriptor, PyrexScanner +from Cython.Compiler.Symtab import ModuleScope +from Cython.Compiler.TreeFragment import StringParseContext + EXT_SUFFIX = sysconfig.get_config_var("EXT_SUFFIX") ABI_SUFFIX = ".abi.json" @@ -66,12 +77,12 @@ def import_from_path(root_package: str, root_dir: Path, path: Path) -> object: def so_path_to_abi_path(so_path: Path, build_dir: Path, abi_dir: Path) -> Path: - abi_name = short_stem(so_path.name) + ABI_SUFFIX + abi_name = f"{short_stem(so_path.name)}{ABI_SUFFIX}" return abi_dir / so_path.parent.relative_to(build_dir) / abi_name def abi_path_to_so_path(abi_path: Path, build_dir: Path, abi_dir: Path) -> Path: - so_name = short_stem(abi_path.name) + EXT_SUFFIX + so_name = f"{short_stem(abi_path.name)}{EXT_SUFFIX}" return build_dir / abi_path.parent.relative_to(abi_dir) / so_name @@ -80,16 +91,244 @@ def is_cython_module(module: object) -> bool: return hasattr(module, "__pyx_capi__") -def module_to_json(module: object) -> dict: - """ - Converts extracts information about a Cython-compiled .so into JSON-serializable information. +###################################################################################### +# STRUCTS + + +def get_cdef_classes(module: object) -> dict: + """Extract cdef class (extension type) basicsize from a compiled Cython module.""" + result = {} + module_name = module.__name__ + for name in sorted(dir(module)): + obj = getattr(module, name, None) + if isinstance(obj, type) and getattr(obj, "__module__", None) == module_name and hasattr(obj, "__basicsize__"): + result[name] = {"basicsize": obj.__basicsize__} + return result + + +def _format_base_type_name(bt: object) -> str: + """Format a Cython base type AST node into a type name string.""" + cls = type(bt).__name__ + if cls == "CSimpleBaseTypeNode": + return bt.name + if cls == "CComplexBaseTypeNode": + inner = _format_base_type_name(bt.base_type) + return _unwrap_declarator(inner, bt.declarator)[0] + return cls + + +def _unwrap_declarator(type_str: str, decl: object) -> tuple[str, str]: + """Unwrap nested Cython declarator nodes to get (type_string, field_name).""" + cls = type(decl).__name__ + if cls == "CNameDeclaratorNode": + return type_str, decl.name + if cls == "CPtrDeclaratorNode": + return _unwrap_declarator(f"{type_str}*", decl.base) + if cls == "CReferenceDeclaratorNode": + return _unwrap_declarator(f"{type_str}&", decl.base) + if cls == "CArrayDeclaratorNode": + dim = getattr(decl, "dimension", None) + size = getattr(dim, "value", "") if dim is not None else "" + return _unwrap_declarator(f"{type_str}[{size}]", decl.base) + return type_str, "" + + +def _extract_fields_from_cvardef(node: object) -> list: + """Extract [type, name] pairs from a CVarDefNode.""" + results = [] + for d in node.declarators: + type_str, name = _unwrap_declarator(_format_base_type_name(node.base_type), d) + if name: + results.append([type_str, name]) + return results + + +def _collect_cvardef_fields(node: object) -> list: + """Recursively collect CVarDefNode fields, skipping nested struct/class/func defs.""" + fields = [] + if type(node).__name__ == "CVarDefNode": + fields.extend(_extract_fields_from_cvardef(node)) + skip = ("CStructOrUnionDefNode", "CClassDefNode", "CFuncDefNode") + for attr_name in getattr(node, "child_attrs", []): + child = getattr(node, attr_name, None) + if child is None: + continue + if isinstance(child, list): + for item in child: + if hasattr(item, "child_attrs") and type(item).__name__ not in skip: + fields.extend(_collect_cvardef_fields(item)) + elif hasattr(child, "child_attrs") and type(child).__name__ not in skip: + fields.extend(_collect_cvardef_fields(child)) + return fields + + +def _collect_structs_from_tree(node: object) -> dict: + """Walk a Cython AST and collect struct/class field definitions.""" + result = {} + cls = type(node).__name__ + + if cls == "CStructOrUnionDefNode": + fields = [] + for attr in node.attributes: + if type(attr).__name__ == "CVarDefNode": + fields.extend(_extract_fields_from_cvardef(attr)) + if fields: + result[node.name] = {"fields": fields} + + elif cls == "CClassDefNode": + fields = _collect_cvardef_fields(node.body) + if fields: + result[node.class_name] = {"fields": fields} + + for attr_name in getattr(node, "child_attrs", []): + child = getattr(node, attr_name, None) + if child is None: + continue + if isinstance(child, list): + for item in child: + if hasattr(item, "child_attrs"): + result.update(_collect_structs_from_tree(item)) + elif hasattr(child, "child_attrs"): + result.update(_collect_structs_from_tree(child)) + + return result + + +class _PxdParseContext(StringParseContext): + """Parse context that resolves includes via real paths and ignores unknown cimports.""" + + def find_module( + self, + module_name, + from_module=None, # noqa: ARG002 + pos=None, # noqa: ARG002 + need_pxd=1, # noqa: ARG002 + absolute_fallback=True, # noqa: ARG002 + relative_import=False, # noqa: ARG002 + ): + return ModuleScope(module_name, parent_module=None, context=self) + + +def parse_pxd_structs(pxd_path: Path) -> dict: + """Parse struct and cdef class field definitions from a .pxd file. + + Uses Cython's own parser (in .pxd mode) for reliable extraction. + cimport lines in the top-level file are stripped since they are + unresolvable without the full compilation context; included files + are handled via a lenient context that returns dummy scopes. + + Returns a dict mapping struct/class name to {"fields": [[type, name], ...]}. """ - # Sort the dictionary by keys to make diffs in the JSON files smaller - pyx_capi = module.__pyx_capi__ + text = pxd_path.read_text(encoding="utf-8") + + # Strip cimport lines (unresolvable without full compilation context) + lines = text.splitlines() + cleaned = "\n".join("" if (" cimport " in ln or ln.lstrip().startswith("cimport ")) else ln for ln in lines) + + name = pxd_path.stem + context = _PxdParseContext(name, include_directories=[str(pxd_path.parent)]) + code_source = FileSourceDescriptor(str(pxd_path)) + scope = context.find_module(name, pos=(code_source, 1, 0), need_pxd=False) + + scanner = PyrexScanner( + StringIO(cleaned), + code_source, + source_encoding="UTF-8", + scope=scope, + context=context, + initial_pos=(code_source, 1, 0), + ) + tree = Parsing.p_module(scanner, pxd=1, full_module_name=name) + tree.scope = scope + + return _collect_structs_from_tree(tree) + + +def get_structs(module: object) -> dict: + # Extract cdef class basicsize from compiled module (primary) + structs = get_cdef_classes(module) + so_path = Path(module.__file__) + + # Parse neighboring .pxd file for struct/class field layout (fallback complement) + if so_path is not None: + pxd_path = so_path.parent / f"{short_stem(so_path.name)}.pxd" + if pxd_path.is_file(): + pxd_structs = parse_pxd_structs(pxd_path) + for name, info in pxd_structs.items(): + if name in structs: + structs[name].update(info) + else: + structs[name] = info + + return dict(sorted(structs.items())) + + +def _report_field_changes(name: str, expected_fields: list, found_fields: list) -> None: + """Print detailed field-level differences for a struct.""" + expected_dict = {f[1]: f[0] for f in expected_fields} + found_dict = {f[1]: f[0] for f in found_fields} + + for field_name, field_type in expected_dict.items(): + if field_name not in found_dict: + print(f" Struct {name}: removed field '{field_name}'") + elif found_dict[field_name] != field_type: + print( + f" Struct {name}: field '{field_name}' type changed from '{field_type}' to '{found_dict[field_name]}'" + ) + for field_name in found_dict: + if field_name not in expected_dict: + print(f" Struct {name}: added field '{field_name}'") + + expected_common = [f[1] for f in expected_fields if f[1] in found_dict] + found_common = [f[1] for f in found_fields if f[1] in expected_dict] + if expected_common != found_common: + print(f" Struct {name}: fields were reordered") + + +def check_structs(expected: dict, found: dict) -> tuple[bool, bool]: + has_errors = False + has_allowed_changes = False + + for name, expected_info in expected.items(): + if name not in found: + print(f" Missing struct/class: {name}") + has_errors = True + continue + found_info = found[name] - return { - "functions": {k: get_capsule_name(pyx_capi[k]) for k in sorted(pyx_capi.keys())}, - } + if "basicsize" in expected_info: + if "basicsize" not in found_info: + print(f" Struct {name}: basicsize no longer available") + has_errors = True + elif found_info["basicsize"] != expected_info["basicsize"]: + print( + f" Struct {name}: basicsize changed from {expected_info['basicsize']} to {found_info['basicsize']}" + ) + has_errors = True + + if "fields" in expected_info: + if "fields" not in found_info: + print(f" Struct {name}: field information no longer available") + has_errors = True + elif found_info["fields"] != expected_info["fields"]: + _report_field_changes(name, expected_info["fields"], found_info["fields"]) + has_errors = True + + for name in found: + if name not in expected: + print(f" Added struct/class: {name}") + has_allowed_changes = True + + return has_errors, has_allowed_changes + + +###################################################################################### +# FUNCTIONS + + +def get_functions(module: object) -> dict: + pyx_capi = module.__pyx_capi__ + return {k: get_capsule_name(pyx_capi[k]) for k in sorted(pyx_capi.keys())} def check_functions(expected: dict[str, str], found: dict[str, str]) -> tuple[bool, bool]: @@ -109,17 +348,29 @@ def check_functions(expected: dict[str, str], found: dict[str, str]) -> tuple[bo return has_errors, has_allowed_changes +###################################################################################### +# MAIN + + def compare(expected: dict, found: dict) -> tuple[bool, bool]: has_errors = False has_allowed_changes = False - errors, allowed_changes = check_functions(expected["functions"], found["functions"]) - has_errors |= errors - has_allowed_changes |= allowed_changes + for func, name in [(check_functions, "functions"), (check_structs, "structs")]: + errors, allowed_changes = func(expected[name], found[name]) + has_errors |= errors + has_allowed_changes |= allowed_changes return has_errors, has_allowed_changes +def module_to_json(module: object) -> dict: + """ + Extracts information about a Cython-compiled .so into JSON-serializable information. + """ + return {"functions": get_functions(module), "structs": get_structs(module)} + + def check(package: str, abi_dir: Path) -> bool: build_dir = get_package_path(package) @@ -168,7 +419,7 @@ def check(package: str, abi_dir: Path) -> bool: return False -def regenerate(package: str, abi_dir: Path) -> bool: +def generate(package: str, abi_dir: Path) -> bool: if abi_dir.is_dir(): print(f"ABI directory {abi_dir} already exists. Please remove it before regenerating.") return True @@ -199,10 +450,10 @@ def regenerate(package: str, abi_dir: Path) -> bool: subparsers = parser.add_subparsers() - regen_parser = subparsers.add_parser("generate", help="Regenerate the ABI files") - regen_parser.set_defaults(func=regenerate) - regen_parser.add_argument("package", help="Python package to collect data from") - regen_parser.add_argument("dir", help="Output directory to save data to") + gen_parser = subparsers.add_parser("generate", help="Regenerate the ABI files") + gen_parser.set_defaults(func=generate) + gen_parser.add_argument("package", help="Python package to collect data from") + gen_parser.add_argument("dir", help="Output directory to save data to") check_parser = subparsers.add_parser("check", help="Check the API against existing ABI files") check_parser.set_defaults(func=check) From aa108439831f301ec927b7d32c51e0e7b0fc3b70 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 22 Apr 2026 11:54:52 -0400 Subject: [PATCH 030/244] Faster benchmarking smoke tests (#1962) --- .github/workflows/test-wheel-linux.yml | 2 +- benchmarks/cuda_bindings/pixi.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 708238e20fd..35c5e6c3734 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -275,7 +275,7 @@ jobs: run: | pip install pyperf pushd benchmarks/cuda_bindings - python run_pyperf.py --fast --min-time 1 + python run_pyperf.py --debug-single-value popd - name: Run cuda.core tests diff --git a/benchmarks/cuda_bindings/pixi.toml b/benchmarks/cuda_bindings/pixi.toml index dbbddcd9397..a265f7f01e7 100644 --- a/benchmarks/cuda_bindings/pixi.toml +++ b/benchmarks/cuda_bindings/pixi.toml @@ -55,8 +55,7 @@ source = { features = ["cu13", "cu13-source", "bench", "cpp-bench", "dev", "bind cmd = ["python", "$PIXI_PROJECT_ROOT/run_pyperf.py"] [target.linux.tasks.bench-smoke-test] -cmd = ["python", "$PIXI_PROJECT_ROOT/run_pyperf.py", "--fast", "--min-time", "1" -] +cmd = ["python", "$PIXI_PROJECT_ROOT/run_pyperf.py", "--debug-single-value"] [target.linux.tasks.bench-legacy] cmd = "pytest --benchmark-only --override-ini 'addopts=' $PIXI_PROJECT_ROOT/pytest-legacy/" From c747f7b5e2893aa41d588191d2a0369afc92a964 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 22 Apr 2026 12:53:49 -0400 Subject: [PATCH 031/244] cuda.core.system: Add basic Nvlink and Utilization support (#1918) * cuda.core.system: Add basic Nvlink and Utilization support * Address comments in the PR * Fix test --- cuda_core/cuda/core/system/_device.pyx | 43 ++++++++++++++++ cuda_core/cuda/core/system/_nvlink.pxi | 52 ++++++++++++++++++++ cuda_core/cuda/core/system/_utilization.pxi | 29 +++++++++++ cuda_core/docs/source/api.rst | 1 + cuda_core/docs/source/api_private.rst | 1 + cuda_core/tests/system/test_system_device.py | 35 +++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 cuda_core/cuda/core/system/_nvlink.pxi create mode 100644 cuda_core/cuda/core/system/_utilization.pxi diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index 27a487bc3f7..4b4c5aa048a 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -33,10 +33,12 @@ include "_field_values.pxi" include "_inforom.pxi" include "_memory.pxi" include "_mig.pxi" +include "_nvlink.pxi" include "_pci_info.pxi" include "_performance.pxi" include "_repair_status.pxi" include "_temperature.pxi" +include "_utilization.pxi" cdef class Device: @@ -702,6 +704,20 @@ cdef class Device: """ return MemoryInfo(nvml.device_get_memory_info_v2(self._handle)) + ########################################################################## + # NVLINK + # See external class definitions in _nvlink.pxi + + def get_nvlink(self, link: int) -> NvlinkInfo: + """ + Get :obj:`~NvlinkInfo` about this device. + + For devices with NVLink support. + """ + if link < 0 or link >= NvlinkInfo.max_links: + raise ValueError(f"Link index {link} is out of range [0, {NvlinkInfo.max_links})") + return NvlinkInfo(self, link) + ########################################################################## # PCI INFO # See external class definitions in _pci_info.pxi @@ -798,6 +814,31 @@ cdef class Device: device._handle = handle yield device + ####################################################################### + # UTILIZATION + + @property + def utilization(self) -> Utilization: + """ + Retrieves the current :obj:`~Utilization` rates for the device's major + subsystems. + + For Fermi™ or newer fully supported devices. + + Note: During driver initialization when ECC is enabled one can see high + GPU and Memory Utilization readings. This is caused by ECC Memory + Scrubbing mechanism that is performed during driver initialization. + + Note: On MIG-enabled GPUs, querying device utilization rates is not + currently supported. + + Returns + ------- + Utilization + An object containing the current utilization rates for the device. + """ + return Utilization(nvml.device_get_utilization_rates(self._handle)) + def get_topology_common_ancestor(device1: Device, device2: Device) -> GpuTopologyLevel: """ @@ -872,10 +913,12 @@ __all__ = [ "GpuP2PStatus", "GpuTopologyLevel", "InforomObject", + "NvlinkVersion", "PcieUtilCounter", "Pstates", "TemperatureSensors", "TemperatureThresholds", "ThermalController", "ThermalTarget", + "Utilization", ] diff --git a/cuda_core/cuda/core/system/_nvlink.pxi b/cuda_core/cuda/core/system/_nvlink.pxi new file mode 100644 index 00000000000..aeee3af1535 --- /dev/null +++ b/cuda_core/cuda/core/system/_nvlink.pxi @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +NvlinkVersion = nvml.NvlinkVersion + + +cdef class NvlinkInfo: + """ + Nvlink information for a device. + """ + cdef Device _device + cdef int _link + + def __init__(self, device: Device, link: int): + self._device = device + self._link = link + + @property + def version(self) -> NvlinkVersion: + """ + Retrieves the :obj:`~NvlinkVersion` for the device and link. + + For all products with NvLink support. + + Returns + ------- + NvlinkVersion + The Nvlink version. + """ + return NvlinkVersion(nvml.device_get_nvlink_version(self._device._handle, self._link)) + + @property + def state(self) -> bool: + """ + Retrieves the state of the device's Nvlink for the device and link specified. + + For Pascal™ or newer fully supported devices. + + For all products with Nvlink support. + + Returns + ------- + bool + `True` if the Nvlink is active. + """ + return ( + nvml.device_get_nvlink_state(self._device._handle, self._link) == nvml.EnableState.FEATURE_ENABLED + ) + + max_links = nvml.NVLINK_MAX_LINKS diff --git a/cuda_core/cuda/core/system/_utilization.pxi b/cuda_core/cuda/core/system/_utilization.pxi new file mode 100644 index 00000000000..689b7dc67f2 --- /dev/null +++ b/cuda_core/cuda/core/system/_utilization.pxi @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +cdef class Utilization: + """ + Utilization rates for a device. + + For devices with compute capability 2.0 or higher. + """ + cdef object _utilization + + def __init__(self, utilization: nvml.Utilization): + self._utilization = utilization + + @property + def gpu(self) -> int: + """ + Percent of time over the past sample period during which one or more kernels was executing on the GPU. + """ + return self._utilization.gpu + + @property + def memory(self) -> int: + """ + Percent of time over the past sample period during which global (device) memory was being read or written. + """ + return self._utilization.memory diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 8bd3638da0e..88780732d54 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -220,6 +220,7 @@ Enums system.FanControlPolicy system.FieldId system.InforomObject + system.NvlinkVersion system.PcieUtilCounter system.Pstates system.TemperatureSensors diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index 604ff9120f3..911820ebc53 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -77,6 +77,7 @@ NVML system._device.InforomInfo system._device.MemoryInfo system._device.MigInfo + system._device.NvlinkInfo system._device.PciInfo system._device.RepairStatus system._device.Temperature diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index 118b09fb9d6..3fc8f7a367f 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -731,6 +731,41 @@ def test_pstates(): assert isinstance(utilization.dec_threshold, int) +def test_nvlink(): + for device in system.Device.get_all_devices(): + max_links = _device.NvlinkInfo.max_links + assert isinstance(max_links, int) + assert max_links > 0 + + for link in range(max_links): + with unsupported_before(device, None): + nvlink_info = device.get_nvlink(link) + assert isinstance(nvlink_info, _device.NvlinkInfo) + + with unsupported_before(device, None): + version = nvlink_info.version + assert isinstance(version, system.NvlinkVersion) + + with unsupported_before(device, None): + state = nvlink_info.state + assert isinstance(state, bool) + + +def test_utilization(): + for device in system.Device.get_all_devices(): + with unsupported_before(device, None): + utilization = device.utilization + assert isinstance(utilization, system.Utilization) + + gpu = utilization.gpu + assert isinstance(gpu, int) + assert 0 <= gpu <= 100 + + memory = utilization.memory + assert isinstance(memory, int) + assert 0 <= memory <= 100 + + @pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="MIG not supported on WSL or Windows") def test_mig(): for device in system.Device.get_all_devices(): From 7b0009854531540441b3b25f07bc3c79e86caa6d Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 22 Apr 2026 22:30:01 -0400 Subject: [PATCH 032/244] cuda.core.system: Add ProcessInfo APIs (#1917) * cuda.core.system: Add ProcessInfo APIs * Add API docs * Fix test --- cuda_core/cuda/core/system/_device.pyx | 28 ++++++++++++ cuda_core/cuda/core/system/_process.pxi | 48 ++++++++++++++++++++ cuda_core/docs/source/api_private.rst | 1 + cuda_core/tests/system/test_system_device.py | 19 ++++++++ 4 files changed, 96 insertions(+) create mode 100644 cuda_core/cuda/core/system/_process.pxi diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index 4b4c5aa048a..9cfc77adb1a 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -36,6 +36,7 @@ include "_mig.pxi" include "_nvlink.pxi" include "_pci_info.pxi" include "_performance.pxi" +include "_process.pxi" include "_repair_status.pxi" include "_temperature.pxi" include "_utilization.pxi" @@ -765,6 +766,33 @@ cdef class Device: """ return [Pstates(x) for x in nvml.device_get_supported_performance_states(self._handle)] + ########################################################################## + # PROCESS + # See external class definitions in _process.pxi + + @property + def compute_running_processes(self) -> list[ProcessInfo]: + """ + Get information about processes with a compute context on a device + + For Fermi™ or newer fully supported devices. + + This function returns information only about compute running processes + (e.g. CUDA application which have active context). Any graphics + applications (e.g. using OpenGL, DirectX) won't be listed by this + function. + + Keep in mind that information returned by this call is dynamic and the + number of elements might change in time. + + In MIG mode, if device handle is provided, the API returns aggregate + information, only if the caller has appropriate privileges. Per-instance + information can be queried by using specific MIG device handles. + Querying per-instance information using MIG device handles is not + supported if the device is in vGPU Host virtualization mode. + """ + return [ProcessInfo(self, proc) for proc in nvml.device_get_compute_running_processes_v3(self._handle)] + ########################################################################## # REPAIR STATUS # See external class definitions in _repair_status.pxi diff --git a/cuda_core/cuda/core/system/_process.pxi b/cuda_core/cuda/core/system/_process.pxi new file mode 100644 index 00000000000..019ebf5c323 --- /dev/null +++ b/cuda_core/cuda/core/system/_process.pxi @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +class ProcessInfo: + """ + Information about running compute processes on the GPU. + """ + def __init__(self, device: "Device", process_info: nvml.ProcessInfo): + self._device = device + self._process_info = process_info + + @property + def pid(self) -> int: + """ + The PID of the process. + """ + return self._process_info.pid + + @property + def used_gpu_memory(self) -> int: + """ + The amount of GPU memory (in bytes) used by the process. + """ + return self._process_info.used_gpu_memory + + @property + def gpu_instance_id(self) -> int: + """ + The GPU instance ID for MIG devices. + + Only valid for processes running on MIG devices. + """ + if not self._device.mig.is_mig_device: + raise nvml.NotSupportedError(nvml.Return.ERROR_NOT_SUPPORTED) + return self._process_info.gpu_instance_id + + @property + def compute_instance_id(self) -> int: + """ + The Compute instance ID for MIG devices. + + Only valid for processes running on MIG devices. + """ + if not self._device.mig.is_mig_device: + raise nvml.NotSupportedError(nvml.Return.ERROR_NOT_SUPPORTED) + return self._process_info.compute_instance_id diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index 911820ebc53..141773967e8 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -79,6 +79,7 @@ NVML system._device.MigInfo system._device.NvlinkInfo system._device.PciInfo + system._device.ProcessInfo system._device.RepairStatus system._device.Temperature system._device.ThermalSensor diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index 3fc8f7a367f..21f3b6164bb 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -731,6 +731,25 @@ def test_pstates(): assert isinstance(utilization.dec_threshold, int) +def test_compute_running_processes(): + for device in system.Device.get_all_devices(): + with unsupported_before(device, "FERMI"): + processes = device.compute_running_processes + assert isinstance(processes, list) + for proc in processes: + assert isinstance(proc, _device.ProcessInfo) + assert isinstance(proc.pid, int) + assert isinstance(proc.used_gpu_memory, int) + if device.mig.is_mig_device: + assert isinstance(proc.gpu_instance_id, int) + assert isinstance(proc.compute_instance_id, int) + else: + with pytest.raises(nvml.NotSupportedError): + proc.gpu_instance_id # noqa: B018 + with pytest.raises(nvml.NotSupportedError): + proc.compute_instance_id # noqa: B018 + + def test_nvlink(): for device in system.Device.get_all_devices(): max_links = _device.NvlinkInfo.max_links From 7cf4a1bfe046284b89a4b545023c9849f94f1b55 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:12:00 -0400 Subject: [PATCH 033/244] fix(pathfinder): Linux .so glob fallback prefers newest (#1732) (#1966) * fix(pathfinder): glob fallback prefers newest lib.so* (#1732) * test(pathfinder): cover Linux .so glob fallback newest-first policy (#1732) --- .../_dynamic_libs/search_platform.py | 11 +- cuda_pathfinder/tests/test_search_steps.py | 105 ++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py index 95e0f4dd1ee..37fd6eb1700 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py @@ -43,10 +43,16 @@ def _find_so_in_rel_dirs( for rel_dir in rel_dirs: sub_dir = tuple(rel_dir.split(os.path.sep)) for abs_dir in find_sub_dirs_all_sitepackages(sub_dir): + # Exact unversioned match first; fall back to versioned names because some + # distros only ship lib.so. (e.g. conda libcupti). Only one match + # is expected in practice. Sort in reverse so the newest-sorting name wins if + # multiple coexist, matching the newest-first bias elsewhere in pathfinder + # (see LinuxSearchPlatform.find_in_lib_dir and load_dl_linux._candidate_sonames). + # Issue #1732 tracks the deferred question of raising on true ambiguity. so_name = os.path.join(abs_dir, so_basename) if os.path.isfile(so_name): return so_name - for so_name in sorted(glob.glob(os.path.join(abs_dir, file_wild))): + for so_name in sorted(glob.glob(os.path.join(abs_dir, file_wild)), reverse=True): if os.path.isfile(so_name): return so_name sub_dirs_searched.append(sub_dir) @@ -150,7 +156,8 @@ def find_in_lib_dir( file_wild = lib_searched_for + "*" # Only one match is expected, but to ensure deterministic behavior in unexpected # situations, and to be internally consistent, we sort in reverse order with the - # intent to return the newest version first. + # intent to return the newest version first. Issue #1732 tracks the deferred + # question of raising on true ambiguity. for so_name in sorted(glob.glob(os.path.join(lib_dir, file_wild)), reverse=True): if os.path.isfile(so_name): return so_name diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index 5672c7e5049..1b881707dfb 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -148,6 +148,63 @@ def test_not_found_appends_error(self, mocker, tmp_path): assert result is None assert any("No such file" in m for m in ctx.error_messages) + # The next three tests cover the Linux glob fallback in + # cuda.pathfinder._dynamic_libs.search_platform._find_so_in_rel_dirs. + # The fallback triggers when the unversioned libfoo.so is absent but + # versioned libfoo.so. files exist (e.g. some conda layouts). + # Issue #1732 tracks the decision to return the newest-sorting match + # deterministically; these tests lock in that policy at the + # site-packages call site. + + def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path): + lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib" + lib_dir.mkdir(parents=True) + versioned = lib_dir / "libcudart.so.13" + versioned.touch() + + mocker.patch( + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + return_value=[str(lib_dir)], + ) + + result = find_in_site_packages(_ctx(platform=LinuxSearchPlatform())) + assert result is not None + assert result.abs_path == str(versioned) + assert result.found_via == "site-packages" + + def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path): + lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib" + lib_dir.mkdir(parents=True) + older = lib_dir / "libcudart.so.12" + newer = lib_dir / "libcudart.so.13" + older.touch() + newer.touch() + + mocker.patch( + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + return_value=[str(lib_dir)], + ) + + result = find_in_site_packages(_ctx(platform=LinuxSearchPlatform())) + assert result is not None + assert result.abs_path == str(newer) + assert result.found_via == "site-packages" + + def test_glob_fallback_zero_matches_returns_none(self, mocker, tmp_path): + lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib" + lib_dir.mkdir(parents=True) + (lib_dir / "unrelated.txt").touch() + + mocker.patch( + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + return_value=[str(lib_dir)], + ) + + ctx = _ctx(platform=LinuxSearchPlatform()) + result = find_in_site_packages(ctx) + assert result is None + assert any("No such file" in m and "libcudart.so" in m for m in ctx.error_messages) + # --------------------------------------------------------------------------- # find_in_conda @@ -189,6 +246,54 @@ def test_found_windows(self, mocker, tmp_path): assert result.abs_path == str(dll) assert result.found_via == "conda" + # The next three tests cover the Linux glob fallback in + # cuda.pathfinder._dynamic_libs.search_platform.LinuxSearchPlatform.find_in_lib_dir, + # which is exercised by find_in_conda (and find_in_cuda_path) when the + # resolved lib dir contains only versioned libfoo.so. files. + # Issue #1732 tracks the decision to return the newest-sorting match + # deterministically; these tests lock in that policy at the conda / + # CUDA_PATH call site. + + def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path): + lib_dir = tmp_path / "lib" + lib_dir.mkdir() + versioned = lib_dir / "libcudart.so.13" + versioned.touch() + + mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)}) + + result = find_in_conda(_ctx(platform=LinuxSearchPlatform())) + assert result is not None + assert result.abs_path == str(versioned) + assert result.found_via == "conda" + + def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path): + lib_dir = tmp_path / "lib" + lib_dir.mkdir() + older = lib_dir / "libcudart.so.12" + newer = lib_dir / "libcudart.so.13" + older.touch() + newer.touch() + + mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)}) + + result = find_in_conda(_ctx(platform=LinuxSearchPlatform())) + assert result is not None + assert result.abs_path == str(newer) + assert result.found_via == "conda" + + def test_glob_fallback_zero_matches_returns_none(self, mocker, tmp_path): + lib_dir = tmp_path / "lib" + lib_dir.mkdir() + (lib_dir / "unrelated.txt").touch() + + mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)}) + + ctx = _ctx(platform=LinuxSearchPlatform()) + result = find_in_conda(ctx) + assert result is None + assert any("No such file" in m and "libcudart.so" in m for m in ctx.error_messages) + # --------------------------------------------------------------------------- # find_in_cuda_path From e29fa8f88fde694110e9f01835ba2dda69610586 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Thu, 23 Apr 2026 23:30:16 -0400 Subject: [PATCH 034/244] Update actions/labeler pin to include actions/labeler#917 (#1971) The previous pin (v6.0.1, 634933e) has a bug where labels added by other workflows during the same run can be removed even with sync-labels: false. This caused Needs-Restricted-Paths-Review to be stripped immediately after being applied (see #1967). Pin to e52e4fb which includes the fix from actions/labeler#917 ("Preserve manually added labels during workflow run and refine label sync logic"). --- .github/workflows/pr-auto-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-auto-label.yml b/.github/workflows/pr-auto-label.yml index 7b86455b2d1..dfd77156a26 100644 --- a/.github/workflows/pr-auto-label.yml +++ b/.github/workflows/pr-auto-label.yml @@ -20,4 +20,4 @@ jobs: pull-requests: write steps: - name: Apply labels - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + uses: actions/labeler@e52e4fb63ed5cd0e07abaad9826b2a893ccb921f # main (include actions/labeler#917) From 19fac32de2584cfeaf212b5a0cd057b993fe3510 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 24 Apr 2026 19:36:25 +0700 Subject: [PATCH 035/244] Remove `enable = "cpython-freethreading"` lines in cuda_bindings, cuda_core pyproject.toml files (#1968) --- cuda_bindings/pyproject.toml | 1 - cuda_core/pyproject.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index 3aa9c625560..d72ab7f7750 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -73,7 +73,6 @@ readme = { file = ["DESCRIPTION.rst"], content-type = "text/x-rst" } [tool.cibuildwheel] skip = "*-musllinux_*" -enable = "cpython-freethreading" build-verbosity = 1 [tool.cibuildwheel.linux] diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index 660c2a577fe..d82932616c8 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -109,7 +109,6 @@ git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--mat [tool.cibuildwheel] skip = "*-musllinux_*" -enable = "cpython-freethreading" build-verbosity = 1 [tool.cibuildwheel.linux] From 44b80d8266f28f02cb92367a4f912ece354d618b Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 24 Apr 2026 11:01:13 -0400 Subject: [PATCH 036/244] cuda.core.system: Naming improvements suggested in #1945 (#1946) * cuda.core.system: Naming improvements suggested in #1945 * persistence_mode -> is_persistence_mode_enabled --- cuda_core/cuda/core/system/_device.pyx | 32 +++++----- cuda_core/cuda/core/system/_fan.pxi | 2 +- cuda_core/cuda/core/system/_pci_info.pxi | 20 ++++--- cuda_core/cuda/core/system/_temperature.pxi | 6 +- cuda_core/tests/system/test_system_device.py | 62 ++++++++++---------- 5 files changed, 66 insertions(+), 56 deletions(-) diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index 9cfc77adb1a..bc7ef433bdc 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -230,14 +230,14 @@ cdef class Device: return nvml.device_get_minor_number(self._handle) @property - def is_c2c_mode_enabled(self) -> bool: + def is_c2c_enabled(self) -> bool: """ Whether the C2C (Chip-to-Chip) mode is enabled for this device. """ return bool(nvml.device_get_c2c_mode_info_v(self._handle).is_c2c_enabled) @property - def persistence_mode_enabled(self) -> bool: + def is_persistence_mode_enabled(self) -> bool: """ Whether persistence mode is enabled for this device. @@ -245,8 +245,8 @@ cdef class Device: """ return nvml.device_get_persistence_mode(self._handle) == nvml.EnableState.FEATURE_ENABLED - @persistence_mode_enabled.setter - def persistence_mode_enabled(self, enabled: bool) -> None: + @is_persistence_mode_enabled.setter + def is_persistence_mode_enabled(self, enabled: bool) -> None: nvml.device_set_persistence_mode( self._handle, nvml.EnableState.FEATURE_ENABLED if enabled else nvml.EnableState.FEATURE_DISABLED @@ -440,13 +440,14 @@ cdef class Device: # CLOCK # See external class definitions in _clock.pxi - def clock(self, clock_type: ClockType) -> ClockInfo: + def get_clock(self, clock_type: ClockType) -> ClockInfo: """ :obj:`~_device.ClockInfo` object to get information about and manage a specific clock on a device. """ return ClockInfo(self._handle, clock_type) - def get_auto_boosted_clocks_enabled(self) -> tuple[bool, bool]: + @property + def is_auto_boosted_clocks_enabled(self) -> tuple[bool, bool]: """ Retrieve the current state of auto boosted clocks on a device. @@ -471,7 +472,8 @@ cdef class Device: current, default = nvml.device_get_auto_boosted_clocks_enabled(self._handle) return current == nvml.EnableState.FEATURE_ENABLED, default == nvml.EnableState.FEATURE_ENABLED - def get_current_clock_event_reasons(self) -> list[ClocksEventReasons]: + @property + def current_clock_event_reasons(self) -> list[ClocksEventReasons]: """ Retrieves the current :obj:`~ClocksEventReasons`. @@ -481,7 +483,8 @@ cdef class Device: reasons[0] = nvml.device_get_current_clocks_event_reasons(self._handle) return [ClocksEventReasons(1 << reason) for reason in _unpack_bitmask(reasons)] - def get_supported_clock_event_reasons(self) -> list[ClocksEventReasons]: + @property + def supported_clock_event_reasons(self) -> list[ClocksEventReasons]: """ Retrieves supported :obj:`~ClocksEventReasons` that can be returned by :meth:`get_current_clock_event_reasons`. @@ -523,17 +526,17 @@ cdef class Device: # DISPLAY @property - def display_mode(self) -> bool: + def is_display_connected(self) -> bool: """ The display mode for this device. Indicates whether a physical display (e.g. monitor) is currently connected to any of the device's connectors. """ - return True if nvml.device_get_display_mode(self._handle) == nvml.EnableState.FEATURE_ENABLED else False + return nvml.device_get_display_mode(self._handle) == nvml.EnableState.FEATURE_ENABLED @property - def display_active(self) -> bool: + def is_display_active(self) -> bool: """ The display active status for this device. @@ -543,7 +546,7 @@ cdef class Device: Display can be active even when no monitor is physically attached. """ - return True if nvml.device_get_display_active(self._handle) == nvml.EnableState.FEATURE_ENABLED else False + return nvml.device_get_display_active(self._handle) == nvml.EnableState.FEATURE_ENABLED ########################################################################## # EVENTS @@ -611,7 +614,7 @@ cdef class Device: # FAN # See external class definitions in _fan.pxi - def fan(self, fan: int = 0) -> FanInfo: + def get_fan(self, fan: int = 0) -> FanInfo: """ :obj:`~_device.FanInfo` object to get information and manage a specific fan on a device. """ @@ -752,7 +755,8 @@ cdef class Device: """ return GpuDynamicPstatesInfo(nvml.device_get_dynamic_pstates_info(self._handle)) - def get_supported_pstates(self) -> list[Pstates]: + @property + def supported_pstates(self) -> list[Pstates]: """ Get all supported Performance States (P-States) for the device. diff --git a/cuda_core/cuda/core/system/_fan.pxi b/cuda_core/cuda/core/system/_fan.pxi index 6b5e26f4b03..bfe417c267e 100644 --- a/cuda_core/cuda/core/system/_fan.pxi +++ b/cuda_core/cuda/core/system/_fan.pxi @@ -96,7 +96,7 @@ cdef class FanInfo: """ return FanControlPolicy(nvml.device_get_fan_control_policy_v2(self._handle, self._fan)) - def set_default_fan_speed(self): + def set_default_speed(self): """ Set the speed of the fan control policy to default. diff --git a/cuda_core/cuda/core/system/_pci_info.pxi b/cuda_core/cuda/core/system/_pci_info.pxi index e5f1bb287e9..1402853fa2f 100644 --- a/cuda_core/cuda/core/system/_pci_info.pxi +++ b/cuda_core/cuda/core/system/_pci_info.pxi @@ -81,7 +81,8 @@ cdef class PciInfo: """ return self._pci_info_ext.sub_class - def get_max_pcie_link_generation(self) -> int: + @property + def link_generation(self) -> int: """ Retrieve the maximum PCIe link generation possible with this device and system. @@ -93,7 +94,8 @@ cdef class PciInfo: """ return nvml.device_get_max_pcie_link_generation(self._handle) - def get_gpu_max_pcie_link_generation(self) -> int: + @property + def max_link_generation(self) -> int: """ Retrieve the maximum PCIe link generation supported by this GPU device. @@ -101,7 +103,8 @@ cdef class PciInfo: """ return nvml.device_get_gpu_max_pcie_link_generation(self._handle) - def get_max_pcie_link_width(self) -> int: + @property + def max_link_width(self) -> int: """ Retrieve the maximum PCIe link width possible with this device and system. @@ -113,7 +116,8 @@ cdef class PciInfo: """ return nvml.device_get_max_pcie_link_width(self._handle) - def get_current_pcie_link_generation(self) -> int: + @property + def current_link_generation(self) -> int: """ Retrieve the current PCIe link generation. @@ -121,7 +125,8 @@ cdef class PciInfo: """ return nvml.device_get_curr_pcie_link_generation(self._handle) - def get_current_pcie_link_width(self) -> int: + @property + def current_link_width(self) -> int: """ Retrieve the current PCIe link width. @@ -129,7 +134,7 @@ cdef class PciInfo: """ return nvml.device_get_curr_pcie_link_width(self._handle) - def get_pcie_throughput(self, counter: PcieUtilCounter) -> int: + def get_throughput(self, counter: PcieUtilCounter) -> int: """ Retrieve PCIe utilization information, in KB/s. @@ -143,7 +148,8 @@ cdef class PciInfo: """ return nvml.device_get_pcie_throughput(self._handle, counter) - def get_pcie_replay_counter(self) -> int: + @property + def replay_counter(self) -> int: """ Retrieve the PCIe replay counter. diff --git a/cuda_core/cuda/core/system/_temperature.pxi b/cuda_core/cuda/core/system/_temperature.pxi index 8f8e10a570b..187f52be81e 100644 --- a/cuda_core/cuda/core/system/_temperature.pxi +++ b/cuda_core/cuda/core/system/_temperature.pxi @@ -77,7 +77,7 @@ cdef class Temperature: def __init__(self, handle: int): self._handle = handle - def sensor( + def get_sensor( self, sensor: TemperatureSensors = TemperatureSensors.TEMPERATURE_GPU ) -> int: @@ -97,7 +97,7 @@ cdef class Temperature: """ return nvml.device_get_temperature_v(self._handle, sensor) - def threshold(self, threshold_type: TemperatureThresholds) -> int: + def get_threshold(self, threshold_type: TemperatureThresholds) -> int: """ Retrieves the temperature threshold for this GPU with the specified threshold type, in degrees Celsius. @@ -127,7 +127,7 @@ cdef class Temperature: """ return nvml.device_get_margin_temperature(self._handle) - def thermal_settings(self, sensor_index: ThermalTarget) -> ThermalSettings: + def get_thermal_settings(self, sensor_index: ThermalTarget) -> ThermalSettings: """ Used to execute a list of thermal system instructions. diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index 21f3b6164bb..d0895dd2237 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -198,25 +198,25 @@ def test_device_pci_info(): assert isinstance(pci_info.sub_class, int) assert 0x00 <= pci_info.sub_class <= 0xFF - assert isinstance(pci_info.get_max_pcie_link_generation(), int) - assert 0 <= pci_info.get_max_pcie_link_generation() <= 0xFF + assert isinstance(pci_info.link_generation, int) + assert 0 <= pci_info.link_generation <= 0xFF - assert isinstance(pci_info.get_gpu_max_pcie_link_generation(), int) - assert 0 <= pci_info.get_gpu_max_pcie_link_generation() <= 0xFF + assert isinstance(pci_info.max_link_generation, int) + assert 0 <= pci_info.max_link_generation <= 0xFF - assert isinstance(pci_info.get_max_pcie_link_width(), int) - assert 0 <= pci_info.get_max_pcie_link_width() <= 0xFF + assert isinstance(pci_info.max_link_width, int) + assert 0 <= pci_info.max_link_width <= 0xFF - assert isinstance(pci_info.get_current_pcie_link_generation(), int) - assert 0 <= pci_info.get_current_pcie_link_generation() <= 0xFF + assert isinstance(pci_info.current_link_generation, int) + assert 0 <= pci_info.current_link_generation <= 0xFF - assert isinstance(pci_info.get_current_pcie_link_width(), int) - assert 0 <= pci_info.get_current_pcie_link_width() <= 0xFF + assert isinstance(pci_info.current_link_width, int) + assert 0 <= pci_info.current_link_width <= 0xFF with unsupported_before(device, None): - assert isinstance(pci_info.get_pcie_throughput(system.PcieUtilCounter.PCIE_UTIL_TX_BYTES), int) + assert isinstance(pci_info.get_throughput(system.PcieUtilCounter.PCIE_UTIL_TX_BYTES), int) - assert isinstance(pci_info.get_pcie_replay_counter(), int) + assert isinstance(pci_info.replay_counter, int) def test_device_serial(): @@ -336,23 +336,23 @@ def test_device_attributes(): def test_c2c_mode_enabled(): for device in system.Device.get_all_devices(): with unsupported_before(device, None): - is_enabled = device.is_c2c_mode_enabled + is_enabled = device.is_c2c_enabled assert isinstance(is_enabled, bool) @pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="Persistence mode not supported on WSL or Windows") def test_persistence_mode_enabled(): for device in system.Device.get_all_devices(): - is_enabled = device.persistence_mode_enabled + is_enabled = device.is_persistence_mode_enabled assert isinstance(is_enabled, bool) try: - device.persistence_mode_enabled = False + device.is_persistence_mode_enabled = False except nvml.NoPermissionError as e: pytest.xfail(f"nvml.NoPermissionError: {e}") try: - assert device.persistence_mode_enabled is False + assert device.is_persistence_mode_enabled is False finally: - device.persistence_mode_enabled = is_enabled + device.is_persistence_mode_enabled = is_enabled def test_field_values(): @@ -440,11 +440,11 @@ def test_addressing_mode(): def test_display_mode(): for device in system.Device.get_all_devices(): - display_mode = device.display_mode - assert isinstance(display_mode, bool) + is_display_connected = device.is_display_connected + assert isinstance(is_display_connected, bool) - display_active = device.display_active - assert isinstance(display_active, bool) + is_display_active = device.is_display_active + assert isinstance(is_display_active, bool) def test_repair_status(): @@ -548,7 +548,7 @@ def test_auto_boosted_clocks_enabled(): # This API is supported on KEPLER and newer, but it also seems # unsupported elsewhere. with unsupported_before(device, None): - current, default = device.get_auto_boosted_clocks_enabled() + current, default = device.is_auto_boosted_clocks_enabled assert isinstance(current, bool) assert isinstance(default, bool) @@ -556,7 +556,7 @@ def test_auto_boosted_clocks_enabled(): def test_clock(): for device in system.Device.get_all_devices(): for clock_type in system.ClockType: - clock = device.clock(clock_type) + clock = device.get_clock(clock_type) assert isinstance(clock, _device.ClockInfo) # These are ordered from oldest API to newest API so we test as much @@ -605,11 +605,11 @@ def test_clock(): def test_clock_event_reasons(): for device in system.Device.get_all_devices(): with unsupported_before(device, None): - reasons = device.get_current_clock_event_reasons() + reasons = device.current_clock_event_reasons assert all(isinstance(reason, system.ClocksEventReasons) for reason in reasons) with unsupported_before(device, None): - reasons = device.get_supported_clock_event_reasons() + reasons = device.supported_clock_event_reasons assert all(isinstance(reason, system.ClocksEventReasons) for reason in reasons) @@ -621,7 +621,7 @@ def test_fan(): pytest.skip("Device has no fans to test") for fan_idx in range(device.num_fans): - fan_info = device.fan(fan_idx) + fan_info = device.get_fan(fan_idx) assert isinstance(fan_info, _device.FanInfo) speed = fan_info.speed @@ -650,7 +650,7 @@ def test_fan(): control_policy = fan_info.control_policy assert isinstance(control_policy, system.FanControlPolicy) finally: - fan_info.set_default_fan_speed() + fan_info.set_default_speed() def test_cooler(): @@ -677,7 +677,7 @@ def test_temperature(): temperature = device.temperature assert isinstance(temperature, _device.Temperature) - sensor = temperature.sensor() + sensor = temperature.get_sensor() assert isinstance(sensor, int) assert sensor >= 0 @@ -685,7 +685,7 @@ def test_temperature(): # is also unsupported on other hardware. with unsupported_before(device, None): for threshold in list(system.TemperatureThresholds)[:-1]: - t = temperature.threshold(threshold) + t = temperature.get_threshold(threshold) assert isinstance(t, int) assert t >= 0 @@ -695,7 +695,7 @@ def test_temperature(): assert margin >= 0 with unsupported_before(device, None): - thermals = temperature.thermal_settings(system.ThermalTarget.ALL) + thermals = temperature.get_thermal_settings(system.ThermalTarget.ALL) assert isinstance(thermals, _device.ThermalSettings) for i, sensor in enumerate(thermals): @@ -716,7 +716,7 @@ def test_pstates(): pstate = device.performance_state assert isinstance(pstate, system.Pstates) - pstates = device.get_supported_pstates() + pstates = device.supported_pstates assert all(isinstance(p, system.Pstates) for p in pstates) dynamic_pstates_info = device.dynamic_pstates_info From f9930988034875d816f93d0e10255a948f67c8bc Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 24 Apr 2026 11:13:56 -0400 Subject: [PATCH 037/244] nvbug6084457: Fix device architecture handling and NVLink link count query (#1937) * nvbug6084457: Fix device architecture handling and NVLink link count query * Apply suggestion from @cpcloud Co-authored-by: Phillip Cloud <417981+cpcloud@users.noreply.github.com> * Simplify code --------- Co-authored-by: Phillip Cloud <417981+cpcloud@users.noreply.github.com> --- cuda_bindings/tests/nvml/test_init.py | 10 ++++++++-- cuda_bindings/tests/nvml/test_nvlink.py | 2 +- cuda_core/cuda/core/system/_device.pyx | 6 +++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cuda_bindings/tests/nvml/test_init.py b/cuda_bindings/tests/nvml/test_init.py index 2a04799708e..4c94dc26a3e 100644 --- a/cuda_bindings/tests/nvml/test_init.py +++ b/cuda_bindings/tests/nvml/test_init.py @@ -25,11 +25,17 @@ def test_devices_are_the_same_architecture(all_devices): # they won't be tested properly. This tests for the (hopefully rare) case # where a system has devices of different architectures and produces a warning. - all_arches = {nvml.DeviceArch(nvml.device_get_architecture(device)) for device in all_devices} + def get_architecture_name(arch): + try: + return nvml.DeviceArch(arch).name + except ValueError: + return f"UNKNOWN_ARCH_ID({arch})" + + all_arches = {nvml.device_get_architecture(device) for device in all_devices} if len(all_arches) > 1: warnings.warn( - f"System has devices of multiple architectures ({', '.join(x.name for x in all_arches)}). " + f"System has devices of multiple architectures ({', '.join(get_architecture_name(x) for x in all_arches)}). " f" Some tests may be skipped unexpectedly", UserWarning, ) diff --git a/cuda_bindings/tests/nvml/test_nvlink.py b/cuda_bindings/tests/nvml/test_nvlink.py index d8e782831ef..be82aa37453 100644 --- a/cuda_bindings/tests/nvml/test_nvlink.py +++ b/cuda_bindings/tests/nvml/test_nvlink.py @@ -26,4 +26,4 @@ def test_nvlink_get_link_count(all_devices): # The feature_nvlink_supported detection is not robust, so we # can't be more specific about how many links we should find. if value.nvml_return == nvml.Return.SUCCESS: - assert value.value.ui_val <= nvml.NVLINK_MAX_LINKS, f"Unexpected link count {value.value.ui_val}" + assert value.value.ui_val[0] <= nvml.NVLINK_MAX_LINKS, f"Unexpected link count {value.value.ui_val[0]}" diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index bc7ef433bdc..987851054d5 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -180,7 +180,11 @@ cdef class Device: "VOLTA"``, and RTX A6000 will report ``DeviceArchitecture.name == "AMPERE"``. """ - return DeviceArch(nvml.device_get_architecture(self._handle)) + arch = nvml.device_get_architecture(self._handle) + try: + return DeviceArch(arch) + except ValueError: + return nvml.DeviceArch.UNKNOWN @property def name(self) -> str: From 9eec53953e40536f3c7b52f79024e5dc0ed69b45 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 25 Apr 2026 04:37:02 +0700 Subject: [PATCH 038/244] [FEA]: Add CUDA driver version query to `cuda.pathfinder` (private API) (#1953) * Add a reusable pathfinder driver info helper. Break out CUDA driver version querying into a standalone internal utility so it can be reused independently from compatibility checks, and cover the ctypes loader paths with focused tests. Made-with: Cursor * Refine pathfinder driver info loader checks. Treat the Windows WinDLL path as the normal runtime case and keep the focused tests aligned with the stricter driver-loader invariants. Made-with: Cursor * Add parsed pathfinder driver version metadata. Wrap the encoded cuDriverGetVersion() result in a DriverVersion dataclass so callers can use major and minor fields directly while retaining a low-level integer helper for loader-focused tests. Made-with: Cursor * Add a real pathfinder driver version test. Cover query_driver_version() alongside the driver library loading tests and reuse the existing strictness mode so host-specific failures still surface cleanly in all_must_work mode. Made-with: Cursor * Reduce redundant pathfinder driver info mocks. Drop the non-Windows loader mock now that a real driver-version test covers the Linux success path, while keeping the Windows branch and failure-path unit coverage. Made-with: Cursor * Rename the pathfinder CUDA driver version dataclass. Use DriverCudaVersion for clearer pairing with the planned release-version type while keeping the existing driver info API behavior unchanged. Made-with: Cursor * Add a pathfinder NVML driver release version helper. Query nvmlSystemGetDriverVersion() through pathfinder's driver library loading path and add a minimal real test so the implementation is preserved as a future reference. Made-with: Cursor * Revert the pathfinder NVML driver release version helper. Step back from the exploratory NVML-based release-version query for now because it adds non-trivial complexity and a new dependency surface without a current pathfinder need, while keeping the reference implementation in history if we need it later. Made-with: Cursor * Clarify the pathfinder CUDA driver version naming. Document that DriverCudaVersion matches the CUDA Version shown by nvidia-smi rather than the graphics driver release, so the dataclass name reads clearly in context. Made-with: Cursor * Finalize the pathfinder CUDA driver version query API. Expose DriverCudaVersion, QueryDriverCudaVersionError, and query_driver_cuda_version publicly, and align the internal naming, caching, docs, and test coverage around the CUDA-specific driver version query. Made-with: Cursor * Add a public pathfinder driver info regression test. Protect the new top-level driver-info re-exports so internal-only test coverage does not miss a broken `cuda.pathfinder` plumbing layer. Made-with: Cursor * Remove pathfinder driver info public re-exports Stop exposing the new driver info helper through cuda.pathfinder while keeping the internal implementation and internal test coverage in place. Made-with: Cursor * Clarify DriverCudaVersion docstring terminology. Spell out that `cuDriverGetVersion()` reports the CUDA-facing user-mode driver (UMD) version rather than the kernel-mode driver (KMD) package version so the `nvidia-smi` comparison is less ambiguous. Made-with: Cursor --- .../cuda/pathfinder/_utils/driver_info.py | 80 ++++++++++++++ .../tests/test_driver_lib_loading.py | 21 ++++ .../tests/test_utils_driver_info.py | 101 ++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py create mode 100644 cuda_pathfinder/tests/test_utils_driver_info.py diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py new file mode 100644 index 00000000000..a5d4d167d33 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import ctypes +import functools +from collections.abc import Callable +from dataclasses import dataclass + +from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import ( + load_nvidia_dynamic_lib as _load_nvidia_dynamic_lib, +) +from cuda.pathfinder._utils.platform_aware import IS_WINDOWS + + +class QueryDriverCudaVersionError(RuntimeError): + """Raised when ``query_driver_cuda_version()`` cannot determine the CUDA driver version.""" + + +@dataclass(frozen=True, slots=True) +class DriverCudaVersion: + """ + CUDA-facing driver version reported by ``cuDriverGetVersion()``. + + The name ``DriverCudaVersion`` is intentionally specific: this dataclass + models the version shown as ``CUDA Version`` in ``nvidia-smi``, not the + graphics driver release shown as ``Driver Version``. More specifically, + it reflects the CUDA user-mode driver (UMD) interface version reported by + ``cuDriverGetVersion()``, not the kernel-mode driver (KMD) package + version. + + Example ``nvidia-smi`` output:: + + +---------------------------------------------------------------------+ + | NVIDIA-SMI 595.58.03 Driver Version: 595.58.03 CUDA Version: 13.2 | + +---------------------------------------------------------------------+ + + For the example above, ``DriverCudaVersion(encoded=13020, major=13, + minor=2)`` corresponds to ``CUDA Version: 13.2``. It does not correspond + to ``Driver Version: 595.58.03``. + """ + + encoded: int + major: int + minor: int + + +@functools.cache +def query_driver_cuda_version() -> DriverCudaVersion: + """Return the CUDA driver version parsed into its major/minor components.""" + try: + encoded = _query_driver_cuda_version_int() + return DriverCudaVersion( + encoded=encoded, + major=encoded // 1000, + minor=(encoded % 1000) // 10, + ) + except Exception as exc: + raise QueryDriverCudaVersionError("Failed to query the CUDA driver version.") from exc + + +def _query_driver_cuda_version_int() -> int: + """Return the encoded CUDA driver version from ``cuDriverGetVersion()``.""" + loaded_cuda = _load_nvidia_dynamic_lib("cuda") + if IS_WINDOWS: + # `ctypes.WinDLL` exists on Windows at runtime. The ignore is only for + # Linux mypy runs, where the platform stubs do not define that attribute. + loader_cls: Callable[[str], ctypes.CDLL] = ctypes.WinDLL # type: ignore[attr-defined] + else: + loader_cls = ctypes.CDLL + driver_lib = loader_cls(loaded_cuda.abs_path) + cu_driver_get_version = driver_lib.cuDriverGetVersion + cu_driver_get_version.argtypes = [ctypes.POINTER(ctypes.c_int)] + cu_driver_get_version.restype = ctypes.c_int + version = ctypes.c_int() + status = cu_driver_get_version(ctypes.byref(version)) + if status != 0: + raise RuntimeError(f"Failed to query CUDA driver version via cuDriverGetVersion() (status={status}).") + return version.value diff --git a/cuda_pathfinder/tests/test_driver_lib_loading.py b/cuda_pathfinder/tests/test_driver_lib_loading.py index bf62a17d703..b97453c9b5a 100644 --- a/cuda_pathfinder/tests/test_driver_lib_loading.py +++ b/cuda_pathfinder/tests/test_driver_lib_loading.py @@ -25,6 +25,7 @@ _load_lib_no_cache, ) from cuda.pathfinder._dynamic_libs.subprocess_protocol import STATUS_NOT_FOUND, parse_dynamic_lib_subprocess_payload +from cuda.pathfinder._utils import driver_info from cuda.pathfinder._utils.platform_aware import IS_WINDOWS, quote_for_shell STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS", "see_what_works") @@ -157,3 +158,23 @@ def raise_child_process_failed(): assert abs_path is not None info_summary_append(f"abs_path={quote_for_shell(abs_path)}") assert os.path.isfile(abs_path) + + +def test_real_query_driver_cuda_version(info_summary_append): + driver_info._load_nvidia_dynamic_lib.cache_clear() + driver_info.query_driver_cuda_version.cache_clear() + try: + version = driver_info.query_driver_cuda_version() + except driver_info.QueryDriverCudaVersionError as exc: + if STRICTNESS == "all_must_work": + raise + info_summary_append(f"driver version unavailable: {exc.__class__.__name__}: {exc}") + return + finally: + driver_info._load_nvidia_dynamic_lib.cache_clear() + driver_info.query_driver_cuda_version.cache_clear() + + info_summary_append(f"driver_version={version.major}.{version.minor} (encoded={version.encoded})") + assert version.encoded > 0 + assert version.major == version.encoded // 1000 + assert version.minor == (version.encoded % 1000) // 10 diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py new file mode 100644 index 00000000000..21948dadafe --- /dev/null +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import ctypes + +import pytest + +from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL +from cuda.pathfinder._utils import driver_info + + +@pytest.fixture(autouse=True) +def _clear_driver_cuda_version_query_cache(): + driver_info.query_driver_cuda_version.cache_clear() + yield + driver_info.query_driver_cuda_version.cache_clear() + + +class _FakeCuDriverGetVersion: + def __init__(self, *, status: int, version: int): + self.argtypes = None + self.restype = None + self._status = status + self._version = version + + def __call__(self, version_ptr) -> int: + ctypes.cast(version_ptr, ctypes.POINTER(ctypes.c_int)).contents.value = self._version + return self._status + + +class _FakeDriverLib: + def __init__(self, *, status: int, version: int): + self.cuDriverGetVersion = _FakeCuDriverGetVersion(status=status, version=version) + + +def _loaded_cuda(abs_path: str) -> LoadedDL: + return LoadedDL( + abs_path=abs_path, + was_already_loaded_from_elsewhere=False, + _handle_uint=0xBEEF, + found_via="system-search", + ) + + +def test_query_driver_cuda_version_uses_windll_on_windows(monkeypatch): + fake_driver_lib = _FakeDriverLib(status=0, version=12080) + loaded_paths: list[str] = [] + + monkeypatch.setattr(driver_info, "IS_WINDOWS", True) + monkeypatch.setattr( + driver_info, + "_load_nvidia_dynamic_lib", + lambda _libname: _loaded_cuda(r"C:\Windows\System32\nvcuda.dll"), + ) + + def fake_windll(abs_path: str): + loaded_paths.append(abs_path) + return fake_driver_lib + + monkeypatch.setattr(driver_info.ctypes, "WinDLL", fake_windll, raising=False) + + assert driver_info._query_driver_cuda_version_int() == 12080 + assert loaded_paths == [r"C:\Windows\System32\nvcuda.dll"] + + +def test_query_driver_cuda_version_returns_parsed_dataclass(monkeypatch): + monkeypatch.setattr(driver_info, "_query_driver_cuda_version_int", lambda: 12080) + + assert driver_info.query_driver_cuda_version() == driver_info.DriverCudaVersion( + encoded=12080, + major=12, + minor=8, + ) + + +def test_query_driver_cuda_version_wraps_internal_failures(monkeypatch): + root_cause = RuntimeError("low-level query failed") + + def fail_query_driver_cuda_version_int() -> int: + raise root_cause + + monkeypatch.setattr(driver_info, "_query_driver_cuda_version_int", fail_query_driver_cuda_version_int) + + with pytest.raises( + driver_info.QueryDriverCudaVersionError, + match="Failed to query the CUDA driver version", + ) as exc_info: + driver_info.query_driver_cuda_version() + + assert exc_info.value.__cause__ is root_cause + + +def test_query_driver_cuda_version_int_raises_when_cuda_call_fails(monkeypatch): + fake_driver_lib = _FakeDriverLib(status=1, version=0) + + monkeypatch.setattr(driver_info, "IS_WINDOWS", False) + monkeypatch.setattr(driver_info, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_cuda("/usr/lib/libcuda.so.1")) + monkeypatch.setattr(driver_info.ctypes, "CDLL", lambda _abs_path: fake_driver_lib) + + with pytest.raises(RuntimeError, match=r"cuDriverGetVersion\(\) \(status=1\)"): + driver_info._query_driver_cuda_version_int() From 8a83a4f7d96bde82490801c4a0920af557e8c915 Mon Sep 17 00:00:00 2001 From: Xiakun Lu Date: Sat, 25 Apr 2026 09:42:42 -0700 Subject: [PATCH 039/244] [FEA]: Add support for pathfinder.find_bitcode_lib("nccl") (#1975) * Add NCCL bitcode library lookup Signed-off-by: Xiakun Lu * Remove default values for libname in _make_bitcode_lib_file and _bitcode_lib_dir_under Signed-off-by: Xiakun Lu * Keep entries of _SUPPORTED_BITCODE_LIBS_INFO in lexicographic order Signed-off-by: Xiakun Lu * Rename NCCL bitcode key to nccl_device Signed-off-by: Xiakun Lu --------- Signed-off-by: Xiakun Lu --- .../_static_libs/find_bitcode_lib.py | 6 +++ .../tests/test_find_bitcode_lib.py | 54 +++++++++++++------ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py index 46926ad48f7..ac038aadfe7 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py @@ -42,6 +42,12 @@ class _BitcodeLibInfo(TypedDict): ), "available_on_windows": True, }, + "nccl_device": { + "filename": "libnccl_device.bc", + "rel_path": "lib", + "site_packages_dirs": ("nvidia/nccl/lib",), + "available_on_windows": False, + }, "nvshmem_device": { "filename": "libnvshmem_device.bc", "rel_path": "lib", diff --git a/cuda_pathfinder/tests/test_find_bitcode_lib.py b/cuda_pathfinder/tests/test_find_bitcode_lib.py index 7368722d295..659b068f0ff 100644 --- a/cuda_pathfinder/tests/test_find_bitcode_lib.py +++ b/cuda_pathfinder/tests/test_find_bitcode_lib.py @@ -18,7 +18,13 @@ STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS", "see_what_works") assert STRICTNESS in ("see_what_works", "all_must_work") -BCL_FILENAME = find_bitcode_lib_module._SUPPORTED_BITCODE_LIBS_INFO["device"]["filename"] + +def _bitcode_lib_info(libname: str): + return find_bitcode_lib_module._SUPPORTED_BITCODE_LIBS_INFO[libname] + + +def _bitcode_lib_filename(libname: str) -> str: + return _bitcode_lib_info(libname)["filename"] @pytest.fixture @@ -30,15 +36,20 @@ def clear_find_bitcode_lib_cache(): get_cuda_path_or_home.cache_clear() -def _make_bitcode_lib_file(dir_path: Path) -> str: +def _make_bitcode_lib_file(dir_path: Path, libname: str) -> str: dir_path.mkdir(parents=True, exist_ok=True) - file_path = dir_path / BCL_FILENAME + file_path = dir_path / _bitcode_lib_filename(libname) file_path.touch() return str(file_path) -def _bitcode_lib_dir_under(anchor_dir: Path) -> Path: - return anchor_dir / "nvvm" / "libdevice" +def _bitcode_lib_dir_under(anchor_dir: Path, libname: str) -> Path: + return anchor_dir / _bitcode_lib_info(libname)["rel_path"] + + +def _site_packages_bitcode_lib_dir_under(anchor_dir: Path, libname: str) -> Path: + rel_dir = _bitcode_lib_info(libname)["site_packages_dirs"][0] + return anchor_dir.joinpath(*rel_dir.split("/")) def _conda_anchor(conda_prefix: Path) -> Path: @@ -79,36 +90,47 @@ def test_locate_bitcode_lib(info_summary_append, libname): @pytest.mark.usefixtures("clear_find_bitcode_lib_cache") -def test_locate_bitcode_lib_search_order(monkeypatch, tmp_path): - site_packages_lib_dir = tmp_path / "site-packages" / "nvidia" / "cu13" / "nvvm" / "libdevice" - site_packages_path = _make_bitcode_lib_file(site_packages_lib_dir) +@pytest.mark.parametrize("libname", SUPPORTED_BITCODE_LIBS) +def test_locate_bitcode_lib_search_order(monkeypatch, tmp_path, libname): + site_packages_lib_dir = _site_packages_bitcode_lib_dir_under(tmp_path / "site-packages", libname) + site_packages_path = _make_bitcode_lib_file(site_packages_lib_dir, libname) conda_prefix = tmp_path / "conda-prefix" - conda_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(_conda_anchor(conda_prefix))) + conda_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(_conda_anchor(conda_prefix), libname), libname) cuda_home = tmp_path / "cuda-home" - cuda_home_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(cuda_home)) + cuda_home_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(cuda_home, libname), libname) + + site_packages_sub_dirs = tuple( + tuple(rel_dir.split("/")) for rel_dir in _bitcode_lib_info(libname)["site_packages_dirs"] + ) + + def find_expected_sub_dir(sub_dir): + assert sub_dir in site_packages_sub_dirs + if sub_dir == site_packages_sub_dirs[0]: + return [str(site_packages_lib_dir)] + return [] monkeypatch.setattr( find_bitcode_lib_module, "find_sub_dirs_all_sitepackages", - lambda _sub_dir: [str(site_packages_lib_dir)], + find_expected_sub_dir, ) monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) monkeypatch.setenv("CUDA_HOME", str(cuda_home)) monkeypatch.delenv("CUDA_PATH", raising=False) - located_lib = locate_bitcode_lib("device") + located_lib = locate_bitcode_lib(libname) assert located_lib.abs_path == site_packages_path assert located_lib.found_via == "site-packages" os.remove(site_packages_path) - located_lib = locate_bitcode_lib("device") + located_lib = locate_bitcode_lib(libname) assert located_lib.abs_path == conda_path assert located_lib.found_via == "conda" os.remove(conda_path) - located_lib = locate_bitcode_lib("device") + located_lib = locate_bitcode_lib(libname) assert located_lib.abs_path == cuda_home_path assert located_lib.found_via == "CUDA_PATH" @@ -116,7 +138,7 @@ def test_locate_bitcode_lib_search_order(monkeypatch, tmp_path): @pytest.mark.usefixtures("clear_find_bitcode_lib_cache") def test_find_bitcode_lib_not_found_error_includes_cuda_home_directory_listing(monkeypatch, tmp_path): cuda_home = tmp_path / "cuda-home" - lib_dir = _bitcode_lib_dir_under(cuda_home) + lib_dir = _bitcode_lib_dir_under(cuda_home, "device") lib_dir.mkdir(parents=True, exist_ok=True) extra_file = lib_dir / "README.txt" extra_file.write_text("placeholder", encoding="utf-8") @@ -134,7 +156,7 @@ def test_find_bitcode_lib_not_found_error_includes_cuda_home_directory_listing(m find_bitcode_lib("device") message = str(exc_info.value) - expected_missing_file = os.path.join(str(lib_dir), BCL_FILENAME) + expected_missing_file = os.path.join(str(lib_dir), _bitcode_lib_filename("device")) assert f"No such file: {expected_missing_file}" in message assert f'listdir("{lib_dir}"):' in message assert "README.txt" in message From 7f97d90f09087f390e3a3ec720f99af40a8d0104 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 27 Apr 2026 14:41:20 -0700 Subject: [PATCH 040/244] [doc-only] docs(pathfinder): prepare 1.5.4 release notes (#1981) * docs(pathfinder): prepare 1.5.4 release notes Add cuda-pathfinder 1.5.4 release notes and register 1.5.4 in nv-versions so the published docs include the new version entry. Made-with: Cursor * fix(ci): harden backport run lookup for artifact downloads Backport artifact downloads were relying on `gh run list -w ci.yml -s success`, which can fail to return runs even when the branch has successful CI artifacts. Move the lookup into a shared helper that queries completed `CI` runs and filters for successful results explicitly, so Linux and Windows workflows resolve prior-branch bindings artifacts reliably. Made-with: Cursor * Revert "fix(ci): harden backport run lookup for artifact downloads" This reverts commit fd31eb24265e9f240bfc396cfc14e76487041f10. --- cuda_pathfinder/docs/nv-versions.json | 4 ++++ .../docs/source/release/1.5.4-notes.rst | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 cuda_pathfinder/docs/source/release/1.5.4-notes.rst diff --git a/cuda_pathfinder/docs/nv-versions.json b/cuda_pathfinder/docs/nv-versions.json index 161f612d7b4..379c772ebee 100644 --- a/cuda_pathfinder/docs/nv-versions.json +++ b/cuda_pathfinder/docs/nv-versions.json @@ -3,6 +3,10 @@ "version": "latest", "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/latest/" }, + { + "version": "1.5.4", + "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.5.4/" + }, { "version": "1.5.3", "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.5.3/" diff --git a/cuda_pathfinder/docs/source/release/1.5.4-notes.rst b/cuda_pathfinder/docs/source/release/1.5.4-notes.rst new file mode 100644 index 00000000000..97853be7f37 --- /dev/null +++ b/cuda_pathfinder/docs/source/release/1.5.4-notes.rst @@ -0,0 +1,21 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +.. py:currentmodule:: cuda.pathfinder + +``cuda-pathfinder`` 1.5.4 Release notes +======================================= + +Highlights +---------- + +* Add ``find_bitcode_lib("nccl_device")`` support. + (`PR #1975 `_) + +Internal maintenance +-------------------- + +* On Linux, make the ``.so`` glob fallback prefer the newest matching library + when an exact SONAME match is unavailable. This improves internal consistency + in rare fallback cases and is not expected to affect most users. + (`PR #1966 `_) From 11347ff956596cb8ab95766b08285821fb16bc95 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Mon, 27 Apr 2026 18:38:05 -0400 Subject: [PATCH 041/244] Add torch.Tensor fast path for StridedMemoryView via AOTI tensor bridge (#1894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add torch.Tensor fast path for StridedMemoryView via AOTI tensor bridge Provide a fast path for constructing a StridedMemoryView from a torch.Tensor by reading tensor metadata directly through PyTorch's AOT Inductor (AOTI) stable C ABI, avoiding DLPack/CAI protocol overhead (~10 ns per tensor via pointer arithmetic). Key design: - Vendored AOTI shim header (aoti_shim.h) with extern "C" wrapping - _tensor_bridge.pyx loaded lazily (only when a torch.Tensor is first passed) to avoid undefined AOTI symbols at import time - RTLD_GLOBAL bootstrap via sys.modules["torch._C"] before loading _tensor_bridge.so - torch detection via type(obj).__module__.startswith("torch") - PyTorch is NOT a build-time or run-time dependency of cuda.core Closes NVIDIA/cuda-python#749 Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Clean up tensor bridge: remove unused AOTI decls, lazy dtype, drop empty .pxd - Remove unused aoti_torch_get_numel and aoti_torch_get_storage_offset declarations from aoti_shim.h and _tensor_bridge.pyx - Fix license headers on new files to 2026 (not 2024-2026) - Delete empty _tensor_bridge.pxd (nothing cimports from it) - Defer numpy dtype resolution for torch tensors: store raw AOTI dtype code in metadata, compute itemsize from a cheap lookup table, and only resolve the full numpy dtype on first .dtype access via get_dtype() Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Move torch tensor fast path into each from_* classmethod Instead of short-circuiting in __init__ and from_any_interface, add the AOTI fast path check to from_dlpack, from_cuda_array_interface, and from_array_interface. This ensures torch tensors always take the fast path regardless of which constructor the user calls. Simplify from_any_interface and _StridedMemoryViewProxy to just delegate to the from_* methods (which now handle torch internally). Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Add stream ordering for torch tensor bridge When stream_ptr is not -1, establish stream ordering between PyTorch's current CUDA stream (the producer) and the consumer stream, using the same event record + stream wait pattern as the CAI path. Uses aoti_torch_get_current_cuda_stream to get the producer stream, matching what PyTorch's own __dlpack__ does internally. Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Extract reusable sync_torch_stream and apply to CAI path Factor out stream ordering into a cpdef sync_torch_stream() helper in _tensor_bridge.pyx, callable from both C (view_as_torch_tensor) and Python (_memoryview.pyx). Apply the same stream ordering in view_as_cai for torch tensors: PyTorch's __cuda_array_interface__ reports version 2 and omits the "stream" field, so the standard CAI sync path is a no-op — leaving the consumer with no guarantee that the producer's work is visible. We now detect torch tensors in the CAI path and query PyTorch's current CUDA stream via AOTI to establish proper ordering. Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Nits: add check_aoti helper, size_t itemsize, 2D sliced test - Add check_aoti() inline helper to replace repetitive err/raise patterns for AOTI calls (one-liner per call) - Change itemsize type from int to size_t - Add test_torch_tensor_bridge_sliced_2d test case Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Revert itemsize to int, memoize int(stream_ptr) - Revert itemsize back to int (size_t was unnecessary for small values) - Memoize int(stream_ptr) to avoid redundant Python operator conversion Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Use except?-1 instead of except* for check_aoti Better Cython 3 performance: except?-1 avoids the overhead of except* which always checks for exceptions. Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Require PyTorch >= 2.3 for tensor bridge, move imports to module level The AOTI stable C ABI functions we use (get_dim, get_dtype, get_device_type, get_device_index, get_current_cuda_stream, complex dtype constants) were all introduced in PyTorch 2.3.0. Earlier versions are missing some or all of them. _is_torch_tensor now returns False when torch < 2.3, causing a graceful fallback to the standard DLPack/CAI paths. The version check result is memoized in a module-level variable. Also move `import ctypes, sys` from _get_tensor_bridge to module level. Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Add tensor bridge entry to 1.0.0 release notes Document the AOTI-based fast path for torch.Tensor in StridedMemoryView with ~10-20x speedup and stream ordering support. Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Update speedup range in release notes to match benchmarks Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Document THPVariable layout change across PyTorch versions The cdata field changed from MaybeOwned (2.3-2.9) to at::Tensor (2.10+). Both layouts are compatible with our offset trick. Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Cache type check in _is_torch_tensor for ~20% speedup Cache the result of the torch tensor type check (module + hasattr + version) keyed by type(obj). Subsequent calls for the same type are a single dict lookup (~76 ns) instead of the full check (~186 ns). Non-torch objects also benefit as the cache returns False immediately after the first miss. Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Add upper bound to torch version check (cap at 2.11) The pyobj_to_aten_handle trick and AtenTensorHandle == at::Tensor* identity are undocumented internals that could change. Cap at the latest tested version so unknown future versions fall back to the standard DLPack/CAI paths. Bump after verifying each new release. Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Update module docstring to document both THPVariable layouts Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Use except?-1 for sync_torch_stream instead of except* Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Fix linter errors Co-Authored-By: Emilio Castillo Co-Authored-By: Claude Opus 4.6 (1M context) * Fix pyobj_to_aten_handle for PyTorch 2.3–2.9 MaybeOwned layout In PyTorch 2.3–2.9, THPVariable::cdata is c10::MaybeOwned, whose first member is bool isBorrowed_ (padded to 8 bytes) before the at::Tensor union member. The previous code always offset by sizeof(PyObject) which pointed to the bool tag (0x0), causing a segfault when AOTI functions dereferenced it as at::Tensor*. Add _get_cdata_extra_offset() that checks the torch version at runtime and adds 8 bytes for torch < 2.10 (MaybeOwned era). The result is memoized after the first call. Tested across PyTorch 2.3.1, 2.4.1, 2.5.1, 2.6.0, 2.7.1, 2.8.0, 2.9.1, 2.10.0, and 2.11.0 with CPU tensors (9 dtypes, sliced tensors, 0d/1d/4d shapes). Co-Authored-By: Claude Opus 4.6 (1M context) * Consolidate torch tensor bridge tests into TestViewCPU/TestViewGPU Move the 9 standalone torch tensor bridge tests (1d, nd, scalar, empty, non-contiguous, sliced, sliced-2d, cpu, decorator) into the existing parametrized TestViewCPU and TestViewGPU classes. Each torch sample now runs through from_any_interface, the args_viewable_as_strided_memory decorator, and the deprecated __init__ path. Add helpers (_arr_ptr, _arr_strides_in_counts, _arr_is_c_contiguous, _arr_is_writeable) so _check_view works uniformly across numpy, cupy, numba, and torch arrays. Retain test_torch_tensor_bridge_dtypes and test_torch_tensor_bridge_bfloat16 as standalone tests since they verify dtype mapping specifically. Co-Authored-By: Claude Opus 4.6 (1M context) * Extract _arr_size helper for torch/numpy size compatibility Co-Authored-By: Claude Opus 4.6 (1M context) * Fix ruff formatting in test_utils.py Co-Authored-By: Claude Opus 4.6 (1M context) * Add readonly comment and fix vendored header license to BSD-3-Clause - Document why readonly=False is correct for torch tensors: PyTorch always reports tensors as writable via both DLPack (flags=0) and CAI (data=(ptr, False)), and the AOTI C ABI has no readonly query. - Change the vendored aoti_shim.h SPDX from Apache-2.0 to BSD-3-Clause to match PyTorch's actual license. Co-Authored-By: Claude Opus 4.6 (1M context) * Merge bfloat16 test into test_torch_tensor_bridge_dtypes parametrization Add bfloat16 as a pytest.param with a skipif mark for ml_dtypes, removing the separate test_torch_tensor_bridge_bfloat16 function. Co-Authored-By: Claude Opus 4.6 (1M context) * Fix SPDX linter: use PyTorch copyright in vendored header Replace the NVIDIA SPDX header with PyTorch's original BSD-3-Clause copyright text (from PyTorch LICENSE lines 3-11), following the same pattern as the vendored dlpack.h. Add aoti_shim.h to .spdx-ignore to bypass the NVIDIA-specific copyright check. Co-Authored-By: Claude Opus 4.6 (1M context) * Fix Windows build: generate stub import library for AOTI symbols On Windows, MSVC requires a .lib to resolve __declspec(dllimport) symbols at link time. The AOTI symbols live in torch_cpu.dll (loaded by `import torch` at runtime) but torch is not a build-time dependency. Add: - aoti_shim.def: symbol list for generating the stub import library - AOTI_SHIM_API macro in aoti_shim.h: expands to __declspec(dllimport) on Windows, empty on Linux/macOS - build_hooks.py: on Windows, run `lib /DEF:... /OUT:...` to generate the stub .lib and link _tensor_bridge against it The stub .lib (~1KB) contains no code — it tells the linker that the symbols will come from torch_cpu.dll. At runtime, `import torch` loads the DLL before our extension is imported. Co-Authored-By: Claude Opus 4.6 (1M context) * Exclude torch DLLs from delvewheel repair on Windows The _tensor_bridge extension links against torch_cpu.dll via a stub import library. delvewheel tries to bundle this DLL into the wheel and fails because torch is not installed in the build environment. Exclude torch_cpu.dll and torch_python.dll with --no-dll so delvewheel skips them — they are provided by the user's torch install. Co-Authored-By: Claude Opus 4.6 (1M context) * Fix delvewheel flag: use --exclude instead of --no-dll delvewheel uses --exclude (not --no-dll) and semicolons as path separators on Windows. Co-Authored-By: Claude Opus 4.6 (1M context) * fix merge conflict resolution * [pre-commit.ci] auto code formatting * Add strided layout guard to tensor bridge, reject sparse tensors Check aoti_torch_get_layout() before extracting metadata — reject non-strided tensors (sparse, mkldnn, etc.) whose shape/strides are not meaningful for dense memory access. We intentionally skip the other Python-level __dlpack__ guards (requires_grad, is_conj, is_neg, wrong-device) for the same reason PyTorch's own __dlpack_c_exchange_api__ C path skips them: the C-level exchange path is designed for performance-critical consumers. PyTorch's DLTensorFromPyObjectNoSync → toDLPackNonOwning performs zero safety checks (see aten/src/ATen/DLConvertor.cpp). Co-Authored-By: Claude Opus 4.6 (1M context) * Revert strided layout guard (symbols missing in torch 2.3–2.8) aoti_torch_get_layout was introduced in torch 2.9; referencing it in cdef extern causes an ImportError on torch 2.3–2.8 at .so load time. Remove the layout check entirely. Like PyTorch's own __dlpack_c_exchange_api__ C path (DLTensorFromPyObjectNoSync → toDLPackNonOwning), we skip all Python-level export guards (requires_grad, is_conj, is_neg, non-strided, wrong-device). Document this as a known limitation matching upstream precedent. Verified: all 9 torch versions (2.3.1–2.11.0) pass again. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review comments: dtypes, stale cache, stream_ptr, sync notes - Add uint16/uint32/uint64 to AOTI dtype and itemsize maps (fixes regression where these torch dtypes would raise TypeError instead of being handled by the bridge) - Clear buf._dtype when repopulating a reused StridedMemoryView to prevent returning a stale cached dtype - Reject stream_ptr=None for CUDA tensors with BufferError (matches DLPack semantics where None is ambiguous) - Add "keep in sync" comments to aoti_shim.h and aoti_shim.def per rwgk's review suggestion Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Emilio Castillo Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .spdx-ignore | 2 + cuda_core/build_hooks.py | 22 +- cuda_core/cuda/core/_include/aoti_shim.def | 37 ++ cuda_core/cuda/core/_include/aoti_shim.h | 117 +++++ cuda_core/cuda/core/_memoryview.pyx | 94 ++++ cuda_core/cuda/core/_tensor_bridge.pyx | 408 ++++++++++++++++++ cuda_core/docs/source/release/1.0.0-notes.rst | 35 ++ cuda_core/pyproject.toml | 2 +- cuda_core/tests/test_utils.py | 151 ++++++- 9 files changed, 848 insertions(+), 20 deletions(-) create mode 100644 cuda_core/cuda/core/_include/aoti_shim.def create mode 100644 cuda_core/cuda/core/_include/aoti_shim.h create mode 100644 cuda_core/cuda/core/_tensor_bridge.pyx create mode 100644 cuda_core/docs/source/release/1.0.0-notes.rst diff --git a/.spdx-ignore b/.spdx-ignore index 7263b5414f7..866b2274e06 100644 --- a/.spdx-ignore +++ b/.spdx-ignore @@ -10,5 +10,7 @@ cuda_bindings/examples/* # Vendored cuda_core/cuda/core/_include/dlpack.h +cuda_core/cuda/core/_include/aoti_shim.h +cuda_core/cuda/core/_include/aoti_shim.def qa/ctk-next.drawio.svg diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 16d393344b8..444da18eb13 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -11,6 +11,7 @@ import glob import os import re +import subprocess import sys import tempfile import zipfile @@ -182,6 +183,25 @@ def get_sources(mod_name): # related to free-threading builds. extra_compile_args += ["-DCYTHON_TRACE_NOGIL=1", "-DCYTHON_USE_SYS_MONITORING=0"] + # On Windows, _tensor_bridge.pyx needs a stub import library so the MSVC + # linker can resolve the AOTI symbols (they live in torch_cpu.dll at + # runtime). We generate the .lib from a .def file at build time. + _aoti_extra_link_args = [] + if sys.platform == "win32": + _def_file = os.path.join("cuda", "core", "_include", "aoti_shim.def") + _lib_file = os.path.join("build", "aoti_shim.lib") + os.makedirs("build", exist_ok=True) + subprocess.check_call( # noqa: S603 + ["lib", f"/DEF:{_def_file}", f"/OUT:{_lib_file}", "/MACHINE:X64"], # noqa: S607 + stdout=subprocess.DEVNULL, + ) + _aoti_extra_link_args = [_lib_file] + + def get_extra_link_args(mod_name): + if mod_name == "_tensor_bridge" and _aoti_extra_link_args: + return extra_link_args + _aoti_extra_link_args + return extra_link_args + ext_modules = tuple( Extension( f"cuda.core.{mod.replace(os.path.sep, '.')}", @@ -193,7 +213,7 @@ def get_sources(mod_name): + all_include_dirs, language="c++", extra_compile_args=extra_compile_args, - extra_link_args=extra_link_args, + extra_link_args=get_extra_link_args(mod), ) for mod in module_names() ) diff --git a/cuda_core/cuda/core/_include/aoti_shim.def b/cuda_core/cuda/core/_include/aoti_shim.def new file mode 100644 index 00000000000..5cc6897e815 --- /dev/null +++ b/cuda_core/cuda/core/_include/aoti_shim.def @@ -0,0 +1,37 @@ +; Stub import library definition for PyTorch's AOTI stable C ABI symbols. +; Used on Windows only: 'lib /DEF:aoti_shim.def /OUT:aoti_shim.lib /MACHINE:X64' +; generates a minimal import library that satisfies the MSVC linker. +; At runtime the symbols resolve from torch_cpu.dll (loaded by 'import torch'). +; +; IMPORTANT: Keep this export list in sync with the AOTI_SHIM_API declarations +; in aoti_shim.h. build_hooks.py turns this file into the stub import library +; that MSVC uses to link _tensor_bridge, so any added/removed/renamed AOTI +; symbol must be updated in both files. +LIBRARY torch_cpu.dll +EXPORTS + aoti_torch_get_data_ptr + aoti_torch_get_dim + aoti_torch_get_sizes + aoti_torch_get_strides + aoti_torch_get_dtype + aoti_torch_dtype_float16 + aoti_torch_dtype_float32 + aoti_torch_dtype_float64 + aoti_torch_dtype_bfloat16 + aoti_torch_dtype_uint8 + aoti_torch_dtype_uint16 + aoti_torch_dtype_uint32 + aoti_torch_dtype_uint64 + aoti_torch_dtype_int8 + aoti_torch_dtype_int16 + aoti_torch_dtype_int32 + aoti_torch_dtype_int64 + aoti_torch_dtype_bool + aoti_torch_dtype_complex32 + aoti_torch_dtype_complex64 + aoti_torch_dtype_complex128 + aoti_torch_get_device_type + aoti_torch_get_device_index + aoti_torch_device_type_cpu + aoti_torch_device_type_cuda + aoti_torch_get_current_cuda_stream diff --git a/cuda_core/cuda/core/_include/aoti_shim.h b/cuda_core/cuda/core/_include/aoti_shim.h new file mode 100644 index 00000000000..809bdb1a2a6 --- /dev/null +++ b/cuda_core/cuda/core/_include/aoti_shim.h @@ -0,0 +1,117 @@ +/* + * Vendored subset of PyTorch's AOT Inductor (AOTI) stable C ABI. + * Original: torch/csrc/inductor/aoti_torch/c/shim.h + * + * These are declarations only -- no definitions are provided. The actual + * symbols are exported by libtorch (loaded via torch._C with RTLD_GLOBAL) + * and resolved at runtime by the dynamic linker. This means PyTorch is + * NOT required at compile time. + * + * From PyTorch: + * + * Copyright (c) 2016- Facebook, Inc (Adam Paszke) + * Copyright (c) 2014- Facebook, Inc (Soumith Chintala) + * Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert) + * Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu) + * Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu) + * Copyright (c) 2011-2013 NYU (Clement Farabet) + * Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston) + * Copyright (c) 2006 Idiap Research Institute (Samy Bengio) + * Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz) + * + * SPDX-License-Identifier: BSD-3-Clause + * See https://github.com/pytorch/pytorch/blob/main/LICENSE + */ + +#ifndef CUDA_CORE_AOTI_SHIM_H +#define CUDA_CORE_AOTI_SHIM_H + +#include + +/* + * On Windows the AOTI symbols live in torch_cpu.dll. We consume them + * via __declspec(dllimport) and a stub import library generated from + * aoti_shim.def at build time. On Linux/macOS the symbols are made + * visible at runtime through ctypes.CDLL(torch._C, RTLD_GLOBAL). + */ +#ifdef _WIN32 +# define AOTI_SHIM_API __declspec(dllimport) +#else +# define AOTI_SHIM_API +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int32_t AOTITorchError; + +/* Opaque tensor handle -- corresponds to at::Tensor on the C++ side. */ +struct AtenTensorOpaque; +typedef struct AtenTensorOpaque* AtenTensorHandle; + +/* + * IMPORTANT: Keep the AOTI_SHIM_API declaration list below in sync with + * aoti_shim.def. On Windows, build_hooks.py turns that .def file into the + * stub import library that MSVC needs to link _tensor_bridge without making + * PyTorch a build-time dependency. If you add, remove, or rename an imported + * AOTI symbol here, update aoti_shim.def in the same change. + */ + +/* ---- tensor metadata --------------------------------------------------- */ + +AOTI_SHIM_API AOTITorchError aoti_torch_get_data_ptr( + AtenTensorHandle tensor, void** ret_data_ptr); + +AOTI_SHIM_API AOTITorchError aoti_torch_get_dim( + AtenTensorHandle tensor, int64_t* ret_dim); + +AOTI_SHIM_API AOTITorchError aoti_torch_get_sizes( + AtenTensorHandle tensor, int64_t** ret_sizes); + +AOTI_SHIM_API AOTITorchError aoti_torch_get_strides( + AtenTensorHandle tensor, int64_t** ret_strides); + +/* ---- dtype ------------------------------------------------------------- */ + +AOTI_SHIM_API AOTITorchError aoti_torch_get_dtype( + AtenTensorHandle tensor, int32_t* ret_dtype); + +AOTI_SHIM_API int32_t aoti_torch_dtype_float16(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_float32(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_float64(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_bfloat16(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_uint8(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_uint16(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_uint32(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_uint64(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_int8(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_int16(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_int32(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_int64(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_bool(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_complex32(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_complex64(void); +AOTI_SHIM_API int32_t aoti_torch_dtype_complex128(void); + +/* ---- device ------------------------------------------------------------ */ + +AOTI_SHIM_API AOTITorchError aoti_torch_get_device_type( + AtenTensorHandle tensor, int32_t* ret_device_type); + +AOTI_SHIM_API AOTITorchError aoti_torch_get_device_index( + AtenTensorHandle tensor, int32_t* ret_device_index); + +AOTI_SHIM_API int32_t aoti_torch_device_type_cpu(void); +AOTI_SHIM_API int32_t aoti_torch_device_type_cuda(void); + +/* ---- stream -------------------------------------------------------------- */ + +AOTI_SHIM_API AOTITorchError aoti_torch_get_current_cuda_stream( + int32_t device_index, void** ret_stream); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* CUDA_CORE_AOTI_SHIM_H */ diff --git a/cuda_core/cuda/core/_memoryview.pyx b/cuda_core/cuda/core/_memoryview.pyx index e0439ef23cd..3ebde8dcff1 100644 --- a/cuda_core/cuda/core/_memoryview.pyx +++ b/cuda_core/cuda/core/_memoryview.pyx @@ -10,7 +10,9 @@ from libc.stdint cimport intptr_t from cuda.core._layout cimport _StridedLayout, get_strides_ptr from cuda.core._stream import Stream +import ctypes import functools +import sys import warnings import numpy @@ -29,6 +31,73 @@ from cuda.core._utils.cuda_utils cimport HANDLE_RETURN from cuda.core._memory import Buffer +# --------------------------------------------------------------------------- +# Lazy tensor bridge (avoids loading _tensor_bridge.so until torch is used) +# --------------------------------------------------------------------------- + +cdef object _tensor_bridge = None +# Cache: type(obj) -> True/False for the torch tensor check. +# Once a type is seen, we never re-check. +cdef dict _torch_type_cache = {} +# Tri-state: None = not checked, True/False = result of version check +cdef object _torch_version_ok = None + +cdef inline bint _torch_version_check(): + """Return True if 2.3 <= torch <= 2.11 (known AOTI ABI range). Memoized. + + Lower bound: AOTI functions we use were introduced in PyTorch 2.3. + Upper bound: the ``pyobj_to_aten_handle`` trick relies on the + THPVariable struct layout (PyObject_HEAD followed by at::Tensor cdata) + and the identity ``AtenTensorHandle == at::Tensor*``. Both are + undocumented internals that could change in a future PyTorch version. + We cap at the latest version we have tested against; unknown versions + fall back to the standard DLPack/CAI paths. Bump the upper bound + after verifying a new PyTorch release. + """ + global _torch_version_ok + if _torch_version_ok is not None: + return _torch_version_ok + torch = sys.modules.get("torch") + if torch is None: + _torch_version_ok = False + return False + try: + major, minor = int(torch.__version__.split(".")[0]), \ + int(torch.__version__.split(".")[1]) + _torch_version_ok = (2, 3) <= (major, minor) <= (2, 11) + except (ValueError, IndexError): + _torch_version_ok = False + return _torch_version_ok + + +cdef inline bint _is_torch_tensor(object obj): + cdef type tp = type(obj) + cdef object cached = _torch_type_cache.get(tp) + if cached is not None: + return cached + cdef str mod = tp.__module__ or "" + cdef bint result = mod.startswith("torch") and hasattr(obj, "data_ptr") \ + and _torch_version_check() + _torch_type_cache[tp] = result + return result + + +cdef object _get_tensor_bridge(): + """Bootstrap AOTI symbols, then import _tensor_bridge on first use.""" + global _tensor_bridge + if _tensor_bridge is not None: + return _tensor_bridge + torch_C = sys.modules.get("torch._C") + if torch_C is None: + raise RuntimeError( + "torch._C is not loaded; cannot initialise the tensor bridge. " + "Make sure PyTorch is imported before passing a torch.Tensor.") + ctypes.CDLL(torch_C.__file__, mode=ctypes.RTLD_GLOBAL) + from cuda.core import _tensor_bridge as tb + _tensor_bridge = tb + return _tensor_bridge + + try: from ml_dtypes import bfloat16 except ImportError: @@ -150,6 +219,9 @@ cdef class StridedMemoryView: Stream pointer for synchronization. If ``None``, no synchronization is performed. """ cdef StridedMemoryView buf = StridedMemoryView.__new__(cls) + if _is_torch_tensor(obj): + _get_tensor_bridge().view_as_torch_tensor(obj, stream_ptr, buf) + return buf view_as_dlpack(obj, stream_ptr, buf) return buf @@ -165,6 +237,9 @@ cdef class StridedMemoryView: Stream pointer for synchronization. If ``None``, no synchronization is performed. """ cdef StridedMemoryView buf = StridedMemoryView.__new__(cls) + if _is_torch_tensor(obj): + _get_tensor_bridge().view_as_torch_tensor(obj, stream_ptr, buf) + return buf view_as_cai(obj, stream_ptr, buf) return buf @@ -178,6 +253,9 @@ cdef class StridedMemoryView: An object implementing the `__array_interface__ `_ protocol (e.g., a numpy array). """ cdef StridedMemoryView buf = StridedMemoryView.__new__(cls) + if _is_torch_tensor(obj): + _get_tensor_bridge().view_as_torch_tensor(obj, None, buf) + return buf view_as_array_interface(obj, buf) return buf @@ -187,6 +265,8 @@ cdef class StridedMemoryView: Tries `DLPack `_ first, then falls back to `__cuda_array_interface__ `_. + ``torch.Tensor`` objects are transparently handled via a fast AOTI path + regardless of which protocol is selected. Parameters ---------- @@ -480,6 +560,10 @@ cdef class StridedMemoryView: if self._dtype is None: if self.dl_tensor != NULL: self._dtype = dtype_dlpack_to_numpy(&self.dl_tensor.dtype) + elif isinstance(self.metadata, int): + # AOTI dtype code stored by the torch tensor bridge + self._dtype = _get_tensor_bridge().resolve_aoti_dtype( + self.metadata) elif self.metadata is not None: self._dtype = _typestr2dtype(self.metadata["typestr"]) return self._dtype @@ -1122,6 +1206,16 @@ cpdef StridedMemoryView view_as_cai(obj, stream_ptr, view=None): as_cu(h_event), producer_s)) HANDLE_RETURN(cydriver.cuStreamWaitEvent( consumer_s, as_cu(h_event), 0)) + elif _is_torch_tensor(obj): + # PyTorch's __cuda_array_interface__ reports version 2 and + # omits the "stream" field, so the standard CAI sync path + # above is a no-op for torch tensors. This is unsafe: the + # consumer has no guarantee that the producer's work is + # visible. We fix this by querying PyTorch's current CUDA + # stream via the AOTI stable C ABI and performing the same + # event-based stream ordering. + _get_tensor_bridge().sync_torch_stream( + buf.device_id, (stream_ptr)) return buf diff --git a/cuda_core/cuda/core/_tensor_bridge.pyx b/cuda_core/cuda/core/_tensor_bridge.pyx new file mode 100644 index 00000000000..388ca738dbb --- /dev/null +++ b/cuda_core/cuda/core/_tensor_bridge.pyx @@ -0,0 +1,408 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tensor bridge: extract PyTorch tensor metadata via the AOTI stable C ABI. + +PyTorch is NOT required at build time. At runtime the AOTI symbols are +resolved from ``torch._C`` (which is loaded with ``RTLD_GLOBAL``). + +The ``pyobj_to_aten_handle`` trick exploits the internal layout of +``THPVariable`` (PyTorch's Python tensor wrapper). + +In PyTorch 2.10+ ``cdata`` is ``at::Tensor`` directly:: + + struct THPVariable { + PyObject_HEAD + at::Tensor cdata; // <-- &cdata is usable as AtenTensorHandle + ... + }; + +In PyTorch 2.3–2.9 ``cdata`` was ``c10::MaybeOwned``, +whose first member is ``bool isBorrowed_`` (padded to 8 bytes), +followed by the ``at::Tensor`` union member:: + + struct THPVariable { + PyObject_HEAD + c10::MaybeOwned cdata; + // MaybeOwned layout: { bool isBorrowed_ (8 bytes); at::Tensor own_; } + ... + }; + +In both cases the address of the ``at::Tensor`` inside ``cdata`` is +accepted by the AOTI stable C ABI functions as an ``AtenTensorHandle``. +The extra 8-byte skip for the ``isBorrowed_`` member is determined +at runtime from the PyTorch version (see ``_get_cdata_extra_offset``). + +Offsetting past ``PyObject_HEAD`` gives us the handle +without any Python attribute access or method calls (~14 ns for all +7 metadata queries). + +Credit: Emilio Castillo (ecastillo@nvidia.com) – original tensor-bridge POC. + +.. note:: + + This module must NOT be imported at ``cuda.core`` load time. It is + loaded lazily (by ``_memoryview.pyx``) only when the user actually + passes a ``torch.Tensor``. The caller must ensure that + ``torch._C`` has been re-opened with ``RTLD_GLOBAL`` *before* + importing this module so that the AOTI symbols are visible. +""" + +from libc.stdint cimport intptr_t, int8_t, int16_t, int32_t, int64_t, uint8_t + +from cuda.core._memoryview cimport StridedMemoryView +from cuda.core._layout cimport _StridedLayout +from cuda.bindings cimport cydriver +from cuda.core._resource_handles cimport ( + EventHandle, + create_event_handle_noctx, + as_cu, +) +from cuda.core._utils.cuda_utils cimport HANDLE_RETURN + +cdef extern from "Python.h": + ctypedef struct PyObject: + pass + +cdef extern from "_include/aoti_shim.h": + ctypedef int32_t AOTITorchError + + ctypedef struct AtenTensorOpaque: + pass + ctypedef AtenTensorOpaque* AtenTensorHandle + + # tensor metadata + AOTITorchError aoti_torch_get_data_ptr(AtenTensorHandle, void**) + AOTITorchError aoti_torch_get_dim(AtenTensorHandle, int64_t*) + AOTITorchError aoti_torch_get_sizes(AtenTensorHandle, int64_t**) + AOTITorchError aoti_torch_get_strides(AtenTensorHandle, int64_t**) + + # dtype + AOTITorchError aoti_torch_get_dtype(AtenTensorHandle, int32_t*) + int32_t aoti_torch_dtype_float16() + int32_t aoti_torch_dtype_float32() + int32_t aoti_torch_dtype_float64() + int32_t aoti_torch_dtype_bfloat16() + int32_t aoti_torch_dtype_uint8() + int32_t aoti_torch_dtype_uint16() + int32_t aoti_torch_dtype_uint32() + int32_t aoti_torch_dtype_uint64() + int32_t aoti_torch_dtype_int8() + int32_t aoti_torch_dtype_int16() + int32_t aoti_torch_dtype_int32() + int32_t aoti_torch_dtype_int64() + int32_t aoti_torch_dtype_bool() + int32_t aoti_torch_dtype_complex32() + int32_t aoti_torch_dtype_complex64() + int32_t aoti_torch_dtype_complex128() + + # device + AOTITorchError aoti_torch_get_device_type(AtenTensorHandle, int32_t*) + AOTITorchError aoti_torch_get_device_index(AtenTensorHandle, int32_t*) + int32_t aoti_torch_device_type_cpu() + int32_t aoti_torch_device_type_cuda() + + # stream + AOTITorchError aoti_torch_get_current_cuda_stream(int32_t, void**) + +import numpy +import sys + + +# --------------------------------------------------------------------------- +# Module-level state (initialised at import time — AOTI symbols are +# guaranteed visible because _memoryview bootstraps RTLD_GLOBAL before +# importing us) +# --------------------------------------------------------------------------- + +cdef int32_t _DEVICE_TYPE_CPU = aoti_torch_device_type_cpu() +cdef int32_t _DEVICE_TYPE_CUDA = aoti_torch_device_type_cuda() +cdef dict _aoti_dtype_map = None +cdef dict _aoti_itemsize_map = None + +# Extra byte offset to skip before reaching the at::Tensor inside +# THPVariable::cdata. See _get_cdata_extra_offset() for details. +# Tri-state: -1 = not yet probed, 0 or 8 = cached result. +cdef Py_ssize_t _cdata_extra_offset = -1 + + +cdef Py_ssize_t _get_cdata_extra_offset(): + """Return the extra byte offset caused by ``MaybeOwned``'s bool member. + + In PyTorch 2.3–2.9 ``THPVariable::cdata`` is + ``c10::MaybeOwned``, whose first member is + ``bool isBorrowed_`` (padded to pointer alignment = 8 bytes). + The actual ``at::Tensor`` sits *after* that bool, so we must + skip 8 extra bytes. + + From PyTorch 2.10 onward ``cdata`` is ``at::Tensor`` directly, + so no extra offset is needed. + """ + global _cdata_extra_offset + if _cdata_extra_offset >= 0: + return _cdata_extra_offset + torch = sys.modules.get("torch") + if torch is None: + raise RuntimeError("torch must be imported before _tensor_bridge") + try: + major = int(torch.__version__.split(".")[0]) + minor = int(torch.__version__.split(".")[1]) + except (ValueError, IndexError): + raise RuntimeError( + f"Cannot parse torch version: {torch.__version__!r}") + if (major, minor) < (2, 10): + _cdata_extra_offset = 8 # skip MaybeOwned::isBorrowed_ padding + else: + _cdata_extra_offset = 0 # at::Tensor directly at base + return _cdata_extra_offset + + +# --------------------------------------------------------------------------- +# pointer extraction +# --------------------------------------------------------------------------- + +cdef inline AtenTensorHandle pyobj_to_aten_handle(object obj): + """Extract AtenTensorHandle by offsetting past PyObject_HEAD. + + In PyTorch 2.3–2.9 the first field after PyObject_HEAD is + ``c10::MaybeOwned cdata``, whose ``isBorrowed_`` + bool member (padded to 8 bytes) precedes the actual + ``at::Tensor``. From 2.10 onward ``cdata`` is ``at::Tensor`` + directly. The extra offset is determined once by + :func:`_get_cdata_extra_offset` and cached. + """ + return ( + obj + sizeof(PyObject) + _get_cdata_extra_offset()) + + +cdef inline int check_aoti(AOTITorchError err, const char* name) except? -1: + """Raise RuntimeError if an AOTI call returned a non-zero error code.""" + if err != 0: + raise RuntimeError(f"{name.decode()} failed") + return 0 + + +# --------------------------------------------------------------------------- +# dtype mapping (AOTI int32 -> numpy dtype) +# --------------------------------------------------------------------------- + +cdef dict _build_dtype_map(): + try: + from ml_dtypes import bfloat16 as _bf16 # noqa: F811 + has_bfloat16 = True + except ImportError: + has_bfloat16 = False + + cdef dict m = { + aoti_torch_dtype_float16(): numpy.dtype(numpy.float16), + aoti_torch_dtype_float32(): numpy.dtype(numpy.float32), + aoti_torch_dtype_float64(): numpy.dtype(numpy.float64), + aoti_torch_dtype_uint8(): numpy.dtype(numpy.uint8), + aoti_torch_dtype_uint16(): numpy.dtype(numpy.uint16), + aoti_torch_dtype_uint32(): numpy.dtype(numpy.uint32), + aoti_torch_dtype_uint64(): numpy.dtype(numpy.uint64), + aoti_torch_dtype_int8(): numpy.dtype(numpy.int8), + aoti_torch_dtype_int16(): numpy.dtype(numpy.int16), + aoti_torch_dtype_int32(): numpy.dtype(numpy.int32), + aoti_torch_dtype_int64(): numpy.dtype(numpy.int64), + aoti_torch_dtype_bool(): numpy.dtype(numpy.bool_), + aoti_torch_dtype_complex64(): numpy.dtype(numpy.complex64), + aoti_torch_dtype_complex128(): numpy.dtype(numpy.complex128), + } + if has_bfloat16: + m[aoti_torch_dtype_bfloat16()] = numpy.dtype(_bf16) + return m + + +cdef object _get_aoti_dtype(int32_t dtype_code): + global _aoti_dtype_map + if _aoti_dtype_map is None: + _aoti_dtype_map = _build_dtype_map() + result = _aoti_dtype_map.get(dtype_code) + if result is None: + raise TypeError(f"Unsupported AOTI dtype code: {dtype_code}") + return result + + +def resolve_aoti_dtype(int32_t dtype_code): + """Python-callable wrapper around _get_aoti_dtype (for lazy resolution).""" + return _get_aoti_dtype(dtype_code) + + +cdef dict _build_itemsize_map(): + return { + aoti_torch_dtype_bool(): sizeof(uint8_t), + aoti_torch_dtype_uint8(): sizeof(uint8_t), + aoti_torch_dtype_uint16(): sizeof(int16_t), + aoti_torch_dtype_uint32(): sizeof(int32_t), + aoti_torch_dtype_uint64(): sizeof(int64_t), + aoti_torch_dtype_int8(): sizeof(int8_t), + aoti_torch_dtype_float16(): sizeof(int16_t), # no C float16 + aoti_torch_dtype_bfloat16(): sizeof(int16_t), # no C bfloat16 + aoti_torch_dtype_int16(): sizeof(int16_t), + aoti_torch_dtype_complex32(): 2 * sizeof(int16_t), # no C complex32 + aoti_torch_dtype_float32(): sizeof(float), + aoti_torch_dtype_int32(): sizeof(int32_t), + aoti_torch_dtype_complex64(): 2 * sizeof(float), + aoti_torch_dtype_float64(): sizeof(double), + aoti_torch_dtype_int64(): sizeof(int64_t), + aoti_torch_dtype_complex128(): 2 * sizeof(double), + } + + +cdef int _get_aoti_itemsize(int32_t dtype_code) except -1: + global _aoti_itemsize_map + if _aoti_itemsize_map is None: + _aoti_itemsize_map = _build_itemsize_map() + result = _aoti_itemsize_map.get(dtype_code) + if result is None: + raise TypeError(f"Unsupported AOTI dtype code: {dtype_code}") + return result + + +# --------------------------------------------------------------------------- +# Stream ordering helper +# --------------------------------------------------------------------------- + +cpdef int sync_torch_stream(int32_t device_index, + intptr_t consumer_s) except? -1: + """Establish stream ordering between PyTorch's current CUDA stream + and the given consumer stream. + + Records an event on PyTorch's current stream (the producer) and makes + the consumer stream wait on it. This is a no-op if both streams are + the same. + """ + cdef void* producer_s + cdef EventHandle h_event + + check_aoti(aoti_torch_get_current_cuda_stream(device_index, &producer_s), + b"aoti_torch_get_current_cuda_stream") + if producer_s != consumer_s: + with nogil: + h_event = create_event_handle_noctx( + cydriver.CUevent_flags.CU_EVENT_DISABLE_TIMING) + HANDLE_RETURN(cydriver.cuEventRecord( + as_cu(h_event), producer_s)) + HANDLE_RETURN(cydriver.cuStreamWaitEvent( + consumer_s, as_cu(h_event), 0)) + return 0 + + +# --------------------------------------------------------------------------- +# Public API: construct StridedMemoryView from a torch.Tensor +# --------------------------------------------------------------------------- + +def view_as_torch_tensor(object obj, object stream_ptr, view=None): + """Create/populate a :class:`StridedMemoryView` from a ``torch.Tensor``. + + This is a fast path that avoids DLPack/CAI protocol overhead by + reading tensor metadata directly through the AOTI stable C ABI. + + Parameters + ---------- + obj : torch.Tensor + The source tensor. + stream_ptr : int or None + Consumer stream pointer. When not ``-1``, stream ordering is + established between PyTorch's current CUDA stream (the producer) + and the consumer stream, matching the DLPack contract. + view : StridedMemoryView, optional + If provided, populate this existing view in-place. Otherwise a + new instance is created. + """ + cdef AtenTensorHandle handle = pyobj_to_aten_handle(obj) + cdef void* data_ptr + cdef int64_t ndim + cdef int64_t* sizes_ptr + cdef int64_t* strides_ptr + cdef int32_t dtype_code + cdef int32_t device_type, device_index + cdef StridedMemoryView buf + cdef int itemsize + cdef intptr_t _stream_ptr_int + cdef _StridedLayout layout + + # Note: we intentionally skip PyTorch's Python-level __dlpack__ guards + # (requires_grad, is_conj, is_neg, non-strided layout, wrong-device) + # for the same reason PyTorch's own __dlpack_c_exchange_api__ C path + # skips them — the C-level exchange path is designed for performance- + # critical consumers. See DLTensorFromPyObjectNoSync in + # torch/csrc/Module.cpp which calls toDLPackNonOwning with zero checks. + + check_aoti(aoti_torch_get_data_ptr(handle, &data_ptr), + b"aoti_torch_get_data_ptr") + check_aoti(aoti_torch_get_dim(handle, &ndim), + b"aoti_torch_get_dim") + check_aoti(aoti_torch_get_sizes(handle, &sizes_ptr), + b"aoti_torch_get_sizes") + check_aoti(aoti_torch_get_strides(handle, &strides_ptr), + b"aoti_torch_get_strides") + check_aoti(aoti_torch_get_dtype(handle, &dtype_code), + b"aoti_torch_get_dtype") + check_aoti(aoti_torch_get_device_type(handle, &device_type), + b"aoti_torch_get_device_type") + check_aoti(aoti_torch_get_device_index(handle, &device_index), + b"aoti_torch_get_device_index") + + # -- populate StridedMemoryView -- + if view is not None: + buf = view + else: + buf = StridedMemoryView.__new__(StridedMemoryView) + + buf.ptr = data_ptr + buf._dtype = None # clear cached dtype (view may be reused) + # PyTorch always reports tensors as writable via both DLPack + # (flags=0, no DLPACK_FLAG_BITMASK_READ_ONLY) and CAI + # (__cuda_array_interface__["data"] = (ptr, False)). Tensors that + # cannot be safely exported (requires_grad, conjugate, non-strided) + # are rejected with BufferError rather than marked read-only. + # The AOTI C ABI has no readonly query either, so False is correct. + buf.readonly = False + buf.exporting_obj = obj + buf.dl_tensor = NULL + buf.metadata = None + buf._buffer = None + + if device_type == _DEVICE_TYPE_CPU: + buf.device_id = -1 + buf.is_device_accessible = False + elif device_type == _DEVICE_TYPE_CUDA: + buf.device_id = device_index + buf.is_device_accessible = True + + # -- stream ordering (matches the DLPack contract) -- + # stream_ptr=None is ambiguous for CUDA tensors — the caller must + # explicitly choose -1 (no sync) or a valid stream pointer. + if stream_ptr is None: + raise BufferError( + "stream_ptr=None is ambiguous for CUDA tensors; " + "pass stream_ptr=-1 to opt out of synchronization, " + "or pass a valid stream pointer") + _stream_ptr_int = int(stream_ptr) + if _stream_ptr_int != -1: + sync_torch_stream(device_index, _stream_ptr_int) + else: + raise BufferError( + f"Unsupported device type from torch tensor " + f"(AOTI device type id: {device_type})") + + # Defer full numpy dtype resolution until first .dtype access. + # Store the raw AOTI dtype code in metadata for lazy lookup. + buf.metadata = dtype_code + + # Build _StridedLayout. init_from_ptr copies shape/strides so we are + # safe even though they are borrowed pointers. + itemsize = _get_aoti_itemsize(dtype_code) + layout = _StridedLayout.__new__(_StridedLayout) + layout.init_from_ptr( + ndim, + sizes_ptr, + strides_ptr, + itemsize, + ) + buf._layout = layout + + return buf diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst new file mode 100644 index 00000000000..34eff571005 --- /dev/null +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -0,0 +1,35 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +.. currentmodule:: cuda.core + +``cuda.core`` 1.0.0 Release Notes +================================= + + +Highlights +---------- + +- TBD + + +New features +------------ + +- TBD + + +Fixes and enhancements +----------------------- + +- :class:`~utils.StridedMemoryView` now provides a fast path for ``torch.Tensor`` + objects via PyTorch's AOT Inductor (AOTI) stable C ABI. When a ``torch.Tensor`` + is passed to any ``from_*`` classmethod (``from_dlpack``, + ``from_cuda_array_interface``, ``from_array_interface``, or + ``from_any_interface``), tensor metadata is read directly from the underlying + C struct, bypassing the DLPack and CUDA Array Interface protocol overhead. + This yields ~7-20x faster ``StridedMemoryView`` construction for PyTorch + tensors (depending on whether stream ordering is required). Proper CUDA stream ordering is established between PyTorch's current + stream and the consumer stream, matching the DLPack synchronization contract. + Requires PyTorch >= 2.3. + (`#749 `__) diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index d82932616c8..aa403409894 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -117,4 +117,4 @@ archs = "native" [tool.cibuildwheel.windows] archs = "AMD64" before-build = "pip install delvewheel" -repair-wheel-command = "delvewheel repair --namespace-pkg cuda -w {dest_dir} {wheel}" +repair-wheel-command = "delvewheel repair --namespace-pkg cuda --exclude \"torch_cpu.dll;torch_python.dll\" -w {dest_dir} {wheel}" diff --git a/cuda_core/tests/test_utils.py b/cuda_core/tests/test_utils.py index 6228c2b2222..91075d85368 100644 --- a/cuda_core/tests/test_utils.py +++ b/cuda_core/tests/test_utils.py @@ -76,9 +76,43 @@ def convert_strides_to_counts(strides, itemsize): return tuple(s // itemsize for s in strides) -@pytest.mark.parametrize( - "in_arr,", - ( +def _arr_ptr(arr): + """Return the data pointer of *arr* regardless of its type.""" + if torch is not None and isinstance(arr, torch.Tensor): + return arr.data_ptr() + if isinstance(arr, np.ndarray): + return arr.ctypes.data + return gpu_array_ptr(arr) + + +def _arr_strides_in_counts(arr): + """Return strides in element counts for *arr* regardless of its type.""" + if torch is not None and isinstance(arr, torch.Tensor): + return tuple(arr.stride()) + return convert_strides_to_counts(arr.strides, arr.dtype.itemsize) + + +def _arr_size(arr): + """Return the number of elements in *arr*.""" + if torch is not None and isinstance(arr, torch.Tensor): + return arr.numel() + return arr.size + + +def _arr_is_c_contiguous(arr): + if torch is not None and isinstance(arr, torch.Tensor): + return arr.is_contiguous() + return arr.flags.c_contiguous if hasattr(arr, "flags") else arr.flags["C_CONTIGUOUS"] + + +def _arr_is_writeable(arr): + if torch is not None and isinstance(arr, torch.Tensor): + return True # torch tensors are writable by default + return arr.flags.writeable if hasattr(arr.flags, "writeable") else True + + +def _cpu_array_samples(): + samples = [ np.empty(3, dtype=np.int32), np.empty((6, 6), dtype=np.float64)[::2, ::2], np.empty((3, 4), order="F"), @@ -88,8 +122,27 @@ def convert_strides_to_counts(strides, itemsize): np.frombuffer(b""), marks=requires_module(np, "2.1"), ), - ), -) + ] + if torch is not None: + samples += [ + pytest.param(torch.arange(12, dtype=torch.float32), id="torch-1d"), + pytest.param(torch.arange(24, dtype=torch.float32).reshape(2, 3, 4), id="torch-nd"), + pytest.param(torch.tensor(42.0), id="torch-scalar"), + pytest.param(torch.empty(0, dtype=torch.float32), id="torch-empty"), + pytest.param( + torch.arange(12, dtype=torch.float32).reshape(3, 4).t(), + id="torch-non-contiguous", + ), + pytest.param(torch.arange(100, dtype=torch.int64)[10:20], id="torch-sliced"), + pytest.param( + torch.arange(60, dtype=torch.float32).reshape(6, 10)[1:4, 2:7], + id="torch-sliced-2d", + ), + ] + return samples + + +@pytest.mark.parametrize("in_arr,", _cpu_array_samples()) class TestViewCPU: def test_args_viewable_as_strided_memory_cpu(self, in_arr): @args_viewable_as_strided_memory((0,)) @@ -113,16 +166,16 @@ def test_strided_memory_view_cpu_init(self, in_arr): def _check_view(self, view, in_arr): assert isinstance(view, StridedMemoryView) - assert view.ptr == in_arr.ctypes.data - assert view.shape == in_arr.shape - assert view.size == in_arr.size - strides_in_counts = convert_strides_to_counts(in_arr.strides, in_arr.dtype.itemsize) - assert (in_arr.flags.c_contiguous and view.strides is None) or view.strides == strides_in_counts - assert view.dtype == in_arr.dtype + assert view.ptr == _arr_ptr(in_arr) + expected_shape = tuple(in_arr.shape) + assert view.shape == expected_shape + assert view.size == _arr_size(in_arr) + strides_in_counts = _arr_strides_in_counts(in_arr) + assert (_arr_is_c_contiguous(in_arr) and view.strides is None) or view.strides == strides_in_counts assert view.device_id == -1 assert view.is_device_accessible is False assert view.exporting_obj is in_arr - assert view.readonly is not in_arr.flags.writeable + assert view.readonly is not _arr_is_writeable(in_arr) def gpu_array_samples(): @@ -141,10 +194,38 @@ def gpu_array_samples(): pytest.param(numba_cuda.device_array((2,), dtype=np.int8), False, id="numba-cuda-int8"), pytest.param(numba_cuda.device_array((4, 2), dtype=np.float32), True, id="numba-cuda-float32"), ] + if torch is not None: + samples += [ + pytest.param(torch.arange(12, dtype=torch.float32, device="cuda"), True, id="torch-1d"), + pytest.param( + torch.arange(24, dtype=torch.float32, device="cuda").reshape(2, 3, 4), + True, + id="torch-nd", + ), + pytest.param(torch.tensor(42.0, dtype=torch.float32, device="cuda"), False, id="torch-scalar"), + pytest.param(torch.empty(0, dtype=torch.float32, device="cuda"), False, id="torch-empty"), + pytest.param( + torch.arange(12, dtype=torch.float32, device="cuda").reshape(3, 4).t(), + True, + id="torch-non-contiguous", + ), + pytest.param( + torch.arange(100, dtype=torch.int64, device="cuda")[10:20], + True, + id="torch-sliced", + ), + pytest.param( + torch.arange(60, dtype=torch.float32, device="cuda").reshape(6, 10)[1:4, 2:7], + True, + id="torch-sliced-2d", + ), + ] return samples def gpu_array_ptr(arr): + if torch is not None and isinstance(arr, torch.Tensor): + return arr.data_ptr() if cp is not None and isinstance(arr, cp.ndarray): return arr.data.ptr if numba_cuda is not None and isinstance(arr, numba_cuda.cudadrv.devicearray.DeviceNDArray): @@ -192,18 +273,18 @@ def test_strided_memory_view_init(self, in_arr, use_stream): def _check_view(self, view, in_arr, dev): assert isinstance(view, StridedMemoryView) assert view.ptr == gpu_array_ptr(in_arr) - assert view.shape == in_arr.shape - assert view.size == in_arr.size - strides_in_counts = convert_strides_to_counts(in_arr.strides, in_arr.dtype.itemsize) - if in_arr.flags["C_CONTIGUOUS"]: + expected_shape = tuple(in_arr.shape) + assert view.shape == expected_shape + assert view.size == _arr_size(in_arr) + strides_in_counts = _arr_strides_in_counts(in_arr) + if _arr_is_c_contiguous(in_arr): assert view.strides in (None, strides_in_counts) else: assert view.strides == strides_in_counts - assert view.dtype == in_arr.dtype assert view.device_id == dev.device_id assert view.is_device_accessible is True assert view.exporting_obj is in_arr - # can't test view.readonly with CuPy or Numba... + # can't test view.readonly with CuPy, Numba, or torch... def test_strided_memory_view_dlpack_export_numpy_roundtrip(): @@ -911,3 +992,37 @@ def test_array_interface_mask_rejected(): obj = _FakeArrayInterfaceWithMask(arr) with pytest.raises(BufferError, match="mask is not supported"): view_as_array_interface(obj) + + +_torch_skip = pytest.mark.skipif(torch is None, reason="PyTorch is not installed") + + +@_torch_skip +@pytest.mark.parametrize( + "dtype", + [ + pytest.param("float16", id="float16"), + pytest.param("float32", id="float32"), + pytest.param("float64", id="float64"), + pytest.param("int8", id="int8"), + pytest.param("int16", id="int16"), + pytest.param("int32", id="int32"), + pytest.param("int64", id="int64"), + pytest.param("uint8", id="uint8"), + pytest.param("bool", id="bool"), + pytest.param("complex64", id="complex64"), + pytest.param("complex128", id="complex128"), + pytest.param( + "bfloat16", + id="bfloat16", + marks=pytest.mark.skipif(ml_dtypes is None, reason="ml_dtypes is not installed"), + ), + ], +) +def test_torch_tensor_bridge_dtypes(init_cuda, dtype): + """Verify that dtype mapping via the tensor bridge matches torch's own dtype.""" + torch_dtype = getattr(torch, dtype) + a = torch.tensor([1, 0, 1], dtype=torch_dtype, device="cuda") + smv = StridedMemoryView.from_any_interface(a, stream_ptr=0) + assert smv.dtype.itemsize == a.element_size() + assert smv.ptr == a.data_ptr() From 76a8cee2f58970d6474a48df1f7f367d0c00a9ef Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Tue, 28 Apr 2026 13:57:27 -0400 Subject: [PATCH 042/244] BUG: Make nvmlGetFieldValues robust to empty field list (#1982) * BUG: Make nvmlGetFieldValues robust to empty field list * Pre commit --- cuda_bindings/cuda/bindings/nvml.pyx | 9 +++++++++ cuda_bindings/tests/nvml/test_nvlink.py | 5 +++++ cuda_core/cuda/core/system/_device.pyx | 10 ++++++++++ cuda_core/tests/system/test_system_device.py | 2 ++ 4 files changed, 26 insertions(+) diff --git a/cuda_bindings/cuda/bindings/nvml.pyx b/cuda_bindings/cuda/bindings/nvml.pyx index 0e2493a764b..d2596d72ebf 100644 --- a/cuda_bindings/cuda/bindings/nvml.pyx +++ b/cuda_bindings/cuda/bindings/nvml.pyx @@ -27030,6 +27030,11 @@ cpdef object device_get_field_values(intptr_t device, values): cdef FieldValue values_ = _cast_field_values(values) cdef nvmlFieldValue_t *ptr = values_._get_ptr() cdef unsigned int valuesCount = len(values) + + # Passing a valuesCount of 0 to nvmlDeviceGetFieldValues returns NVML_INVALID_ARGUMENT + if valuesCount == 0: + return values_ + with nogil: __status__ = nvmlDeviceGetFieldValues(device, valuesCount, ptr) check_status(__status__) @@ -27050,6 +27055,10 @@ cpdef device_clear_field_values(intptr_t device, values): cdef nvmlFieldValue_t *ptr = values_._get_ptr() cdef unsigned int valuesCount = len(values) + # Passing a valuesCount of 0 to nvmlDeviceClearFieldValues returns NVML_INVALID_ARGUMENT + if valuesCount == 0: + return values_ + with nogil: __status__ = nvmlDeviceClearFieldValues(device, valuesCount, ptr) check_status(__status__) diff --git a/cuda_bindings/tests/nvml/test_nvlink.py b/cuda_bindings/tests/nvml/test_nvlink.py index be82aa37453..912c90d7c17 100644 --- a/cuda_bindings/tests/nvml/test_nvlink.py +++ b/cuda_bindings/tests/nvml/test_nvlink.py @@ -10,6 +10,11 @@ def test_nvlink_get_link_count(all_devices): Checks that the link count of the device is same. """ for device in all_devices: + fields = nvml.FieldValue(0) + assert len(nvml.device_get_field_values(device, fields)) == 0 + + assert len(nvml.device_get_field_values(device, [])) == 0 + fields = nvml.FieldValue(1) fields[0].field_id = nvml.FieldId.DEV_NVLINK_LINK_COUNT value = nvml.device_get_field_values(device, fields)[0] diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index 987851054d5..8615be0c531 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -661,6 +661,11 @@ cdef class Device: :obj:`~_device.FieldValues` Container of field values corresponding to the requested field IDs. """ + # Passing a field_ids array of length 0 raises an InvalidArgumentError, + # so avoid that. + if len(field_ids) == 0: + return FieldValues(nvml.FieldValue(0)) + return FieldValues(nvml.device_get_field_values(self._handle, field_ids)) def clear_field_values(self, field_ids: list[int | tuple[int, int]]) -> None: @@ -675,6 +680,11 @@ cdef class Device: Each item may be either a single value from the :class:`FieldId` enum, or a pair of (:class:`FieldId`, scope ID). """ + # Passing a field_ids array of length 0 raises an InvalidArgumentError, + # so avoid that. + if len(field_ids) == 0: + return + nvml.device_clear_field_values(self._handle, field_ids) ########################################################################## diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index d0895dd2237..932022c3422 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -360,6 +360,8 @@ def test_field_values(): # TODO: Are there any fields that return double's? It would be good to # test those. + assert len(device.get_field_values([])) == 0 + field_ids = [ system.FieldId.DEV_TOTAL_ENERGY_CONSUMPTION, system.FieldId.DEV_PCIE_COUNT_TX_BYTES, From 3fc7c80adb6dad4a70081a2f943eacc7f2d42a62 Mon Sep 17 00:00:00 2001 From: Daniel Rodriguez Date: Tue, 28 Apr 2026 17:32:44 -0500 Subject: [PATCH 043/244] cuda.bindings benchmarks part 5 (#1964) * Improve Cpp harness with min-time for more stable collection * Improve Cpp harness with min-time for more stable collection * Remove limit and added drain * Remove limit and added drain * Lint * Improve cpp harness --- benchmarks/cuda_bindings/README.md | 2 + .../benchmarks/cpp/bench_event.cpp | 5 + .../benchmarks/cpp/bench_launch.cpp | 6 + .../benchmarks/cpp/bench_memory.cpp | 5 + .../benchmarks/cpp/bench_stream.cpp | 6 + .../benchmarks/cpp/bench_support.hpp | 176 +++++++++++++++++- benchmarks/cuda_bindings/compare.py | 43 +++-- 7 files changed, 226 insertions(+), 17 deletions(-) diff --git a/benchmarks/cuda_bindings/README.md b/benchmarks/cuda_bindings/README.md index f8d5ccf0436..cffca57bef3 100644 --- a/benchmarks/cuda_bindings/README.md +++ b/benchmarks/cuda_bindings/README.md @@ -47,12 +47,14 @@ To run the benchmarks combine the environment and task: ```bash # Run the Python benchmarks in the wheel environment pixi run -e wheel bench +pixi run -e wheel bench --min-time 0.1 # Run the Python benchmarks in the source environment pixi run -e source bench # Run the C++ benchmarks pixi run -e wheel bench-cpp +pixi run -e wheel bench-cpp --min-time 0.1 ``` Both runners automatically save results to JSON files in the benchmarks diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_event.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_event.cpp index 44cd6177786..c24aa983199 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_event.cpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_event.cpp @@ -45,6 +45,11 @@ int main(int argc, char** argv) { check_cu(cuStreamSynchronize(stream), "cuStreamSynchronize failed"); bench::BenchmarkSuite suite(options); + // Drain the persistent stream after calibration so event_record (which + // enqueues onto the stream) and event_synchronize start from a known state. + suite.set_post_calibrate([&]() { + check_cu(cuStreamSynchronize(stream), "post-calibrate sync failed"); + }); // --- event_create_destroy --- { diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp index 984c82fcf32..4897859a61a 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_launch.cpp @@ -238,6 +238,12 @@ int main(int argc, char** argv) { void* struct_params[] = {&struct_2048B}; bench::BenchmarkSuite suite(options); + // After calibration, drain the persistent stream so the first measured + // sample does not start on a backlogged stream. Calibration for enqueue- + // style ops (kernel launches) may queue many thousands of operations. + suite.set_post_calibrate([&]() { + check_cu(cuStreamSynchronize(stream), "post-calibrate sync failed"); + }); suite.run("launch.launch_empty_kernel", [&]() { check_cu(cuLaunchKernel(empty_kernel, 1, 1, 1, 1, 1, 1, 0, stream, nullptr, nullptr), diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_memory.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_memory.cpp index 4e71b73fb5e..803363be480 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_memory.cpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_memory.cpp @@ -52,6 +52,11 @@ int main(int argc, char** argv) { uint8_t host_dst[COPY_SIZE] = {}; bench::BenchmarkSuite suite(options); + // Drain the persistent stream after calibration so async benchmarks + // (mem_alloc_async_free_async) don't start measurement on a backlogged stream. + suite.set_post_calibrate([&]() { + check_cu(cuStreamSynchronize(stream), "post-calibrate sync failed"); + }); // --- mem_alloc_free --- { diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_stream.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_stream.cpp index 702e86aef02..95ad0790f9f 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_stream.cpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_stream.cpp @@ -38,6 +38,12 @@ int main(int argc, char** argv) { check_cu(cuStreamCreate(&stream, CU_STREAM_NON_BLOCKING), "cuStreamCreate failed"); bench::BenchmarkSuite suite(options); + // Drain the persistent stream after calibration for completeness. + // stream_create_destroy uses a local stream, but stream_query/synchronize + // observe the persistent one. + suite.set_post_calibrate([&]() { + check_cu(cuStreamSynchronize(stream), "post-calibrate sync failed"); + }); // --- stream_create_destroy --- { diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp index 8b541228667..1207ee7c4dc 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_support.hpp @@ -6,10 +6,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -22,6 +24,12 @@ struct Options { std::uint64_t warmups = 5; std::uint64_t values = 20; std::uint64_t runs = 20; + double min_time_sec = 0.0; + // Safety cap for the calibration doubling loop. Set high enough that even + // sub-nanosecond ops can reach typical --min-time targets (e.g. 100ms). + // A warning is printed if calibration hits this cap before reaching min-time. + std::uint64_t max_loops = 100000000; + std::uint64_t calibrate_rounds = 3; std::string output_path; std::string benchmark_name; }; @@ -46,6 +54,18 @@ inline Options parse_args(int argc, char** argv) { options.warmups = std::strtoull(argv[++i], nullptr, 10); continue; } + if (arg == "--min-time" && i + 1 < argc) { + options.min_time_sec = std::strtod(argv[++i], nullptr); + continue; + } + if (arg == "--max-loops" && i + 1 < argc) { + options.max_loops = std::strtoull(argv[++i], nullptr, 10); + continue; + } + if (arg == "--calibrate-rounds" && i + 1 < argc) { + options.calibrate_rounds = std::strtoull(argv[++i], nullptr, 10); + continue; + } if (arg == "--values" && i + 1 < argc) { options.values = std::strtoull(argv[++i], nullptr, 10); continue; @@ -68,6 +88,9 @@ inline Options parse_args(int argc, char** argv) { << " --warmups N Warmup values per run (default: 5)\n" << " --values N Timed values per run (default: 20)\n" << " --runs N Number of runs (default: 20)\n" + << " --min-time S Calibrate loops to reach S seconds per value\n" + << " --max-loops N Safety cap for calibration loop count (default: 100000000)\n" + << " --calibrate-rounds N Calibration passes (default: 3)\n" << " -o, --output F Write pyperf-compatible JSON to file\n" << " --name S Benchmark name (overrides default)\n"; std::exit(0); @@ -93,6 +116,82 @@ inline std::string iso_now() { return std::string(buf); } +// Calibrate loop count to hit a minimum wall time per value. +// Returns the chosen loop count. If `capped_out` is non-null, it is set to +// true when calibration reached `max_loops` before hitting `min_time_sec` +// (meaning --min-time was NOT actually satisfied by the calibration). +template +std::uint64_t calibrate_loops( + const Options& options, + Fn&& fn, + const std::function& post_calibrate = {}, + bool* capped_out = nullptr, + double* last_elapsed_out = nullptr +) { + if (options.min_time_sec <= 0.0) { + if (capped_out) *capped_out = false; + if (last_elapsed_out) *last_elapsed_out = 0.0; + return options.loops; + } + + // Allow callers (e.g. the explicit-loop overload) to request a minimum + // starting loop count via options.loops. + const std::uint64_t start_loops = std::max(1, options.loops); + const std::uint64_t max_loops = std::max(start_loops, options.max_loops); + const std::uint64_t rounds = std::max(1, options.calibrate_rounds); + + // Track the round that produced the best (largest) loop count so the + // returned loop count, capped flag, and last-elapsed time all describe + // the same round. + std::uint64_t best_loops = 0; + bool best_capped = false; + double best_elapsed = 0.0; + + for (std::uint64_t round = 0; round < rounds; ++round) { + std::uint64_t loops = start_loops; + bool round_capped = false; + double elapsed = 0.0; + + while (true) { + const auto t0 = std::chrono::steady_clock::now(); + for (std::uint64_t i = 0; i < loops; ++i) { + fn(); + } + const auto t1 = std::chrono::steady_clock::now(); + elapsed = std::chrono::duration(t1 - t0).count(); + + // Drain any state left behind by this probe (e.g. queued async + // work on a persistent stream) before the next probe, the next + // round, or the first measured warmup/value runs. + if (post_calibrate) { + post_calibrate(); + } + if (elapsed >= options.min_time_sec) { + break; + } + if (loops >= max_loops) { + round_capped = true; + break; + } + if (loops > max_loops / 2) { + loops = max_loops; + } else { + loops *= 2; + } + } + + if (loops >= best_loops) { + best_loops = loops; + best_capped = round_capped; + best_elapsed = elapsed; + } + } + + if (capped_out) *capped_out = best_capped; + if (last_elapsed_out) *last_elapsed_out = best_elapsed; + return best_loops; +} + // Run a benchmark function. The function signature is: void fn() — one call = one operation. // The harness calls fn() in a tight loop `loops` times per value. template @@ -235,22 +334,57 @@ class BenchmarkSuite { public: explicit BenchmarkSuite(Options options) : options_(std::move(options)) {} + // Post-calibration hook. If set, invoked between calibration probes so + // async benchmarks can drain state left behind by each probe before the + // next one runs. The final probe leaves the benchmark in a drained state + // before the first measured warmup/value. Can be overridden per-call via + // the `post_calibrate` parameter on `run()`. + void set_post_calibrate(std::function hook) { + post_calibrate_ = std::move(hook); + } + // Run a benchmark and record it. The name is used as the benchmark ID. + // If --min-time is set, loop count is auto-calibrated. `post_calibrate`, + // if provided, runs between calibration probes to reset async state. template - void run(const std::string& name, Fn&& fn) { - auto results = run_benchmark(options_, std::forward(fn)); + void run( + const std::string& name, + Fn&& fn, + std::function post_calibrate = {} + ) { + std::uint64_t loops = options_.loops; + Options custom = options_; + if (options_.min_time_sec > 0.0) { + loops = calibrate_and_warn(name, options_, fn, select_post_calibrate(post_calibrate)); + custom.loops = loops; + } + auto results = run_benchmark(custom, std::forward(fn)); print_summary(name, results); - entries_.push_back({name, options_.loops, std::move(results)}); + entries_.push_back({name, loops, std::move(results)}); } - // Run a benchmark with a custom loop count (for slow operations like compilation). + // Run a benchmark with a custom loop count (used as a floor for fast ops + // or a fixed count for slow ops like compilation). When --min-time is set, + // calibration still runs but starts from `loops_override` as the minimum. template - void run(const std::string& name, std::uint64_t loops_override, Fn&& fn) { + void run( + const std::string& name, + std::uint64_t loops_override, + Fn&& fn, + std::function post_calibrate = {} + ) { + std::uint64_t loops = loops_override; Options custom = options_; custom.loops = loops_override; + if (options_.min_time_sec > 0.0) { + Options calib_opts = options_; + calib_opts.loops = loops_override; // floor + loops = calibrate_and_warn(name, calib_opts, fn, select_post_calibrate(post_calibrate)); + custom.loops = loops; + } auto results = run_benchmark(custom, std::forward(fn)); print_summary(name, results); - entries_.push_back({name, loops_override, std::move(results)}); + entries_.push_back({name, loops, std::move(results)}); } // Write all collected benchmarks to the output file (if -o was given). @@ -263,6 +397,36 @@ class BenchmarkSuite { private: Options options_; std::vector entries_; + std::function post_calibrate_; + + std::function select_post_calibrate(const std::function& per_call) const { + if (per_call) { + return per_call; + } + return post_calibrate_; + } + + template + std::uint64_t calibrate_and_warn( + const std::string& name, + const Options& calib_opts, + Fn&& fn, + const std::function& post_calibrate + ) const { + bool capped = false; + double last_elapsed = 0.0; + std::uint64_t loops = calibrate_loops( + calib_opts, std::forward(fn), post_calibrate, &capped, &last_elapsed + ); + if (capped) { + std::cerr << "WARNING: " << name + << ": calibration hit --max-loops (" << calib_opts.max_loops + << ") before reaching --min-time (" << calib_opts.min_time_sec + << "s). Last sample: " << last_elapsed + << "s. Raise --max-loops to satisfy --min-time for this benchmark.\n"; + } + return loops; + } static void write_multi_pyperf_json( const std::string& output_path, diff --git a/benchmarks/cuda_bindings/compare.py b/benchmarks/cuda_bindings/compare.py index 6a3e94f3447..cb1c2fdede3 100644 --- a/benchmarks/cuda_bindings/compare.py +++ b/benchmarks/cuda_bindings/compare.py @@ -29,7 +29,7 @@ def load_benchmarks(path: Path) -> dict[str, list[float]]: name = run.get("metadata", {}).get("name", "") if name: break - values = [] + values: list[float] = [] for run in bench.get("runs", []): values.extend(run.get("values", [])) if name and values: @@ -37,6 +37,19 @@ def load_benchmarks(path: Path) -> dict[str, list[float]]: return results +def stats(values: list[float]) -> tuple[float, float, float, int]: + mean = statistics.mean(values) + stdev = statistics.pstdev(values) if len(values) > 1 else 0.0 + rsd = (stdev / mean) if mean else 0.0 + return mean, stdev, rsd, len(values) + + +def fmt_rsd(rsd: float | None) -> str: + if rsd is None: + return "-" + return f"{rsd * 100:.1f}%" + + def fmt_ns(seconds: float) -> str: ns = seconds * 1e9 if ns >= 1000: @@ -79,13 +92,16 @@ def main() -> None: # Header if cpp_benchmarks: - header = f"{'Benchmark':<{name_width}} {'C++ (mean)':>12} {'Python (mean)':>14} {'Overhead':>10}" + header = ( + f"{'Benchmark':<{name_width}} {'C++ (mean)':>12} {'C++ RSD':>8} " + f"{'Python (mean)':>14} {'Py RSD':>7} {'Overhead':>10}" + ) sep = "-" * len(header) print(sep) print(header) print(sep) else: - header = f"{'Benchmark':<{name_width}} {'Python (mean)':>14}" + header = f"{'Benchmark':<{name_width}} {'Python (mean)':>14} {'Py RSD':>7}" sep = "-" * len(header) print(sep) print(header) @@ -95,21 +111,26 @@ def main() -> None: py_vals = py_benchmarks.get(name) cpp_vals = cpp_benchmarks.get(name) - py_str = fmt_ns(statistics.mean(py_vals)) if py_vals else "-" - cpp_str = fmt_ns(statistics.mean(cpp_vals)) if cpp_vals else "-" + py_stats = stats(py_vals) if py_vals else None + cpp_stats = stats(cpp_vals) if cpp_vals else None + + py_str = fmt_ns(py_stats[0]) if py_stats else "-" + cpp_str = fmt_ns(cpp_stats[0]) if cpp_stats else "-" + py_rsd = fmt_rsd(py_stats[2]) if py_stats else "-" + cpp_rsd = fmt_rsd(cpp_stats[2]) if cpp_stats else "-" - if py_vals and cpp_vals: - py_mean = statistics.mean(py_vals) - cpp_mean = statistics.mean(cpp_vals) + if py_stats and cpp_stats: + py_mean = py_stats[0] + cpp_mean = cpp_stats[0] overhead_ns = (py_mean - cpp_mean) * 1e9 - overhead_str = f"+{overhead_ns:.0f} ns" + overhead_str = f"{overhead_ns:+.0f} ns" else: overhead_str = "-" if cpp_benchmarks: - print(f"{name:<{name_width}} {cpp_str:>12} {py_str:>14} {overhead_str:>10}") + print(f"{name:<{name_width}} {cpp_str:>12} {cpp_rsd:>8} {py_str:>14} {py_rsd:>7} {overhead_str:>10}") else: - print(f"{name:<{name_width}} {py_str:>14}") + print(f"{name:<{name_width}} {py_str:>14} {py_rsd:>7}") print(sep) From ac24a183e905b96ce997f364f12685cf066b0d54 Mon Sep 17 00:00:00 2001 From: Daniel Rodriguez Date: Tue, 28 Apr 2026 18:22:30 -0500 Subject: [PATCH 044/244] Ignore bench_support.hpp as binary [no-ci] (#1985) --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 222c26f0cf7..5c4e63b373e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,7 @@ *.h binary *.hpp binary # Exception: headers we own +benchmarks/cuda_bindings/benchmarks/cpp/*.hpp -binary text diff cuda_bindings/cuda/bindings/_bindings/*.h -binary text diff cuda_bindings/cuda/bindings/_lib/*.h -binary text diff cuda_core/cuda/core/_cpp/*.h -binary text diff From 6d4e82d894d6d997f965f2294fd55aa065a667c5 Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Tue, 28 Apr 2026 16:35:01 -0700 Subject: [PATCH 045/244] Rename GraphDef -> GraphDefinition and Condition -> GraphCondition (#1984) Spell out abbreviations for consistency with the rest of cuda.core (#1950) and add the Graph* prefix to Condition for consistency with GraphBuilder / GraphDefinition / GraphNode (#1945). Source file _graph_def.{pyx,pxd} and test files test_graphdef*.py renamed to match. 0.7.0 release notes converted to literal inline-code form for the old GraphDef name so :class: cross-references resolve cleanly in 1.0.0+ doc builds without rewriting historical claims. Made-with: Cursor --- cuda_core/cuda/core/__init__.py | 4 +- cuda_core/cuda/core/graph/__init__.pxd | 2 +- cuda_core/cuda/core/graph/__init__.py | 6 +- cuda_core/cuda/core/graph/_graph_builder.pyx | 10 +-- .../{_graph_def.pxd => _graph_definition.pxd} | 6 +- .../{_graph_def.pyx => _graph_definition.pyx} | 46 +++++------ cuda_core/cuda/core/graph/_graph_node.pyx | 52 ++++++------- cuda_core/cuda/core/graph/_subclasses.pxd | 4 +- cuda_core/cuda/core/graph/_subclasses.pyx | 30 +++---- cuda_core/docs/source/api.rst | 6 +- cuda_core/docs/source/release/0.7.0-notes.rst | 4 +- cuda_core/docs/source/release/1.0.0-notes.rst | 13 ++++ ...t_graphdef.py => test_graph_definition.py} | 78 +++++++++---------- ...ors.py => test_graph_definition_errors.py} | 46 +++++------ ...y => test_graph_definition_integration.py} | 12 +-- ...e.py => test_graph_definition_lifetime.py} | 54 ++++++------- ...n.py => test_graph_definition_mutation.py} | 22 +++--- cuda_core/tests/graph/test_graph_update.py | 10 +-- cuda_core/tests/helpers/misc.py | 2 +- cuda_core/tests/test_object_protocols.py | 24 +++--- 20 files changed, 222 insertions(+), 209 deletions(-) rename cuda_core/cuda/core/graph/{_graph_def.pxd => _graph_definition.pxd} (78%) rename cuda_core/cuda/core/graph/{_graph_def.pyx => _graph_definition.pyx} (89%) rename cuda_core/tests/graph/{test_graphdef.py => test_graph_definition.py} (95%) rename cuda_core/tests/graph/{test_graphdef_errors.py => test_graph_definition_errors.py} (89%) rename cuda_core/tests/graph/{test_graphdef_integration.py => test_graph_definition_integration.py} (98%) rename cuda_core/tests/graph/{test_graphdef_lifetime.py => test_graph_definition_lifetime.py} (93%) rename cuda_core/tests/graph/{test_graphdef_mutation.py => test_graph_definition_mutation.py} (96%) diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index dfd52accea3..52ebb9e3ddb 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -63,11 +63,11 @@ def _import_versioned_module(): ) from cuda.core._tensor_map import TensorMapDescriptor, TensorMapDescriptorOptions from cuda.core.graph import ( - Condition, Graph, GraphAllocOptions, GraphBuilder, GraphCompleteOptions, + GraphCondition, GraphDebugPrintOptions, - GraphDef, + GraphDefinition, ) diff --git a/cuda_core/cuda/core/graph/__init__.pxd b/cuda_core/cuda/core/graph/__init__.pxd index 0358b847388..f367745acc1 100644 --- a/cuda_core/cuda/core/graph/__init__.pxd +++ b/cuda_core/cuda/core/graph/__init__.pxd @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -from cuda.core.graph._graph_def cimport Condition, GraphDef +from cuda.core.graph._graph_definition cimport GraphCondition, GraphDefinition from cuda.core.graph._graph_node cimport GraphNode from cuda.core.graph._subclasses cimport ( AllocNode, diff --git a/cuda_core/cuda/core/graph/__init__.py b/cuda_core/cuda/core/graph/__init__.py index 7c608584ae9..57a6988d861 100644 --- a/cuda_core/cuda/core/graph/__init__.py +++ b/cuda_core/cuda/core/graph/__init__.py @@ -8,10 +8,10 @@ GraphCompleteOptions, GraphDebugPrintOptions, ) -from cuda.core.graph._graph_def import ( - Condition, +from cuda.core.graph._graph_definition import ( GraphAllocOptions, - GraphDef, + GraphCondition, + GraphDefinition, ) from cuda.core.graph._graph_node import GraphNode from cuda.core.graph._subclasses import ( diff --git a/cuda_core/cuda/core/graph/_graph_builder.pyx b/cuda_core/cuda/core/graph/_graph_builder.pyx index 9639c323c73..42370029706 100644 --- a/cuda_core/cuda/core/graph/_graph_builder.pyx +++ b/cuda_core/cuda/core/graph/_graph_builder.pyx @@ -786,19 +786,19 @@ class Graph: """ return self._mnff.graph - def update(self, source: "GraphBuilder | GraphDef") -> None: + def update(self, source: "GraphBuilder | GraphDefinition") -> None: """Update the graph using a new graph definition. The topology of the provided source must be identical to this graph. Parameters ---------- - source : :obj:`~graph.GraphBuilder` or :obj:`~graph.GraphDef` + source : :obj:`~graph.GraphBuilder` or :obj:`~graph.GraphDefinition` The graph definition to update from. A GraphBuilder must have finished building. """ - from cuda.core.graph import GraphDef + from cuda.core.graph import GraphDefinition cdef cydriver.CUgraph cu_graph cdef cydriver.CUgraphExec cu_exec = int(self._mnff.graph) @@ -807,11 +807,11 @@ class Graph: if not source._building_ended: raise ValueError("Graph has not finished building.") cu_graph = int(source._mnff.graph) - elif isinstance(source, GraphDef): + elif isinstance(source, GraphDefinition): cu_graph = int(source.handle) else: raise TypeError( - f"expected GraphBuilder or GraphDef, got {type(source).__name__}") + f"expected GraphBuilder or GraphDefinition, got {type(source).__name__}") cdef cydriver.CUgraphExecUpdateResultInfo result_info cdef cydriver.CUresult err diff --git a/cuda_core/cuda/core/graph/_graph_def.pxd b/cuda_core/cuda/core/graph/_graph_definition.pxd similarity index 78% rename from cuda_core/cuda/core/graph/_graph_def.pxd rename to cuda_core/cuda/core/graph/_graph_definition.pxd index 19c4f080316..b414568e986 100644 --- a/cuda_core/cuda/core/graph/_graph_def.pxd +++ b/cuda_core/cuda/core/graph/_graph_definition.pxd @@ -6,16 +6,16 @@ from cuda.bindings cimport cydriver from cuda.core._resource_handles cimport GraphHandle -cdef class Condition: +cdef class GraphCondition: cdef: cydriver.CUgraphConditionalHandle _c_handle object __weakref__ -cdef class GraphDef: +cdef class GraphDefinition: cdef: GraphHandle _h_graph object __weakref__ @staticmethod - cdef GraphDef _from_handle(GraphHandle h_graph) + cdef GraphDefinition _from_handle(GraphHandle h_graph) diff --git a/cuda_core/cuda/core/graph/_graph_def.pyx b/cuda_core/cuda/core/graph/_graph_definition.pyx similarity index 89% rename from cuda_core/cuda/core/graph/_graph_def.pyx rename to cuda_core/cuda/core/graph/_graph_definition.pyx index db9e384ff9e..195a1e300b0 100644 --- a/cuda_core/cuda/core/graph/_graph_def.pyx +++ b/cuda_core/cuda/core/graph/_graph_definition.pyx @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -"""GraphDef: explicit CUDA graph definition.""" +"""GraphDefinition: explicit CUDA graph definition.""" from __future__ import annotations @@ -28,22 +28,22 @@ from dataclasses import dataclass from cuda.core._utils.cuda_utils import driver -cdef class Condition: +cdef class GraphCondition: """A condition variable for conditional graph nodes. - Created by :meth:`GraphDef.create_condition` and passed to + Created by :meth:`GraphDefinition.create_condition` and passed to conditional-node builder methods (``if_cond``, ``if_else``, ``while_loop``, ``switch``). The underlying value is set at runtime by device code via ``cudaGraphSetConditional``. """ def __repr__(self) -> str: - return f"self._c_handle:x}>" + return f"self._c_handle:x}>" def __eq__(self, other) -> bool: - if not isinstance(other, Condition): + if not isinstance(other, GraphCondition): return NotImplemented - return self._c_handle == (other)._c_handle + return self._c_handle == (other)._c_handle def __hash__(self) -> int: return hash(self._c_handle) @@ -90,10 +90,10 @@ class GraphAllocOptions: peer_access: list | None = None -cdef class GraphDef: +cdef class GraphDefinition: """A graph definition. - A GraphDef is used to construct a graph explicitly by adding nodes + A GraphDefinition is used to construct a graph explicitly by adding nodes and specifying dependencies. Once construction is complete, call instantiate() to obtain an executable Graph. """ @@ -106,19 +106,19 @@ cdef class GraphDef: self._h_graph = create_graph_handle(graph) @staticmethod - cdef GraphDef _from_handle(GraphHandle h_graph): - """Create a GraphDef from an existing GraphHandle (internal use).""" - cdef GraphDef g = GraphDef.__new__(GraphDef) + cdef GraphDefinition _from_handle(GraphHandle h_graph): + """Create a GraphDefinition from an existing GraphHandle (internal use).""" + cdef GraphDefinition g = GraphDefinition.__new__(GraphDefinition) g._h_graph = h_graph return g def __repr__(self) -> str: - return f"" + return f"" def __eq__(self, other) -> bool: - if not isinstance(other, GraphDef): + if not isinstance(other, GraphDefinition): return NotImplemented - return as_intptr(self._h_graph) == as_intptr((other)._h_graph) + return as_intptr(self._h_graph) == as_intptr((other)._h_graph) def __hash__(self) -> int: return hash(as_intptr(self._h_graph)) @@ -190,7 +190,7 @@ cdef class GraphDef: """ return self._entry.memcpy(dst, src, size) - def embed(self, child: GraphDef) -> "ChildGraphNode": + def embed(self, child: GraphDefinition) -> "ChildGraphNode": """Add an entry-point child graph node (no dependencies). See :meth:`GraphNode.embed` for full documentation. @@ -218,10 +218,10 @@ cdef class GraphDef: """ return self._entry.callback(fn, user_data=user_data) - def create_condition(self, default_value: int | None = None) -> Condition: + def create_condition(self, default_value: int | None = None) -> GraphCondition: """Create a condition variable for use with conditional nodes. - The returned :class:`Condition` object is passed to conditional-node + The returned :class:`GraphCondition` object is passed to conditional-node builder methods. Its value is controlled at runtime by device code via ``cudaGraphSetConditional``. @@ -233,7 +233,7 @@ cdef class GraphDef: Returns ------- - Condition + GraphCondition A condition variable for controlling conditional execution. """ cdef cydriver.CUgraphConditionalHandle c_handle @@ -250,32 +250,32 @@ cdef class GraphDef: HANDLE_RETURN(cydriver.cuGraphConditionalHandleCreate( &c_handle, as_cu(self._h_graph), ctx, default_val, flags)) - cdef Condition cond = Condition.__new__(Condition) + cdef GraphCondition cond = GraphCondition.__new__(GraphCondition) cond._c_handle = c_handle return cond - def if_cond(self, condition: Condition) -> "IfNode": + def if_cond(self, condition: GraphCondition) -> "IfNode": """Add an entry-point if-conditional node (no dependencies). See :meth:`GraphNode.if_cond` for full documentation. """ return self._entry.if_cond(condition) - def if_else(self, condition: Condition) -> "IfElseNode": + def if_else(self, condition: GraphCondition) -> "IfElseNode": """Add an entry-point if-else conditional node (no dependencies). See :meth:`GraphNode.if_else` for full documentation. """ return self._entry.if_else(condition) - def while_loop(self, condition: Condition) -> "WhileNode": + def while_loop(self, condition: GraphCondition) -> "WhileNode": """Add an entry-point while-loop conditional node (no dependencies). See :meth:`GraphNode.while_loop` for full documentation. """ return self._entry.while_loop(condition) - def switch(self, condition: Condition, unsigned int count) -> "SwitchNode": + def switch(self, condition: GraphCondition, unsigned int count) -> "SwitchNode": """Add an entry-point switch conditional node (no dependencies). See :meth:`GraphNode.switch` for full documentation. diff --git a/cuda_core/cuda/core/graph/_graph_node.pyx b/cuda_core/cuda/core/graph/_graph_node.pyx index ea52be75ed0..553304885be 100644 --- a/cuda_core/cuda/core/graph/_graph_node.pyx +++ b/cuda_core/cuda/core/graph/_graph_node.pyx @@ -18,7 +18,7 @@ from cuda.core._event cimport Event from cuda.core._kernel_arg_handler cimport ParamHolder from cuda.core._launch_config cimport LaunchConfig from cuda.core._module cimport Kernel -from cuda.core.graph._graph_def cimport Condition, GraphDef +from cuda.core.graph._graph_definition cimport GraphCondition, GraphDefinition from cuda.core.graph._subclasses cimport ( AllocNode, ChildGraphNode, @@ -73,7 +73,7 @@ cdef inline GraphNode _registered(GraphNode n): cdef class GraphNode: """A node in a graph definition. - Nodes are created by calling builder methods on GraphDef (for + Nodes are created by calling builder methods on GraphDefinition (for entry-point nodes with no dependencies) or on other Nodes (for nodes that depend on a predecessor). """ @@ -120,9 +120,9 @@ cdef class GraphNode: return driver.CUgraphNodeType(node_type) @property - def graph(self) -> "GraphDef": - """Return the GraphDef this node belongs to.""" - return GraphDef._from_handle(graph_node_get_graph(self._h_node)) + def graph(self) -> "GraphDefinition": + """Return the GraphDefinition this node belongs to.""" + return GraphDefinition._from_handle(graph_node_get_graph(self._h_node)) @property def handle(self) -> driver.CUgraphNode: @@ -296,7 +296,7 @@ cdef class GraphNode: """ return GN_memcpy(self, dst, src, size) - def embed(self, child: GraphDef) -> ChildGraphNode: + def embed(self, child: GraphDefinition) -> ChildGraphNode: """Add a child graph node depending on this node. Embeds a clone of the given graph definition as a sub-graph node. @@ -305,7 +305,7 @@ cdef class GraphNode: Parameters ---------- - child : GraphDef + child : GraphDefinition The graph definition to embed (will be cloned). Returns @@ -313,7 +313,7 @@ cdef class GraphNode: ChildGraphNode A new ChildGraphNode representing the embedded sub-graph. """ - return GN_embed(self, child) + return GN_embed(self, child) def record_event(self, event: Event) -> EventRecordNode: """Add an event record node depending on this node. @@ -380,7 +380,7 @@ cdef class GraphNode: """ return GN_callback(self, fn, user_data) - def if_cond(self, condition: Condition) -> IfNode: + def if_cond(self, condition: GraphCondition) -> IfNode: """Add an if-conditional node depending on this node. The body graph executes only when the condition evaluates to @@ -388,8 +388,8 @@ cdef class GraphNode: Parameters ---------- - condition : Condition - Condition from :meth:`GraphDef.create_condition`. + condition : GraphCondition + GraphCondition from :meth:`GraphDefinition.create_condition`. Returns ------- @@ -400,7 +400,7 @@ cdef class GraphNode: self, condition, cydriver.CU_GRAPH_COND_TYPE_IF, 1, IfNode) - def if_else(self, condition: Condition) -> IfElseNode: + def if_else(self, condition: GraphCondition) -> IfElseNode: """Add an if-else conditional node depending on this node. Two body graphs: the first executes when the condition is @@ -408,8 +408,8 @@ cdef class GraphNode: Parameters ---------- - condition : Condition - Condition from :meth:`GraphDef.create_condition`. + condition : GraphCondition + GraphCondition from :meth:`GraphDefinition.create_condition`. Returns ------- @@ -421,7 +421,7 @@ cdef class GraphNode: self, condition, cydriver.CU_GRAPH_COND_TYPE_IF, 2, IfElseNode) - def while_loop(self, condition: Condition) -> WhileNode: + def while_loop(self, condition: GraphCondition) -> WhileNode: """Add a while-loop conditional node depending on this node. The body graph executes repeatedly while the condition @@ -429,8 +429,8 @@ cdef class GraphNode: Parameters ---------- - condition : Condition - Condition from :meth:`GraphDef.create_condition`. + condition : GraphCondition + GraphCondition from :meth:`GraphDefinition.create_condition`. Returns ------- @@ -441,7 +441,7 @@ cdef class GraphNode: self, condition, cydriver.CU_GRAPH_COND_TYPE_WHILE, 1, WhileNode) - def switch(self, condition: Condition, unsigned int count) -> SwitchNode: + def switch(self, condition: GraphCondition, unsigned int count) -> SwitchNode: """Add a switch conditional node depending on this node. The condition value selects which branch to execute. If the @@ -449,8 +449,8 @@ cdef class GraphNode: Parameters ---------- - condition : Condition - Condition from :meth:`GraphDef.create_condition`. + condition : GraphCondition + GraphCondition from :meth:`GraphDefinition.create_condition`. count : int Number of switch cases (branches). @@ -476,14 +476,14 @@ cdef void _destroy_kernel_handle_copy(void* ptr) noexcept nogil: cdef inline ConditionalNode _make_conditional_node( GraphNode pred, - Condition condition, + GraphCondition condition, cydriver.CUgraphConditionalNodeType cond_type, unsigned int size, type node_cls): - if not isinstance(condition, Condition): + if not isinstance(condition, GraphCondition): raise TypeError( - f"condition must be a Condition object (from " - f"GraphDef.create_condition()), got {type(condition).__name__}") + f"condition must be a GraphCondition object (from " + f"GraphDefinition.create_condition()), got {type(condition).__name__}") cdef cydriver.CUgraphNodeParams params cdef cydriver.CUgraphNode new_node = NULL @@ -522,7 +522,7 @@ cdef inline ConditionalNode _make_conditional_node( for i in range(size): bg = params.conditional.phGraph_out[i] h_branch = create_graph_handle_ref(bg, h_graph) - branch_list.append(GraphDef._from_handle(h_branch)) + branch_list.append(GraphDefinition._from_handle(h_branch)) cdef tuple branches = tuple(branch_list) cdef ConditionalNode n = node_cls.__new__(node_cls) @@ -841,7 +841,7 @@ cdef inline MemcpyNode GN_memcpy( c_dst_type, c_src_type)) -cdef inline ChildGraphNode GN_embed(GraphNode self, GraphDef child_def): +cdef inline ChildGraphNode GN_embed(GraphNode self, GraphDefinition child_def): cdef cydriver.CUgraphNode new_node = NULL cdef GraphHandle h_graph = graph_node_get_graph(self._h_node) cdef cydriver.CUgraphNode pred_node = as_cu(self._h_node) diff --git a/cuda_core/cuda/core/graph/_subclasses.pxd b/cuda_core/cuda/core/graph/_subclasses.pxd index d21dcd74654..7f84b713429 100644 --- a/cuda_core/cuda/core/graph/_subclasses.pxd +++ b/cuda_core/cuda/core/graph/_subclasses.pxd @@ -5,7 +5,7 @@ from libc.stddef cimport size_t from cuda.bindings cimport cydriver -from cuda.core.graph._graph_def cimport Condition +from cuda.core.graph._graph_definition cimport GraphCondition from cuda.core.graph._graph_node cimport GraphNode from cuda.core._resource_handles cimport EventHandle, GraphHandle, GraphNodeHandle, KernelHandle @@ -150,7 +150,7 @@ cdef class HostCallbackNode(GraphNode): cdef class ConditionalNode(GraphNode): cdef: - Condition _condition + GraphCondition _condition cydriver.CUgraphConditionalNodeType _cond_type tuple _branches diff --git a/cuda_core/cuda/core/graph/_subclasses.pyx b/cuda_core/cuda/core/graph/_subclasses.pyx index 277fea4f78b..ef08bb30856 100644 --- a/cuda_core/cuda/core/graph/_subclasses.pyx +++ b/cuda_core/cuda/core/graph/_subclasses.pyx @@ -14,7 +14,7 @@ from cuda.bindings cimport cydriver from cuda.core._event cimport Event from cuda.core._launch_config cimport LaunchConfig from cuda.core._module cimport Kernel -from cuda.core.graph._graph_def cimport Condition, GraphDef +from cuda.core.graph._graph_definition cimport GraphCondition, GraphDefinition from cuda.core.graph._graph_node cimport GraphNode from cuda.core._resource_handles cimport ( EventHandle, @@ -237,7 +237,7 @@ cdef class AllocNode(GraphNode): @property def options(self): """A GraphAllocOptions reconstructed from this node's parameters.""" - from cuda.core.graph._graph_def import GraphAllocOptions + from cuda.core.graph._graph_definition import GraphAllocOptions return GraphAllocOptions( device=self._device_id, memory_type=self._memory_type, @@ -440,7 +440,7 @@ cdef class ChildGraphNode(GraphNode): Properties ---------- - child_graph : GraphDef + child_graph : GraphDefinition The embedded graph definition (non-owning wrapper). """ @@ -469,9 +469,9 @@ cdef class ChildGraphNode(GraphNode): f" child=0x{as_intptr(self._h_child_graph):x}>") @property - def child_graph(self) -> "GraphDef": + def child_graph(self) -> "GraphDefinition": """The embedded graph definition (non-owning wrapper).""" - return GraphDef._from_handle(self._h_child_graph) + return GraphDefinition._from_handle(self._h_child_graph) cdef class EventRecordNode(GraphNode): @@ -611,11 +611,11 @@ cdef class ConditionalNode(GraphNode): Properties ---------- - condition : Condition or None + condition : GraphCondition or None The condition variable controlling execution (None pre-13.2). cond_type : str or None The conditional type ("if", "while", or "switch"; None pre-13.2). - branches : tuple of GraphDef + branches : tuple of GraphDefinition The body graphs for each branch (empty pre-13.2). """ @@ -637,7 +637,7 @@ cdef class ConditionalNode(GraphNode): cdef int cond_type_int = int(cond_params.type) cdef unsigned int size = int(cond_params.size) - cdef Condition condition = Condition.__new__(Condition) + cdef GraphCondition condition = GraphCondition.__new__(GraphCondition) condition._c_handle = ( int(cond_params.handle)) @@ -650,7 +650,7 @@ cdef class ConditionalNode(GraphNode): h_branch = create_graph_handle_ref( int(cond_params.phGraph_out[i]), h_graph) - branch_list.append(GraphDef._from_handle(h_branch)) + branch_list.append(GraphDefinition._from_handle(h_branch)) cdef tuple branches = tuple(branch_list) cdef type cls @@ -675,7 +675,7 @@ cdef class ConditionalNode(GraphNode): return f"" @property - def condition(self) -> Condition | None: + def condition(self) -> GraphCondition | None: """The condition variable controlling execution.""" return self._condition @@ -697,7 +697,7 @@ cdef class ConditionalNode(GraphNode): @property def branches(self) -> tuple: - """The body graphs for each branch as a tuple of GraphDef. + """The body graphs for each branch as a tuple of GraphDefinition. Returns an empty tuple when reconstructed from the driver pre-CUDA 13.2. @@ -713,7 +713,7 @@ cdef class IfNode(ConditionalNode): f" condition=0x{self._condition._c_handle:x}>") @property - def then(self) -> "GraphDef": + def then(self) -> "GraphDefinition": """The 'then' branch graph.""" return self._branches[0] @@ -726,12 +726,12 @@ cdef class IfElseNode(ConditionalNode): f" condition=0x{self._condition._c_handle:x}>") @property - def then(self) -> "GraphDef": + def then(self) -> "GraphDefinition": """The ``then`` branch graph (executed when condition is non-zero).""" return self._branches[0] @property - def else_(self) -> "GraphDef": + def else_(self) -> "GraphDefinition": """The ``else`` branch graph (executed when condition is zero).""" return self._branches[1] @@ -744,7 +744,7 @@ cdef class WhileNode(ConditionalNode): f" condition=0x{self._condition._c_handle:x}>") @property - def body(self) -> "GraphDef": + def body(self) -> "GraphDefinition": """The loop body graph.""" return self._branches[0] diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 88780732d54..054831a03c1 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -78,7 +78,7 @@ A CUDA graph captures a set of GPU operations and their dependencies, allowing them to be defined once and launched repeatedly with minimal CPU overhead. Graphs can be constructed in two ways: :class:`~graph.GraphBuilder` captures operations from a stream, while -:class:`~graph.GraphDef` builds a graph explicitly by adding nodes and +:class:`~graph.GraphDefinition` builds a graph explicitly by adding nodes and edges. Both produce an executable :class:`~graph.Graph` that can be launched on a :class:`Stream`. @@ -87,12 +87,12 @@ launched on a :class:`Stream`. graph.Graph graph.GraphBuilder - graph.GraphDef + graph.GraphDefinition :template: autosummary/cyclass.rst graph.GraphNode - graph.Condition + graph.GraphCondition :template: dataclass.rst diff --git a/cuda_core/docs/source/release/0.7.0-notes.rst b/cuda_core/docs/source/release/0.7.0-notes.rst index 3946c8804bf..2839474adb2 100644 --- a/cuda_core/docs/source/release/0.7.0-notes.rst +++ b/cuda_core/docs/source/release/0.7.0-notes.rst @@ -25,12 +25,12 @@ New features ------------ - Added the :mod:`cuda.core.graph` public module containing - :class:`~graph.GraphDef` for explicit graph construction, typed node + ``GraphDef`` for explicit graph construction, typed node subclasses, and supporting types. :class:`~graph.GraphBuilder` (stream capture) also moves into this module. - Added :meth:`~graph.GraphBuilder.callback` for CPU callbacks during stream - capture, mirroring the existing :meth:`~graph.GraphDef.callback` API. + capture, mirroring the existing ``GraphDef.callback`` API. - Added :class:`GraphicsResource` for CUDA-OpenGL interoperability. Factory classmethods :meth:`~GraphicsResource.from_gl_buffer` and diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst index 34eff571005..7a93eff4696 100644 --- a/cuda_core/docs/source/release/1.0.0-notes.rst +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -19,6 +19,19 @@ New features - TBD +Breaking changes +---------------- + +- Renamed :class:`~graph.GraphDef` to :class:`~graph.GraphDefinition` for + consistency with the rest of the API, which spells words out (e.g. + ``TensorMapDescriptor``, not ``TensorMapDesc``). + (`#1950 `__) +- Renamed ``cuda.core.graph.Condition`` to :class:`~graph.GraphCondition` + to follow the ``Graph*`` prefix convention used by ``GraphBuilder``, + ``GraphDefinition``, ``GraphNode``. + (`#1945 `__) + + Fixes and enhancements ----------------------- diff --git a/cuda_core/tests/graph/test_graphdef.py b/cuda_core/tests/graph/test_graph_definition.py similarity index 95% rename from cuda_core/tests/graph/test_graphdef.py rename to cuda_core/tests/graph/test_graph_definition.py index e81c0b03b97..4e0f28e22dc 100644 --- a/cuda_core/tests/graph/test_graphdef.py +++ b/cuda_core/tests/graph/test_graph_definition.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Tests for GraphDef topology, node types, instantiation, and execution.""" +"""Tests for GraphDefinition topology, node types, instantiation, and execution.""" from collections.abc import Callable from dataclasses import dataclass, field @@ -22,7 +22,7 @@ GraphAllocOptions, GraphCompleteOptions, GraphDebugPrintOptions, - GraphDef, + GraphDefinition, GraphNode, HostCallbackNode, IfElseNode, @@ -76,7 +76,7 @@ class GraphSpec: """Describes a graph topology with expected structural properties.""" name: str - graphdef: GraphDef + graph_definition: GraphDefinition named_nodes: dict = field(default_factory=dict) expected_edges: set = field(default_factory=set) expected_pred: dict = field(default_factory=dict) @@ -85,12 +85,12 @@ class GraphSpec: def _build_empty(): """No nodes, no edges.""" - return GraphSpec("empty", GraphDef()) + return GraphSpec("empty", GraphDefinition()) def _build_single(): """One alloc node, no edges.""" - g = GraphDef() + g = GraphDefinition() a = g.alloc(ALLOC_SIZE) return GraphSpec( "single", @@ -104,7 +104,7 @@ def _build_single(): def _build_chain(): """Linear chain: a -> b -> c.""" - g = GraphDef() + g = GraphDefinition() a = g.alloc(ALLOC_SIZE) b = a.alloc(ALLOC_SIZE) c = b.alloc(ALLOC_SIZE) @@ -120,7 +120,7 @@ def _build_chain(): def _build_fan_out(): """One node feeds three: a -> {b, c, d}.""" - g = GraphDef() + g = GraphDefinition() a = g.alloc(ALLOC_SIZE) b = a.alloc(ALLOC_SIZE) c = a.alloc(ALLOC_SIZE) @@ -137,7 +137,7 @@ def _build_fan_out(): def _build_fan_in(): """Three entry nodes merge: {a, b, c} -> d (join).""" - g = GraphDef() + g = GraphDefinition() a = g.alloc(ALLOC_SIZE) b = g.alloc(ALLOC_SIZE) c = g.alloc(ALLOC_SIZE) @@ -154,7 +154,7 @@ def _build_fan_in(): def _build_diamond(): """Diamond: a -> {b, c} -> d (join).""" - g = GraphDef() + g = GraphDefinition() a = g.alloc(ALLOC_SIZE) b = a.alloc(ALLOC_SIZE) c = a.alloc(ALLOC_SIZE) @@ -171,7 +171,7 @@ def _build_diamond(): def _build_disconnected(): """Two independent entry nodes: a, b.""" - g = GraphDef() + g = GraphDefinition() a = g.alloc(ALLOC_SIZE) b = g.alloc(ALLOC_SIZE) return GraphSpec( @@ -227,7 +227,7 @@ class NodeSpec: name: str expected_class: type expected_type_name: str - builder: Callable[[GraphDef], tuple[GraphNode, dict]] + builder: Callable[[GraphDefinition], tuple[GraphNode, dict]] reconstructed_class: type | None = None needs_mempool: bool = True @@ -404,7 +404,7 @@ def noop(data): def _build_child_graph_node(g): - child = GraphDef() + child = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) @@ -412,7 +412,7 @@ def _build_child_graph_node(g): child.launch(config, kernel) node = g.embed(child) return node, { - "child_graph": lambda v: isinstance(v, GraphDef) and len(v.nodes()) == 2, + "child_graph": lambda v: isinstance(v, GraphDefinition) and len(v.nodes()) == 2, } @@ -423,7 +423,7 @@ def _build_if_cond_node(g): "condition": condition, "cond_type": "if", "branches": lambda v: isinstance(v, tuple) and len(v) == 1, - "then": lambda v: isinstance(v, GraphDef), + "then": lambda v: isinstance(v, GraphDefinition), } @@ -434,8 +434,8 @@ def _build_if_else_node(g): "condition": condition, "cond_type": "if", "branches": lambda v: isinstance(v, tuple) and len(v) == 2, - "then": lambda v: isinstance(v, GraphDef), - "else_": lambda v: isinstance(v, GraphDef), + "then": lambda v: isinstance(v, GraphDefinition), + "else_": lambda v: isinstance(v, GraphDefinition), } @@ -446,7 +446,7 @@ def _build_while_loop_node(g): "condition": condition, "cond_type": "while", "branches": lambda v: isinstance(v, tuple) and len(v) == 1, - "body": lambda v: isinstance(v, GraphDef), + "body": lambda v: isinstance(v, GraphDefinition), } @@ -564,7 +564,7 @@ def node_spec(request, init_cuda): spec = request.param if spec.needs_mempool: _skip_if_no_mempool() - g = GraphDef() + g = GraphDefinition() node, expected_attrs = spec.builder(g) return spec, g, node, expected_attrs @@ -576,8 +576,8 @@ def node_spec(request, init_cuda): @pytest.fixture def sample_graphdef(init_cuda): - """A sample GraphDef for standalone tests.""" - return GraphDef() + """A sample GraphDefinition for standalone tests.""" + return GraphDefinition() @pytest.fixture @@ -595,20 +595,20 @@ def dot_file(tmp_path): def test_node_count(graph_spec): """Graph contains the expected number of nodes.""" - assert len(graph_spec.graphdef.nodes()) == len(graph_spec.named_nodes) + assert len(graph_spec.graph_definition.nodes()) == len(graph_spec.named_nodes) def test_nodes_match(nonempty_graph_spec): """nodes() returns exactly the expected nodes.""" spec = nonempty_graph_spec - assert set(spec.graphdef.nodes()) == set(spec.named_nodes.values()) + assert set(spec.graph_definition.nodes()) == set(spec.named_nodes.values()) def test_edges(graph_spec): """edges() returns exactly the expected edges.""" spec = graph_spec node_to_name = {v: k for k, v in spec.named_nodes.items()} - actual = {(node_to_name[a], node_to_name[b]) for a, b in spec.graphdef.edges()} + actual = {(node_to_name[a], node_to_name[b]) for a, b in spec.graph_definition.edges()} assert actual == spec.expected_edges @@ -631,10 +631,10 @@ def test_succ(nonempty_graph_spec): def test_node_graph_property(nonempty_graph_spec): - """Every node's .graph property returns the parent GraphDef.""" + """Every node's .graph property returns the parent GraphDefinition.""" spec = nonempty_graph_spec for name, node in spec.named_nodes.items(): - assert node.graph == spec.graphdef, f"graph mismatch for node {name}" + assert node.graph == spec.graph_definition, f"graph mismatch for node {name}" # ============================================================================= @@ -656,7 +656,7 @@ def test_node_type_property(node_spec): def test_node_type_preserved_by_nodes(node_spec): - """Node type is preserved when retrieved via graphdef.nodes().""" + """Node type is preserved when retrieved via graph_definition.nodes().""" spec, g, node, _ = node_spec all_nodes = g.nodes() matched = [n for n in all_nodes if n == node] @@ -689,7 +689,7 @@ def test_node_attrs(node_spec): def test_node_attrs_preserved_by_nodes(node_spec): - """Type-specific attributes survive round-trip through graphdef.nodes().""" + """Type-specific attributes survive round-trip through graph_definition.nodes().""" spec, g, node, expected_attrs = node_spec if not expected_attrs: pytest.skip("no type-specific attributes") @@ -703,7 +703,7 @@ def test_node_attrs_preserved_by_nodes(node_spec): def test_identity_preservation(init_cuda): """Round-trips through nodes(), edges(), and pred/succ return extant objects rather than duplicates.""" - g = GraphDef() + g = GraphDefinition() a = g.empty() b = g.empty() @@ -737,7 +737,7 @@ def registered(node): gc.collect() assert len(_node_registry) == 0 - g = GraphDef() + g = GraphDefinition() a = g.empty() b = g.empty() c = g.empty() @@ -770,12 +770,12 @@ def registered(node): # ============================================================================= -# GraphDef basics +# GraphDefinition basics # ============================================================================= def test_graphdef_handle_valid(sample_graphdef): - """GraphDef has a valid non-null handle.""" + """GraphDefinition has a valid non-null handle.""" assert sample_graphdef.handle is not None assert int(sample_graphdef.handle) != 0 @@ -854,7 +854,7 @@ def test_alloc_device_option(sample_graphdef, device_spec): def test_alloc_peer_access(mempool_device_x2): """AllocNode.peer_access reflects requested peers.""" d0, d1 = mempool_device_x2 - g = GraphDef() + g = GraphDefinition() options = GraphAllocOptions(device=d0.device_id, peer_access=[d1.device_id]) node = g.alloc(ALLOC_SIZE, options) assert d1.device_id in node.peer_access @@ -928,10 +928,10 @@ def test_launch_chain_dependencies(sample_graphdef): ] -def _instantiate(graphdef, kwargs, stream=None): - """Call graphdef.instantiate() with the given kwargs, resolving sentinels.""" +def _instantiate(graph_definition, kwargs, stream=None): + """Call graph_definition.instantiate() with the given kwargs, resolving sentinels.""" if "no_arg" in kwargs: - return graphdef.instantiate() + return graph_definition.instantiate() opts = kwargs.get("options") if opts is not None and opts.upload_stream == _SENTINEL_UPLOAD_STREAM: opts = GraphCompleteOptions( @@ -940,12 +940,12 @@ def _instantiate(graphdef, kwargs, stream=None): device_launch=opts.device_launch, use_node_priority=opts.use_node_priority, ) - return graphdef.instantiate(options=opts) + return graph_definition.instantiate(options=opts) -def _instantiate_and_upload(graphdef, kwargs, stream): +def _instantiate_and_upload(graph_definition, kwargs, stream): """Instantiate and upload, handling upload_stream option.""" - graph = _instantiate(graphdef, kwargs, stream) + graph = _instantiate(graph_definition, kwargs, stream) if not (kwargs.get("options") and kwargs["options"].upload_stream): graph.upload(stream) return graph @@ -1053,7 +1053,7 @@ def test_instantiate_and_execute_memcpy(sample_graphdef, inst_kwargs): def test_instantiate_and_execute_child_graph(sample_graphdef): """Graph with embedded child graph can be executed.""" - child = GraphDef() + child = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) diff --git a/cuda_core/tests/graph/test_graphdef_errors.py b/cuda_core/tests/graph/test_graph_definition_errors.py similarity index 89% rename from cuda_core/tests/graph/test_graphdef_errors.py rename to cuda_core/tests/graph/test_graph_definition_errors.py index 54b28d7a5c8..2f6935d6eb0 100644 --- a/cuda_core/tests/graph/test_graphdef_errors.py +++ b/cuda_core/tests/graph/test_graph_definition_errors.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Tests for GraphDef input validation, error handling, and edge cases.""" +"""Tests for GraphDefinition input validation, error handling, and edge cases.""" import ctypes @@ -12,9 +12,9 @@ from cuda.core import Device, LaunchConfig from cuda.core._utils.cuda_utils import CUDAError from cuda.core.graph import ( - Condition, EmptyNode, - GraphDef, + GraphCondition, + GraphDefinition, ) SIZEOF_INT = ctypes.sizeof(ctypes.c_int) @@ -40,15 +40,15 @@ def _skip_if_no_mempool(): ], ) def test_conditional_rejects_non_condition(init_cuda, method, args): - """Conditional node methods reject non-Condition arguments.""" - g = GraphDef() - with pytest.raises(TypeError, match="Condition"): + """Conditional node methods reject non-GraphCondition arguments.""" + g = GraphDefinition() + with pytest.raises(TypeError, match="GraphCondition"): getattr(g, method)(*args) def test_embed_rejects_non_graphdef(init_cuda): - """embed() rejects non-GraphDef arguments.""" - g = GraphDef() + """embed() rejects non-GraphDefinition arguments.""" + g = GraphDefinition() with pytest.raises((TypeError, AttributeError)): g.embed("not a graph") @@ -60,7 +60,7 @@ def test_embed_rejects_non_graphdef(init_cuda): def test_free_null_pointer(init_cuda): """free(0) raises a CUDA error.""" - g = GraphDef() + g = GraphDefinition() with pytest.raises(CUDAError): g.free(0) @@ -68,7 +68,7 @@ def test_free_null_pointer(init_cuda): def test_memset_invalid_value_size(init_cuda): """memset with 3-byte value (not 1, 2, or 4) raises ValueError.""" _skip_if_no_mempool() - g = GraphDef() + g = GraphDefinition() alloc = g.alloc(1024) with pytest.raises(ValueError): alloc.memset(alloc.dptr, b"\x01\x02\x03", 100) @@ -76,7 +76,7 @@ def test_memset_invalid_value_size(init_cuda): def test_switch_zero_branches(init_cuda): """switch with count=0 raises an error.""" - g = GraphDef() + g = GraphDefinition() condition = try_create_condition(g) with pytest.raises(CUDAError): g.switch(condition, 0) @@ -89,8 +89,8 @@ def test_switch_zero_branches(init_cuda): def test_condition_from_different_graph(init_cuda): """Using a condition created for graph A in graph B raises an error.""" - g1 = GraphDef() - g2 = GraphDef() + g1 = GraphDefinition() + g2 = GraphDefinition() condition = try_create_condition(g1) with pytest.raises(CUDAError): g2.if_cond(condition) @@ -103,7 +103,7 @@ def test_condition_from_different_graph(init_cuda): def test_empty_node(init_cuda): """empty() creates a single entry-point empty node.""" - g = GraphDef() + g = GraphDefinition() joined = g.empty() assert isinstance(joined, EmptyNode) assert len(g.nodes()) == 1 @@ -112,7 +112,7 @@ def test_empty_node(init_cuda): def test_join_single_predecessor(init_cuda): """node.join() with no extra args creates a single-dep empty node.""" _skip_if_no_mempool() - g = GraphDef() + g = GraphDefinition() a = g.alloc(1024) joined = a.join() assert isinstance(joined, EmptyNode) @@ -120,12 +120,12 @@ def test_join_single_predecessor(init_cuda): def test_multiple_instantiation(init_cuda): - """Same GraphDef can be instantiated multiple times independently.""" + """Same GraphDefinition can be instantiated multiple times independently.""" mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") cfg = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() g.launch(cfg, kernel) g1 = g.instantiate() g2 = g.instantiate() @@ -135,7 +135,7 @@ def test_multiple_instantiation(init_cuda): def test_unmatched_alloc_succeeds(init_cuda): """Alloc without corresponding free is valid (graph-scoped lifetime).""" _skip_if_no_mempool() - g = GraphDef() + g = GraphDefinition() g.alloc(1024) graph = g.instantiate() stream = Device().create_stream() @@ -145,12 +145,12 @@ def test_unmatched_alloc_succeeds(init_cuda): def test_create_condition_no_default_value(init_cuda): """create_condition with no default_value succeeds.""" - g = GraphDef() + g = GraphDefinition() try: condition = g.create_condition() except CUDAError: pytest.skip("Conditional nodes not supported (requires CC >= 9.0)") - assert isinstance(condition, Condition) + assert isinstance(condition, GraphCondition) # ============================================================================= @@ -172,7 +172,7 @@ def test_while_loop_zero_iterations(init_cuda): add_one = mod.get_kernel("add_one") cfg = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() condition = g.create_condition(default_value=0) alloc = g.alloc(SIZEOF_INT) ms = alloc.memset(alloc.dptr, 0, SIZEOF_INT) @@ -200,7 +200,7 @@ def test_if_cond_false_skips_body(init_cuda): add_one = mod.get_kernel("add_one") cfg = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() condition = g.create_condition(default_value=0) alloc = g.alloc(SIZEOF_INT) ms = alloc.memset(alloc.dptr, 0, SIZEOF_INT) @@ -228,7 +228,7 @@ def test_switch_oob_skips_all_branches(init_cuda): add_one = mod.get_kernel("add_one") cfg = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() condition = g.create_condition(default_value=99) alloc = g.alloc(SIZEOF_INT) ms = alloc.memset(alloc.dptr, 0, SIZEOF_INT) diff --git a/cuda_core/tests/graph/test_graphdef_integration.py b/cuda_core/tests/graph/test_graph_definition_integration.py similarity index 98% rename from cuda_core/tests/graph/test_graphdef_integration.py rename to cuda_core/tests/graph/test_graph_definition_integration.py index 3fc0263c953..1c5eecf40b6 100644 --- a/cuda_core/tests/graph/test_graphdef_integration.py +++ b/cuda_core/tests/graph/test_graph_definition_integration.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""End-to-end integration tests exercising all GraphDef node types in realistic scenarios.""" +"""End-to-end integration tests exercising all GraphDefinition node types in realistic scenarios.""" import ctypes @@ -10,7 +10,7 @@ from cuda.core import Device, EventOptions, LaunchConfig, Program, ProgramOptions from cuda.core._utils.cuda_utils import driver, handle_return -from cuda.core.graph import GraphDef +from cuda.core.graph import GraphDefinition SIZEOF_FLOAT = 4 SIZEOF_INT = 4 @@ -213,7 +213,7 @@ def _run_heat_graph(dev, k_heat, k_countdown, host_ptr): """Build, instantiate, launch, and verify the heat-diffusion graph.""" # Definitions - g = GraphDef() + g = GraphDefinition() condition = g.create_condition(default_value=1) event_start = dev.create_event(EventOptions(enable_timing=True)) event_end = dev.create_event(EventOptions(enable_timing=True)) @@ -240,7 +240,7 @@ def capture_result(): m_ctr = a_ctr.memset(a_ctr.dptr, np.int32(_HEAT_ITERS), 1) # Phase 3 — Boundary conditions (child graph) - bc = GraphDef() \ + bc = GraphDefinition() \ .memset(a_curr.dptr, np.float32(_HEAT_T_LEFT), 1) \ .memset(a_curr.dptr + (_HEAT_N - 1) * SIZEOF_FLOAT, np.float32(_HEAT_T_RIGHT), 1) \ @@ -323,7 +323,7 @@ def _run_bisection_graph(dev, k_eval, k_hi, k_lo, k_cd, k_check, k_newton, host_ """Build, instantiate, launch, and verify the bisection graph.""" # Definitions - g = GraphDef() + g = GraphDefinition() cfg = LaunchConfig(grid=1, block=1) results = {} @@ -426,7 +426,7 @@ def test_switch_dispatch(init_cuda, mode, expected): def _run_switch_graph(dev, mode, k_negate, k_double, k_square, host_ptr): """Build, instantiate, launch, and verify the switch-dispatch graph.""" - g = GraphDef() + g = GraphDefinition() cfg = LaunchConfig(grid=1, block=1) # fmt: off diff --git a/cuda_core/tests/graph/test_graphdef_lifetime.py b/cuda_core/tests/graph/test_graph_definition_lifetime.py similarity index 93% rename from cuda_core/tests/graph/test_graphdef_lifetime.py rename to cuda_core/tests/graph/test_graph_definition_lifetime.py index a9b30030773..cfa538539f6 100644 --- a/cuda_core/tests/graph/test_graphdef_lifetime.py +++ b/cuda_core/tests/graph/test_graph_definition_lifetime.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Tests for GraphDef resource lifetime management and RAII correctness.""" +"""Tests for GraphDefinition resource lifetime management and RAII correctness.""" import gc @@ -13,7 +13,7 @@ from cuda.core.graph import ( ChildGraphNode, ConditionalNode, - GraphDef, + GraphDefinition, KernelNode, ) @@ -58,8 +58,8 @@ def _make_switch(g, cond): @pytest.mark.parametrize("builder, expected_count", _COND_BUILDERS) def test_branches_survive_parent_deletion(init_cuda, builder, expected_count): - """All branch graphs remain valid after parent GraphDef is deleted.""" - g = GraphDef() + """All branch graphs remain valid after parent GraphDefinition is deleted.""" + g = GraphDefinition() condition = try_create_condition(g) branches = builder(g, condition) assert len(branches) == expected_count @@ -73,12 +73,12 @@ def test_branches_survive_parent_deletion(init_cuda, builder, expected_count): @pytest.mark.parametrize("builder, expected_count", _COND_BUILDERS) def test_branches_usable_after_parent_deletion(init_cuda, builder, expected_count): - """Nodes can be added to branch graphs after parent GraphDef is deleted.""" + """Nodes can be added to branch graphs after parent GraphDefinition is deleted.""" mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() condition = try_create_condition(g) branches = builder(g, condition) @@ -92,7 +92,7 @@ def test_branches_usable_after_parent_deletion(init_cuda, builder, expected_coun def test_reconstructed_body_survives_parent_deletion(init_cuda): """Body graph obtained via nodes() reconstruction survives parent deletion.""" - g = GraphDef() + g = GraphDefinition() condition = try_create_condition(g) g.while_loop(condition) @@ -117,16 +117,16 @@ def test_reconstructed_body_survives_parent_deletion(init_cuda): def test_child_graph_survives_parent_deletion(init_cuda): - """Embedded child graph remains valid after parent GraphDef is deleted.""" + """Embedded child graph remains valid after parent GraphDefinition is deleted.""" mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) - child_def = GraphDef() + child_def = GraphDefinition() child_def.launch(config, kernel) child_def.launch(config, kernel) - g = GraphDef() + g = GraphDefinition() node = g.embed(child_def) child_ref = node.child_graph @@ -142,13 +142,13 @@ def test_nested_child_graph_lifetime(init_cuda): kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) - inner = GraphDef() + inner = GraphDefinition() inner.launch(config, kernel) - middle = GraphDef() + middle = GraphDefinition() middle.embed(inner) - outer = GraphDef() + outer = GraphDefinition() outer_node = outer.embed(middle) middle_ref = outer_node.child_graph @@ -171,7 +171,7 @@ def test_event_record_node_keeps_event_alive(init_cuda): """EventRecordNode should keep the Event alive after original is deleted.""" _skip_if_no_mempool() dev = Device() - g = GraphDef() + g = GraphDefinition() alloc = g.alloc(1024) event = dev.create_event(EventOptions(enable_timing=False)) @@ -188,7 +188,7 @@ def test_event_wait_node_keeps_event_alive(init_cuda): """EventWaitNode should keep the Event alive after original is deleted.""" _skip_if_no_mempool() dev = Device() - g = GraphDef() + g = GraphDefinition() alloc = g.alloc(1024) event = dev.create_event(EventOptions(enable_timing=False)) @@ -204,7 +204,7 @@ def test_event_wait_node_keeps_event_alive(init_cuda): def test_event_record_node_preserves_metadata(init_cuda): """Reconstructed EventRecordNode recovers full Event metadata via reverse lookup.""" dev = Device() - g = GraphDef() + g = GraphDefinition() event = dev.create_event(EventOptions(enable_timing=True, busy_waited_sync=True)) node = g.record_event(event) @@ -219,7 +219,7 @@ def test_event_record_node_preserves_metadata(init_cuda): def test_event_wait_node_preserves_metadata(init_cuda): """Reconstructed EventWaitNode recovers full Event metadata via reverse lookup.""" dev = Device() - g = GraphDef() + g = GraphDefinition() event = dev.create_event(EventOptions(enable_timing=False)) node = g.wait_event(event) @@ -233,7 +233,7 @@ def test_event_wait_node_preserves_metadata(init_cuda): def test_event_metadata_survives_gc(init_cuda): """Event metadata is preserved through reverse lookup even after original is GC'd.""" dev = Device() - g = GraphDef() + g = GraphDefinition() event = dev.create_event(EventOptions(enable_timing=True, busy_waited_sync=True)) node = g.record_event(event) @@ -250,7 +250,7 @@ def test_event_metadata_survives_gc(init_cuda): def test_event_survives_graph_instantiation_and_execution(init_cuda): """Graph with event nodes executes correctly after original Event is deleted.""" dev = Device() - g = GraphDef() + g = GraphDefinition() event = dev.create_event(EventOptions(enable_timing=False)) rec = g.record_event(event) @@ -275,7 +275,7 @@ def test_event_survives_graph_clone_and_execution(init_cuda): from cuda.core._utils.cuda_utils import driver, handle_return dev = Device() - g = GraphDef() + g = GraphDefinition() event = dev.create_event(EventOptions(enable_timing=False)) rec = g.record_event(event) @@ -304,7 +304,7 @@ def test_python_callable_callback_survives_del(init_cuda): def my_callback(): called[0] = True - g = GraphDef() + g = GraphDefinition() g.callback(my_callback) del my_callback @@ -330,7 +330,7 @@ def test_cfunc_callback_survives_del(init_cuda): def raw_fn(data): called[0] = True - g = GraphDef() + g = GraphDefinition() g.callback(raw_fn) del raw_fn @@ -357,7 +357,7 @@ def read_byte(data): result[0] = ctypes.cast(data, ctypes.POINTER(ctypes.c_uint8))[0] payload = bytes([0xCD]) - g = GraphDef() + g = GraphDefinition() g.callback(read_byte, user_data=payload) del payload @@ -383,7 +383,7 @@ def test_kernel_node_keeps_kernel_alive(init_cuda): kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() node = g.launch(config, kernel) del kernel, mod @@ -399,7 +399,7 @@ def test_kernel_survives_graph_instantiation_and_execution(init_cuda): kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() g.launch(config, kernel) del kernel, mod @@ -424,7 +424,7 @@ def test_kernel_survives_graph_clone_and_execution(init_cuda): kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() g.launch(config, kernel) cloned_cu_graph = handle_return(driver.cuGraphClone(driver.CUgraph(g.handle))) @@ -465,7 +465,7 @@ def test_kernel_node_reconstruction_preserves_validity(init_cuda): kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) - g = GraphDef() + g = GraphDefinition() kernel_node = g.launch(config, kernel) # Chain a second node so we can reconstruct the kernel node via pred event = Device().create_event() diff --git a/cuda_core/tests/graph/test_graphdef_mutation.py b/cuda_core/tests/graph/test_graph_definition_mutation.py similarity index 96% rename from cuda_core/tests/graph/test_graphdef_mutation.py rename to cuda_core/tests/graph/test_graph_definition_mutation.py index 2f9dd442da5..b176503e3df 100644 --- a/cuda_core/tests/graph/test_graphdef_mutation.py +++ b/cuda_core/tests/graph/test_graph_definition_mutation.py @@ -11,7 +11,7 @@ from cuda.core import Device, LaunchConfig, LegacyPinnedMemoryResource from cuda.core._utils.cuda_utils import CUDAError -from cuda.core.graph import GraphDef, KernelNode, MemsetNode +from cuda.core.graph import GraphDefinition, KernelNode, MemsetNode class YRig: @@ -53,7 +53,7 @@ def __init__(self): self.ptr_b = self._arr[1:].ctypes.data self.ptr_r = self._arr[2:].ctypes.data - self.graph_def = GraphDef() + self.graph_def = GraphDefinition() self.stream = None # Arm A @@ -210,7 +210,7 @@ def test_insert_b(self, init_cuda): def test_adjacency_set_interface(init_cuda): """Exercise every MutableSet method on AdjacencySetProxy.""" - g = GraphDef() + g = GraphDefinition() hub = g.empty() items = [g.empty() for _ in range(5)] assert_mutable_set_interface(hub.succ, items) @@ -218,7 +218,7 @@ def test_adjacency_set_interface(init_cuda): def test_adjacency_set_pred_direction(init_cuda): """Verify that pred works symmetrically with succ.""" - g = GraphDef() + g = GraphDefinition() target = g.empty() x, y, z = (g.empty() for _ in range(3)) @@ -241,7 +241,7 @@ def test_adjacency_set_pred_direction(init_cuda): def test_adjacency_set_property_setter(init_cuda): """Verify that assigning to node.pred or node.succ replaces all edges.""" - g = GraphDef() + g = GraphDefinition() hub = g.empty() a, b, c = (g.empty() for _ in range(3)) @@ -273,7 +273,7 @@ def test_destroyed_node(init_cuda): arr[:] = 0 ptr = arr[0:].ctypes.data - g = GraphDef() + g = GraphDefinition() a = g.memset(ptr, 0, 4) b = a.memset(ptr, 42, 4) @@ -309,7 +309,7 @@ def test_destroyed_node(init_cuda): def test_add_wrong_type(init_cuda): """Adding a non-GraphNode raises TypeError.""" - g = GraphDef() + g = GraphDefinition() node = g.empty() with pytest.raises(TypeError, match="expected GraphNode"): node.succ.add("not a node") @@ -319,8 +319,8 @@ def test_add_wrong_type(init_cuda): def test_cross_graph_edge(init_cuda): """Adding an edge to a node from a different graph raises CUDAError.""" - g1 = GraphDef() - g2 = GraphDef() + g1 = GraphDefinition() + g2 = GraphDefinition() a = g1.empty() b = g2.empty() with pytest.raises(CUDAError): @@ -329,7 +329,7 @@ def test_cross_graph_edge(init_cuda): def test_self_edge(init_cuda): """Adding a self-edge raises CUDAError.""" - g = GraphDef() + g = GraphDefinition() node = g.empty() with pytest.raises(CUDAError): node.succ.add(node) @@ -365,7 +365,7 @@ def test_convert_linear_to_fan_in(init_cuda): ptrs = [arr[i:].ctypes.data for i in range(5)] # Create the initial graph. - g = GraphDef() + g = GraphDefinition() prev = g for i, val in enumerate(values): prev = prev.memset(ptrs[i], val, 1) diff --git a/cuda_core/tests/graph/test_graph_update.py b/cuda_core/tests/graph/test_graph_update.py index 84dfd4daa9c..01d0183de5c 100644 --- a/cuda_core/tests/graph/test_graph_update.py +++ b/cuda_core/tests/graph/test_graph_update.py @@ -10,10 +10,10 @@ from cuda.core import Device, LaunchConfig, LegacyPinnedMemoryResource, launch from cuda.core._utils.cuda_utils import CUDAError -from cuda.core.graph import GraphDef +from cuda.core.graph import GraphDefinition -@pytest.mark.parametrize("builder", ["GraphBuilder", "GraphDef"]) +@pytest.mark.parametrize("builder", ["GraphBuilder", "GraphDefinition"]) @requires_module(np, "2.1") def test_graph_update_kernel_args(init_cuda, builder): """Update redirects a kernel to write to a different pointer.""" @@ -35,10 +35,10 @@ def build(ptr): launch(gb, LaunchConfig(grid=1, block=1), add_one, ptr) finished = gb.end_building() return finished.complete(), finished - elif builder == "GraphDef": + elif builder == "GraphDefinition": def build(ptr): - g = GraphDef() + g = GraphDefinition() n = g.launch(LaunchConfig(grid=1, block=1), add_one, ptr) n.launch(LaunchConfig(grid=1, block=1), add_one, ptr) return g.instantiate(), g @@ -205,5 +205,5 @@ def test_graph_update_wrong_type(init_cuda): launch(gb, LaunchConfig(grid=1, block=1), empty_kernel) graph = gb.end_building().complete() - with pytest.raises(TypeError, match="expected GraphBuilder or GraphDef"): + with pytest.raises(TypeError, match="expected GraphBuilder or GraphDefinition"): graph.update("not a graph") diff --git a/cuda_core/tests/helpers/misc.py b/cuda_core/tests/helpers/misc.py index 6b83c751abd..89bc175a97d 100644 --- a/cuda_core/tests/helpers/misc.py +++ b/cuda_core/tests/helpers/misc.py @@ -5,7 +5,7 @@ def try_create_condition(g, default_value=1): - """Create a Condition on graph *g*, skipping the test if unsupported.""" + """Create a GraphCondition on graph *g*, skipping the test if unsupported.""" from cuda.core._utils.cuda_utils import CUDAError try: diff --git a/cuda_core/tests/test_object_protocols.py b/cuda_core/tests/test_object_protocols.py index 8c58ddcb9c2..b9f396310d7 100644 --- a/cuda_core/tests/test_object_protocols.py +++ b/cuda_core/tests/test_object_protocols.py @@ -28,7 +28,7 @@ system, ) from cuda.core._program import _can_load_generated_ptx -from cuda.core.graph import GraphDef +from cuda.core.graph import GraphDefinition def _skip_if_no_mempool(): @@ -244,7 +244,7 @@ def sample_ipc_event_descriptor(ipc_device): # ============================================================================= -# Fixtures - Graph types (GraphDef and GraphNode) +# Fixtures - Graph types (GraphDefinition and GraphNode) # ============================================================================= ALLOC_SIZE = 1024 @@ -252,14 +252,14 @@ def sample_ipc_event_descriptor(ipc_device): @pytest.fixture def sample_graphdef(init_cuda): - """A sample GraphDef.""" - return GraphDef() + """A sample GraphDefinition.""" + return GraphDefinition() @pytest.fixture def sample_graphdef_alt(init_cuda): - """An alternate GraphDef (for inequality testing).""" - return GraphDef() + """An alternate GraphDefinition (for inequality testing).""" + return GraphDefinition() @pytest.fixture @@ -379,7 +379,7 @@ def sample_memcpy_node_alt(sample_graphdef): @pytest.fixture def sample_child_graph_node(sample_graphdef): """A ChildGraphNode.""" - child = GraphDef() + child = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") child.launch(LaunchConfig(grid=1, block=1), kernel) @@ -389,7 +389,7 @@ def sample_child_graph_node(sample_graphdef): @pytest.fixture def sample_child_graph_node_alt(sample_graphdef): """An alternate ChildGraphNode from same graph.""" - child = GraphDef() + child = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") child.launch(LaunchConfig(grid=1, block=1), kernel) @@ -446,13 +446,13 @@ def other_callback(): @pytest.fixture def sample_condition(sample_graphdef): - """A Condition object.""" + """A GraphCondition object.""" return try_create_condition(sample_graphdef) @pytest.fixture def sample_condition_alt(sample_graphdef): - """An alternate Condition from same graph.""" + """An alternate GraphCondition from same graph.""" return try_create_condition(sample_graphdef) @@ -682,8 +682,8 @@ def sample_switch_node_alt(sample_graphdef): ("sample_program_ptx", r""), ("sample_program_nvvm", r""), # Graph types - ("sample_graphdef", r""), - ("sample_condition", r""), + ("sample_graphdef", r""), + ("sample_condition", r""), ("sample_root_node", r""), ("sample_empty_node", r""), ("sample_alloc_node", r""), From 20f9a54b6c3608982125dada90666dedc45b073d Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Wed, 29 Apr 2026 14:33:56 -0700 Subject: [PATCH 046/244] Reduce nightly test repetitions from 100 to 5 (#1991) The nightly schedule runs every test 100x (via pytest --count) across ~60 matrix configurations on 3 platforms, causing 6-12 hour runtimes that block the runner queue for PR CI during the day. 5 repetitions still provide useful flaky-test signal without monopolizing runners. Co-authored-by: Leo's bot --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 292263f0c44..dee6cbe8b9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -343,7 +343,7 @@ jobs: build-type: pull-request host-platform: ${{ matrix.host-platform }} build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }} # See test-linux-64 for why test jobs are split by platform. @@ -368,7 +368,7 @@ jobs: build-type: pull-request host-platform: ${{ matrix.host-platform }} build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }} # See test-linux-64 for why test jobs are split by platform. @@ -393,7 +393,7 @@ jobs: build-type: pull-request host-platform: ${{ matrix.host-platform }} build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }} doc: From 66eb984278c8812efc763e089fda041d7bb45874 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:48:46 -0400 Subject: [PATCH 047/244] CI: Check versioned release notes exist before releasing (#1907) * ci: check versioned release notes exist before releasing Add a check-release-notes job to the release workflow that verifies the versioned release-notes file (e.g. 13.1.0-notes.rst) exists and is non-empty for each package being released. The job blocks doc, upload-archive, and publish-testpypi via needs: gates. Helper script at toolshed/check_release_notes.py parses the git tag, maps component to package directories, and checks file presence. Post-release tags (.postN) are silently skipped. Tests cover tag parsing, component mapping, missing/empty detection, and the CLI. Refs #1326 Co-Authored-By: Claude Opus 4.6 (1M context) * fix: checkout release tag ref in check-release-notes job Ensures the release-notes check validates the tagged tree, not the default branch HEAD. Without this, manually triggered runs could validate the wrong commit. Co-Authored-By: Claude Opus 4.6 (1M context) * style: fix ruff lint and format issues Co-Authored-By: Claude Opus 4.6 (1M context) * ci: move release-notes checker to ci/tools and drop component=all Addresses review feedback on #1907: - Relocate check_release_notes.py (and its tests) from toolshed/ to ci/tools/, matching validate-release-wheels which is the closest conceptual neighbor. toolshed/ is for scripts we rarely re-run; this one is invoked from release.yml on every release. - Drop the `all` component. The shared-version assumption it encoded no longer matches the repo's independent tag families (v*, cuda-core-v*, cuda-pathfinder-v*). The broader release-workflow cleanup for `all` will happen in a separate pass. - Register ci/tools/tests under pytest testpaths, and run the unit tests in the check-release-notes job before invoking the script. * ci: tighten release-notes tag parser Addresses defensive issues flagged on earlier revisions of this branch: - Replace the single permissive TAG_RE with a per-component pattern map. Each component now only matches its own tag-prefix family (cuda-core-v*, cuda-pathfinder-v*, bare v* for cuda-bindings and cuda-python), so a cuda-core tag paired with --component cuda-pathfinder is rejected rather than silently quarried for the wrong notes file. - Restrict the captured version to digit-prefixed word chars and dots so malformed inputs like "v../evil" or "v1/2/3" cannot flow into the joined notes path. - Return exit code 2 on unparsable tags and component/prefix mismatches, matching the documented CLI contract. Only genuine missing/empty notes return 1. - Route error output to stderr so stdout stays clean when the check is used as a CI gate. - Add tests for the new rejection cases. * apply Ralf's patch --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Leo Fang --- .github/workflows/release.yml | 32 +++- ci/tools/check_release_notes.py | 121 +++++++++++++++ ci/tools/tests/test_check_release_notes.py | 164 +++++++++++++++++++++ pytest.ini | 1 + 4 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 ci/tools/check_release_notes.py create mode 100644 ci/tools/tests/test_check_release_notes.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97d58d8ae59..7cbc847e2c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,6 @@ on: - cuda-bindings - cuda-pathfinder - cuda-python - - all git-tag: description: "The release git tag" required: true @@ -89,6 +88,30 @@ jobs: gh release create "${{ inputs.git-tag }}" --draft --repo "${{ github.repository }}" --title "Release ${{ inputs.git-tag }}" --notes "Release ${{ inputs.git-tag }}" fi + check-release-notes: + runs-on: ubuntu-latest + steps: + - name: Checkout Source + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.git-tag }} + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Self-test release-notes checker + run: | + pip install pytest + pytest ci/tools/tests + + - name: Check versioned release notes exist + run: | + python ci/tools/check_release_notes.py \ + --git-tag "${{ inputs.git-tag }}" \ + --component "${{ inputs.component }}" + doc: name: Build release docs if: ${{ github.repository_owner == 'nvidia' }} @@ -99,6 +122,7 @@ jobs: pull-requests: write needs: - check-tag + - check-release-notes - determine-run-id secrets: inherit uses: ./.github/workflows/build-docs.yml @@ -114,6 +138,7 @@ jobs: contents: write needs: - check-tag + - check-release-notes - determine-run-id - doc secrets: inherit @@ -128,11 +153,12 @@ jobs: runs-on: ubuntu-latest needs: - check-tag + - check-release-notes - determine-run-id - doc environment: name: testpypi - url: https://test.pypi.org/${{ inputs.component != 'all' && format('p/{0}/', inputs.component) || '' }} + url: https://test.pypi.org/p/${{ inputs.component }}/ permissions: id-token: write steps: @@ -162,7 +188,7 @@ jobs: - publish-testpypi environment: name: pypi - url: https://pypi.org/${{ inputs.component != 'all' && format('p/{0}/', inputs.component) || '' }} + url: https://pypi.org/p/${{ inputs.component }}/ permissions: id-token: write steps: diff --git a/ci/tools/check_release_notes.py b/ci/tools/check_release_notes.py new file mode 100644 index 00000000000..ad2a640a865 --- /dev/null +++ b/ci/tools/check_release_notes.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Check that versioned release-notes files exist before releasing. + +Usage: + python check_release_notes.py --git-tag --component + +Exit codes: + 0 — release notes present and non-empty (or .post version, skipped) + 1 — release notes missing or empty + 2 — invalid arguments (including unparsable tag, or component/tag-prefix mismatch) +""" + +from __future__ import annotations + +import argparse +import os +import re +import sys + +COMPONENT_TO_PACKAGE: dict[str, str] = { + "cuda-core": "cuda_core", + "cuda-bindings": "cuda_bindings", + "cuda-pathfinder": "cuda_pathfinder", + "cuda-python": "cuda_python", +} + +# Version characters are restricted to digit-prefixed word chars and dots, so +# malformed inputs like "v../evil" or "v1/2/3" cannot flow into the notes path. +_VERSION_PATTERN = r"\d[\w.]*" + +# Each component has exactly one valid tag-prefix form. cuda-bindings and +# cuda-python share the bare "v" namespace (setuptools-scm lookup). +COMPONENT_TO_TAG_RE: dict[str, re.Pattern[str]] = { + "cuda-bindings": re.compile(rf"^v(?P{_VERSION_PATTERN})$"), + "cuda-python": re.compile(rf"^v(?P{_VERSION_PATTERN})$"), + "cuda-core": re.compile(rf"^cuda-core-v(?P{_VERSION_PATTERN})$"), + "cuda-pathfinder": re.compile(rf"^cuda-pathfinder-v(?P{_VERSION_PATTERN})$"), +} + + +def parse_version_from_tag(git_tag: str, component: str) -> str | None: + """Extract the version string from a tag, given the target component. + + Returns None if the tag does not match the component's expected prefix + or contains characters outside the allowed version set. + """ + pattern = COMPONENT_TO_TAG_RE.get(component) + if pattern is None: + return None + m = pattern.match(git_tag) + return m.group("version") if m else None + + +def is_post_release(version: str) -> bool: + return ".post" in version + + +def notes_path(package: str, version: str) -> str: + return os.path.join(package, "docs", "source", "release", f"{version}-notes.rst") + + +def check_release_notes(git_tag: str, component: str, repo_root: str = ".") -> list[tuple[str, str]]: + """Return a list of (path, reason) for missing or empty release notes. + + Returns an empty list when notes are present and non-empty, or when the + tag is a .post release (no new notes required). + """ + if component not in COMPONENT_TO_PACKAGE: + return [("", f"unknown component '{component}'")] + + version = parse_version_from_tag(git_tag, component) + if version is None: + return [("", f"cannot parse version from tag '{git_tag}' for component '{component}'")] + + if is_post_release(version): + return [] + + path = notes_path(COMPONENT_TO_PACKAGE[component], version) + full = os.path.join(repo_root, path) + if not os.path.isfile(full): + return [(path, "missing")] + if os.path.getsize(full) == 0: + return [(path, "empty")] + return [] + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--git-tag", required=True) + parser.add_argument("--component", required=True, choices=list(COMPONENT_TO_PACKAGE)) + parser.add_argument("--repo-root", default=".") + args = parser.parse_args(argv) + + version = parse_version_from_tag(args.git_tag, args.component) + if version is None: + print( + f"ERROR: tag {args.git_tag!r} does not match the expected format for component {args.component!r}.", + file=sys.stderr, + ) + return 2 + + if is_post_release(version): + print(f"Post-release tag ({args.git_tag}), skipping release-notes check.") + return 0 + + problems = check_release_notes(args.git_tag, args.component, args.repo_root) + if not problems: + print(f"Release notes present for tag {args.git_tag}, component {args.component}.") + return 0 + + print(f"ERROR: missing or empty release notes for tag {args.git_tag}:", file=sys.stderr) + for path, reason in problems: + print(f" - {path} ({reason})", file=sys.stderr) + print("Add versioned release notes before releasing.", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ci/tools/tests/test_check_release_notes.py b/ci/tools/tests/test_check_release_notes.py new file mode 100644 index 00000000000..8033eca620c --- /dev/null +++ b/ci/tools/tests/test_check_release_notes.py @@ -0,0 +1,164 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from check_release_notes import ( + check_release_notes, + is_post_release, + main, + parse_version_from_tag, +) + + +class TestParseVersionFromTag: + def test_plain_tag_bindings(self): + assert parse_version_from_tag("v13.1.0", "cuda-bindings") == "13.1.0" + + def test_plain_tag_python(self): + assert parse_version_from_tag("v13.1.0", "cuda-python") == "13.1.0" + + def test_component_prefix_core(self): + assert parse_version_from_tag("cuda-core-v0.7.0", "cuda-core") == "0.7.0" + + def test_component_prefix_pathfinder(self): + assert parse_version_from_tag("cuda-pathfinder-v1.5.2", "cuda-pathfinder") == "1.5.2" + + def test_post_release(self): + assert parse_version_from_tag("v12.6.2.post1", "cuda-bindings") == "12.6.2.post1" + + def test_invalid_tag(self): + assert parse_version_from_tag("not-a-tag", "cuda-core") is None + + def test_no_v_prefix(self): + assert parse_version_from_tag("13.1.0", "cuda-bindings") is None + + def test_component_prefix_mismatch(self): + # cuda-core-v* must not be accepted for component=cuda-pathfinder + assert parse_version_from_tag("cuda-core-v0.7.0", "cuda-pathfinder") is None + + def test_bare_v_rejected_for_core(self): + # bare v* belongs to cuda-bindings/cuda-python, not cuda-core + assert parse_version_from_tag("v0.7.0", "cuda-core") is None + + def test_unknown_component(self): + assert parse_version_from_tag("v13.1.0", "bogus") is None + + def test_path_traversal_rejected(self): + assert parse_version_from_tag("v1.0.0/../evil", "cuda-bindings") is None + + def test_path_separator_rejected(self): + assert parse_version_from_tag("v1/2/3", "cuda-bindings") is None + + def test_leading_dot_rejected(self): + assert parse_version_from_tag("v.1.0", "cuda-bindings") is None + + def test_whitespace_rejected(self): + assert parse_version_from_tag("v1.0.0 ", "cuda-bindings") is None + + def test_trailing_suffix_rejected(self): + # \w permits alphanumerics + underscore only; hyphens and shell meta-chars are out + assert parse_version_from_tag("v1.0.0-extra", "cuda-bindings") is None + + +class TestIsPostRelease: + def test_normal(self): + assert not is_post_release("13.1.0") + + def test_post(self): + assert is_post_release("12.6.2.post1") + + def test_post_no_number(self): + assert is_post_release("1.0.0.post") + + +class TestCheckReleaseNotes: + def _make_notes(self, tmp_path, pkg, version, content="Release notes."): + d = tmp_path / pkg / "docs" / "source" / "release" + d.mkdir(parents=True, exist_ok=True) + f = d / f"{version}-notes.rst" + f.write_text(content) + return f + + def test_present_and_nonempty(self, tmp_path): + self._make_notes(tmp_path, "cuda_core", "0.7.0") + problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", str(tmp_path)) + assert problems == [] + + def test_missing(self, tmp_path): + problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", str(tmp_path)) + assert len(problems) == 1 + assert problems[0][1] == "missing" + + def test_empty(self, tmp_path): + self._make_notes(tmp_path, "cuda_core", "0.7.0", content="") + problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", str(tmp_path)) + assert len(problems) == 1 + assert problems[0][1] == "empty" + + def test_post_release_skipped(self, tmp_path): + problems = check_release_notes("v12.6.2.post1", "cuda-bindings", str(tmp_path)) + assert problems == [] + + def test_invalid_tag(self, tmp_path): + problems = check_release_notes("not-a-tag", "cuda-core", str(tmp_path)) + assert len(problems) == 1 + assert "cannot parse" in problems[0][1] + + def test_component_prefix_mismatch(self, tmp_path): + # Pass a cuda-core tag with component=cuda-pathfinder; must be rejected. + problems = check_release_notes("cuda-core-v0.7.0", "cuda-pathfinder", str(tmp_path)) + assert len(problems) == 1 + assert "cannot parse" in problems[0][1] + + def test_unknown_component(self, tmp_path): + problems = check_release_notes("v13.1.0", "bogus", str(tmp_path)) + assert len(problems) == 1 + assert "unknown component" in problems[0][1] + + def test_plain_v_tag(self, tmp_path): + self._make_notes(tmp_path, "cuda_python", "13.1.0") + problems = check_release_notes("v13.1.0", "cuda-python", str(tmp_path)) + assert problems == [] + + +class TestMain: + def test_success(self, tmp_path): + d = tmp_path / "cuda_core" / "docs" / "source" / "release" + d.mkdir(parents=True) + (d / "0.7.0-notes.rst").write_text("Notes here.") + rc = main(["--git-tag", "cuda-core-v0.7.0", "--component", "cuda-core", "--repo-root", str(tmp_path)]) + assert rc == 0 + + def test_failure(self, tmp_path): + rc = main(["--git-tag", "cuda-core-v0.7.0", "--component", "cuda-core", "--repo-root", str(tmp_path)]) + assert rc == 1 + + def test_post_skip(self, tmp_path): + rc = main(["--git-tag", "v12.6.2.post1", "--component", "cuda-bindings", "--repo-root", str(tmp_path)]) + assert rc == 0 + + def test_unparsable_tag_returns_2(self, tmp_path): + rc = main(["--git-tag", "not-a-tag", "--component", "cuda-core", "--repo-root", str(tmp_path)]) + assert rc == 2 + + def test_path_traversal_returns_2(self, tmp_path): + rc = main(["--git-tag", "v1.0.0/../evil", "--component", "cuda-bindings", "--repo-root", str(tmp_path)]) + assert rc == 2 + + def test_component_prefix_mismatch_returns_2(self, tmp_path): + rc = main( + [ + "--git-tag", + "cuda-core-v0.7.0", + "--component", + "cuda-pathfinder", + "--repo-root", + str(tmp_path), + ] + ) + assert rc == 2 diff --git a/pytest.ini b/pytest.ini index 978e659bf07..d1a82feb749 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,6 +12,7 @@ testpaths = cuda_bindings/tests cuda_core/tests tests/integration + ci/tools/tests markers = pathfinder: tests for cuda_pathfinder From edb19011e1b7d732b7cb6ea0900dada237e26205 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Wed, 29 Apr 2026 20:03:36 -0700 Subject: [PATCH 048/244] Fix tensor bridge DLL import failure on Windows (#1988) * Fix tensor bridge DLL import failure on Windows aoti_torch_get_current_cuda_stream lives in torch_cuda.dll, not torch_cpu.dll. The stub import library pointed at the wrong DLL, causing "The specified procedure could not be found" on Windows. - Move aoti_torch_get_current_cuda_stream from aoti_shim.def (torch_cpu.dll) to new aoti_shim_cuda.def (torch_cuda.dll) - Update build_hooks.py to generate stub libs for both DLLs via a loop - Add torch_cuda.dll to delvewheel exclude list Co-Authored-By: Claude Opus 4.6 (1M context) * Add SPDX headers to aoti_shim_cuda.def Co-Authored-By: Claude Opus 4.6 (1M context) * Resolve aoti_torch_get_current_cuda_stream lazily at runtime The symbol lives in torch_cuda (not torch_cpu), so linking against it at build time breaks CPU-only PyTorch installs and requires a second stub import library on Windows. Instead, resolve it lazily on first use via dlsym (Linux) / LoadLibrary+GetProcAddress (Windows). The cached function pointer keeps subsequent calls fully in C with zero Python overhead. This reverts the two-def-file approach from the previous commit and replaces it with a self-contained inline C helper that handles both platforms. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_core/build_hooks.py | 3 ++ cuda_core/cuda/core/_include/aoti_shim.def | 1 - cuda_core/cuda/core/_include/aoti_shim.h | 18 +++++---- cuda_core/cuda/core/_tensor_bridge.pyx | 43 ++++++++++++++++++++-- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 444da18eb13..f4fb4af01f7 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -186,6 +186,9 @@ def get_sources(mod_name): # On Windows, _tensor_bridge.pyx needs a stub import library so the MSVC # linker can resolve the AOTI symbols (they live in torch_cpu.dll at # runtime). We generate the .lib from a .def file at build time. + # Note: aoti_torch_get_current_cuda_stream lives in torch_cuda.dll and + # is resolved lazily at runtime (not via the stub lib) — see + # _tensor_bridge.pyx. _aoti_extra_link_args = [] if sys.platform == "win32": _def_file = os.path.join("cuda", "core", "_include", "aoti_shim.def") diff --git a/cuda_core/cuda/core/_include/aoti_shim.def b/cuda_core/cuda/core/_include/aoti_shim.def index 5cc6897e815..e21097bd25e 100644 --- a/cuda_core/cuda/core/_include/aoti_shim.def +++ b/cuda_core/cuda/core/_include/aoti_shim.def @@ -34,4 +34,3 @@ EXPORTS aoti_torch_get_device_index aoti_torch_device_type_cpu aoti_torch_device_type_cuda - aoti_torch_get_current_cuda_stream diff --git a/cuda_core/cuda/core/_include/aoti_shim.h b/cuda_core/cuda/core/_include/aoti_shim.h index 809bdb1a2a6..464d27de46c 100644 --- a/cuda_core/cuda/core/_include/aoti_shim.h +++ b/cuda_core/cuda/core/_include/aoti_shim.h @@ -52,10 +52,13 @@ typedef struct AtenTensorOpaque* AtenTensorHandle; /* * IMPORTANT: Keep the AOTI_SHIM_API declaration list below in sync with - * aoti_shim.def. On Windows, build_hooks.py turns that .def file into the + * aoti_shim.def. On Windows, build_hooks.py turns that .def file into the * stub import library that MSVC needs to link _tensor_bridge without making - * PyTorch a build-time dependency. If you add, remove, or rename an imported - * AOTI symbol here, update aoti_shim.def in the same change. + * PyTorch a build-time dependency. If you add, remove, or rename an + * imported AOTI symbol here, update aoti_shim.def in the same change. + * + * Exception: aoti_torch_get_current_cuda_stream lives in torch_cuda (not + * torch_cpu) and is resolved lazily at runtime — see _tensor_bridge.pyx. */ /* ---- tensor metadata --------------------------------------------------- */ @@ -105,10 +108,11 @@ AOTI_SHIM_API AOTITorchError aoti_torch_get_device_index( AOTI_SHIM_API int32_t aoti_torch_device_type_cpu(void); AOTI_SHIM_API int32_t aoti_torch_device_type_cuda(void); -/* ---- stream -------------------------------------------------------------- */ - -AOTI_SHIM_API AOTITorchError aoti_torch_get_current_cuda_stream( - int32_t device_index, void** ret_stream); +/* ---- stream -------------------------------------------------------------- + * aoti_torch_get_current_cuda_stream is NOT declared here — it lives in + * torch_cuda (not torch_cpu) and is resolved at runtime. See the inline + * C helper _resolve_cuda_stream_fn() in _tensor_bridge.pyx. + * ---------------------------------------------------------------------- */ #ifdef __cplusplus } /* extern "C" */ diff --git a/cuda_core/cuda/core/_tensor_bridge.pyx b/cuda_core/cuda/core/_tensor_bridge.pyx index 388ca738dbb..07eec56537b 100644 --- a/cuda_core/cuda/core/_tensor_bridge.pyx +++ b/cuda_core/cuda/core/_tensor_bridge.pyx @@ -103,8 +103,38 @@ cdef extern from "_include/aoti_shim.h": int32_t aoti_torch_device_type_cpu() int32_t aoti_torch_device_type_cuda() - # stream - AOTITorchError aoti_torch_get_current_cuda_stream(int32_t, void**) + # Note: aoti_torch_get_current_cuda_stream is NOT declared here because + # it lives in torch_cuda.dll (not torch_cpu.dll). It is resolved lazily + # at runtime via dlsym / GetProcAddress — see _resolve_cuda_stream_fn(). + +# Runtime resolution for aoti_torch_get_current_cuda_stream. +# This symbol lives in torch_cuda.dll (Windows) / libtorch_cuda.so (Linux), +# NOT in torch_cpu. We resolve it lazily on first use so that the module +# can be imported even with CPU-only PyTorch. +ctypedef AOTITorchError (*_get_cuda_stream_fn_t)(int32_t, void**) nogil + +cdef extern from *: + """ + #ifdef _WIN32 + #include + static void* _resolve_cuda_stream_fn(void) { + HMODULE h = LoadLibraryA("torch_cuda.dll"); + if (!h) return NULL; + return (void*)GetProcAddress(h, "aoti_torch_get_current_cuda_stream"); + } + #else + #include + #ifndef RTLD_DEFAULT + #define RTLD_DEFAULT ((void*)0) + #endif + static void* _resolve_cuda_stream_fn(void) { + return dlsym(RTLD_DEFAULT, "aoti_torch_get_current_cuda_stream"); + } + #endif + """ + void* _resolve_cuda_stream_fn() nogil + +cdef _get_cuda_stream_fn_t _cached_get_cuda_stream = NULL import numpy import sys @@ -274,10 +304,17 @@ cpdef int sync_torch_stream(int32_t device_index, the consumer stream wait on it. This is a no-op if both streams are the same. """ + global _cached_get_cuda_stream cdef void* producer_s cdef EventHandle h_event - check_aoti(aoti_torch_get_current_cuda_stream(device_index, &producer_s), + if _cached_get_cuda_stream == NULL: + _cached_get_cuda_stream = <_get_cuda_stream_fn_t>_resolve_cuda_stream_fn() + if _cached_get_cuda_stream == NULL: + raise RuntimeError( + "Cannot resolve aoti_torch_get_current_cuda_stream from " + "torch_cuda — is CUDA-enabled PyTorch installed?") + check_aoti(_cached_get_cuda_stream(device_index, &producer_s), b"aoti_torch_get_current_cuda_stream") if producer_s != consumer_s: with nogil: From 97c5b2a320ce57909f1ff8ea7dba527d9e8c64fb Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Thu, 30 Apr 2026 09:55:05 -0400 Subject: [PATCH 049/244] Restructure graph/ subpackage to use __all__ and star imports (#1944) Add __all__ to each graph submodule and rewrite graph/__init__.py to use 'from ._module import *' pattern, matching the convention already used by _memory/ and system/ subpackages. - _graph_builder.pyx: __all__ = Graph, GraphBuilder, GraphCompleteOptions, GraphDebugPrintOptions - _graph_def.pyx: __all__ = Condition, GraphAllocOptions, GraphDef - _graph_node.pyx: __all__ = GraphNode - _subclasses.pyx: __all__ = all 15 node subclasses - __init__.py: replaced explicit named imports with star imports --- cuda_core/cuda/core/graph/__init__.py | 33 +++---------------- cuda_core/cuda/core/graph/_graph_builder.pyx | 3 ++ .../cuda/core/graph/_graph_definition.pyx | 2 ++ cuda_core/cuda/core/graph/_graph_node.pyx | 2 ++ cuda_core/cuda/core/graph/_subclasses.pyx | 18 ++++++++++ 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/cuda_core/cuda/core/graph/__init__.py b/cuda_core/cuda/core/graph/__init__.py index 57a6988d861..e1091114368 100644 --- a/cuda_core/cuda/core/graph/__init__.py +++ b/cuda_core/cuda/core/graph/__init__.py @@ -2,32 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -from cuda.core.graph._graph_builder import ( - Graph, - GraphBuilder, - GraphCompleteOptions, - GraphDebugPrintOptions, -) -from cuda.core.graph._graph_definition import ( - GraphAllocOptions, - GraphCondition, - GraphDefinition, -) -from cuda.core.graph._graph_node import GraphNode -from cuda.core.graph._subclasses import ( - AllocNode, - ChildGraphNode, - ConditionalNode, - EmptyNode, - EventRecordNode, - EventWaitNode, - FreeNode, - HostCallbackNode, - IfElseNode, - IfNode, - KernelNode, - MemcpyNode, - MemsetNode, - SwitchNode, - WhileNode, -) +from ._graph_builder import * +from ._graph_definition import * +from ._graph_node import * +from ._subclasses import * diff --git a/cuda_core/cuda/core/graph/_graph_builder.pyx b/cuda_core/cuda/core/graph/_graph_builder.pyx index 42370029706..5b304a24d38 100644 --- a/cuda_core/cuda/core/graph/_graph_builder.pyx +++ b/cuda_core/cuda/core/graph/_graph_builder.pyx @@ -21,6 +21,9 @@ from cuda.core._utils.cuda_utils import ( handle_return, ) +__all__ = ['Graph', 'GraphBuilder', 'GraphCompleteOptions', 'GraphDebugPrintOptions'] + + @dataclass class GraphDebugPrintOptions: """Options for debug_dot_print(). diff --git a/cuda_core/cuda/core/graph/_graph_definition.pyx b/cuda_core/cuda/core/graph/_graph_definition.pyx index 195a1e300b0..56b0af5d9ec 100644 --- a/cuda_core/cuda/core/graph/_graph_definition.pyx +++ b/cuda_core/cuda/core/graph/_graph_definition.pyx @@ -27,6 +27,8 @@ from dataclasses import dataclass from cuda.core._utils.cuda_utils import driver +__all__ = ['GraphCondition', 'GraphAllocOptions', 'GraphDefinition'] + cdef class GraphCondition: """A condition variable for conditional graph nodes. diff --git a/cuda_core/cuda/core/graph/_graph_node.pyx b/cuda_core/cuda/core/graph/_graph_node.pyx index 553304885be..bd10bfa007f 100644 --- a/cuda_core/cuda/core/graph/_graph_node.pyx +++ b/cuda_core/cuda/core/graph/_graph_node.pyx @@ -61,6 +61,8 @@ import weakref from cuda.core.graph._adjacency_set_proxy import AdjacencySetProxy from cuda.core._utils.cuda_utils import driver +__all__ = ['GraphNode'] + # See _cpp/REGISTRY_DESIGN.md (Level 2: Resource Handle -> Python Object) _node_registry = weakref.WeakValueDictionary() diff --git a/cuda_core/cuda/core/graph/_subclasses.pyx b/cuda_core/cuda/core/graph/_subclasses.pyx index ef08bb30856..86cf9eea53e 100644 --- a/cuda_core/cuda/core/graph/_subclasses.pyx +++ b/cuda_core/cuda/core/graph/_subclasses.pyx @@ -34,6 +34,24 @@ from cuda.core.graph._utils cimport _is_py_host_trampoline from cuda.core._utils.cuda_utils import driver, handle_return +__all__ = [ + 'AllocNode', + 'ChildGraphNode', + 'ConditionalNode', + 'EmptyNode', + 'EventRecordNode', + 'EventWaitNode', + 'FreeNode', + 'HostCallbackNode', + 'IfElseNode', + 'IfNode', + 'KernelNode', + 'MemcpyNode', + 'MemsetNode', + 'SwitchNode', + 'WhileNode', +] + cdef bint _has_cuGraphNodeGetParams = False cdef bint _version_checked = False From 868b4c3a84cc7948d78a8fcce153fd37866cc0a2 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Thu, 30 Apr 2026 12:55:24 -0400 Subject: [PATCH 050/244] BUG: NVML system events may only be registered once per process (#1992) * BUG: NVML system events may only be registered once per process * Avoid possible free of NULL pointer on Windows --- cuda_core/cuda/core/system/_system_events.pyx | 4 +++- cuda_core/tests/system/test_system_events.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cuda_core/cuda/core/system/_system_events.pyx b/cuda_core/cuda/core/system/_system_events.pyx index 81f69d872af..eea17523294 100644 --- a/cuda_core/cuda/core/system/_system_events.pyx +++ b/cuda_core/cuda/core/system/_system_events.pyx @@ -81,13 +81,15 @@ cdef class RegisteredSystemEvents: initialize() + self._event_set = 0 self._event_set = nvml.system_event_set_create() # If this raises, the event needs to be freed and this is handled by # this class's __dealloc__ method. nvml.system_register_events(event_bitmask, self._event_set) def __dealloc__(self): - nvml.system_event_set_free(self._event_set) + if self._event_set != 0: + nvml.system_event_set_free(self._event_set) def wait(self, timeout_ms: int = 0, buffer_size: int = 1) -> SystemEvents: """ diff --git a/cuda_core/tests/system/test_system_events.py b/cuda_core/tests/system/test_system_events.py index 8cdb10a4e90..493f1b79934 100644 --- a/cuda_core/tests/system/test_system_events.py +++ b/cuda_core/tests/system/test_system_events.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 @@ -22,6 +22,10 @@ def test_register_events(): # Also, some hardware doesn't support any event types. - events = system.register_events([system.SystemEventType.GPU_DRIVER_UNBIND]) + try: + events = system.register_events([system.SystemEventType.GPU_DRIVER_UNBIND]) + except system.UnknownError: + pytest.skip("system events may only be registered once per process") + with pytest.raises(system.TimeoutError): events.wait(timeout_ms=500, buffer_size=1) From 7f32b39c22e361c487273adbc50c5b771919e5fe Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Thu, 30 Apr 2026 14:19:23 -0400 Subject: [PATCH 051/244] BUG: Handle error return in nvJitLinkGetErrorLogSize and nvJitLinkGetErrorLog (#1993) --- cuda_core/cuda/core/_linker.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cuda_core/cuda/core/_linker.pyx b/cuda_core/cuda/core/_linker.pyx index 09aa9863cd7..c8dcf8e6150 100644 --- a/cuda_core/cuda/core/_linker.pyx +++ b/cuda_core/cuda/core/_linker.pyx @@ -106,11 +106,11 @@ cdef class Linker: cdef char* c_log_ptr if self._use_nvjitlink: c_h = as_cu(self._nvjitlink_handle) - cynvjitlink.nvJitLinkGetErrorLogSize(c_h, &c_log_size) + HANDLE_RETURN_NVJITLINK(c_h, cynvjitlink.nvJitLinkGetErrorLogSize(c_h, &c_log_size)) log = bytearray(c_log_size) if c_log_size > 0: c_log_ptr = (log) - cynvjitlink.nvJitLinkGetErrorLog(c_h, c_log_ptr) + HANDLE_RETURN_NVJITLINK(c_h, cynvjitlink.nvJitLinkGetErrorLog(c_h, c_log_ptr)) return log.decode("utf-8", errors="backslashreplace") else: return (self._drv_log_bufs[2]).decode( @@ -132,11 +132,11 @@ cdef class Linker: cdef char* c_log_ptr if self._use_nvjitlink: c_h = as_cu(self._nvjitlink_handle) - cynvjitlink.nvJitLinkGetInfoLogSize(c_h, &c_log_size) + HANDLE_RETURN_NVJITLINK(c_h, cynvjitlink.nvJitLinkGetInfoLogSize(c_h, &c_log_size)) log = bytearray(c_log_size) if c_log_size > 0: c_log_ptr = (log) - cynvjitlink.nvJitLinkGetInfoLog(c_h, c_log_ptr) + HANDLE_RETURN_NVJITLINK(c_h, cynvjitlink.nvJitLinkGetInfoLog(c_h, c_log_ptr)) return log.decode("utf-8", errors="backslashreplace") else: return (self._drv_log_bufs[0]).decode( From 8b55a6cef6544fbe93b00d4d9bc8a1f3a2aaaa0f Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Thu, 30 Apr 2026 12:37:21 -0700 Subject: [PATCH 052/244] cuda.core: complete the naming audit from #1945 (#1986) * cuda.core: address naming-audit items from #1945 Apply the in-scope renames from the issue checklist. Each change is described in the 1.0.0 release notes' Breaking changes section. Property conversions (no-arg deterministic getters): Buffer.get_ipc_descriptor() -> Buffer.ipc_descriptor Event.get_ipc_descriptor() -> Event.ipc_descriptor DeviceMemoryResource.get_allocation_handle() -> .allocation_handle PinnedMemoryResource.get_allocation_handle() -> .allocation_handle Boolean / non-noun properties (renamed for clarity, polarity fixed): LaunchConfig.cooperative_launch -> .is_cooperative Event.is_timing_disabled -> .is_timing_enabled Event.is_sync_busy_waited -> .uses_blocking_sync EventOptions.busy_waited_sync -> .use_blocking_sync The Event boolean rename also corrects a long-standing inversion in the underlying handle module: the C++ field/accessor pair stored "BLOCKING_SYNC is set" but was named "busy_waited", which is the opposite of what the flag actually does. The new name (and the public property) match CUDA's terminology directly. Graph allocation method consistency (matches MemoryResource): GraphDefinition.alloc() -> .allocate() GraphDefinition.free() -> .deallocate() GraphNode.alloc() -> .allocate() GraphNode.free() -> .deallocate() Cross-API consistency for graph builders: GraphBuilder.add_child() -> .embed() GraphDefinition.record_event() / .wait_event() -> .record() / .wait() GraphNode.record_event() / .wait_event() -> .record() / .wait() Also updates the underlying handle layer to match the renamed public properties: C++ EventBox.timing_disabled -> .timing_enabled (polarity flipped) C++ EventBox.busy_waited -> .uses_blocking_sync C++ get_event_timing_disabled -> get_event_timing_enabled C++ get_event_busy_waited -> get_event_uses_blocking_sync Cython mirrors in _resource_handles.{pxd,pyx} IPCEventDescriptor._busy_waited -> ._uses_blocking_sync Historical references to renamed/removed names in 0.3.0 and 0.4.0 release notes are converted to inline literals so latest-only Sphinx builds do not break. Out of scope (deferred): - KernelAttributes redesign (separate design PR) - Conditional API rework (create_condition unification, if_then, GraphCondition.__int__) -- separate work in progress on this branch - DeviceProperties.cooperative_launch (pending team discussion) - All system/* and fan items (Mike) Made-with: Cursor * cuda.core: unify conditional graph API (#1945) GraphBuilder, GraphDefinition, and GraphNode now share one conditional vocabulary. The factory returns a GraphCondition (no raw handle on the public surface), the conditional-node verbs are consistent, and a GraphCondition can be passed straight to launch(). Public API changes: GraphBuilder.create_conditional_handle() -> .create_condition() Returns GraphCondition (matches GraphDefinition.create_condition). The four conditional builder methods (if_then, if_else, while_loop, switch) on GraphBuilder now accept GraphCondition instead of a raw CUgraphConditionalHandle, and they validate the argument type. GraphBuilder.if_cond() -> .if_then() GraphDefinition.if_cond() -> .if_then() GraphNode.if_cond() -> .if_then() The new name parallels if_else / while_loop / switch (verb describing the construct) and matches Python's if/then/else. GraphCondition.__int__ (new) Returns the underlying CUgraphConditionalHandle value, so a GraphCondition can be passed directly as a kernel argument. _kernel_arg_handler grew an explicit GraphCondition fast path that packs it as CUgraphConditionalHandle, mirroring the existing driver.CUgraphConditionalHandle case. Internals: GraphCondition._from_handle (static cdef factory) keeps the GraphBuilder and GraphDefinition implementations of create_condition on the same construction path. Tests updated to use the new flow throughout, including passing GraphCondition directly to set_handle / countdown kernels rather than extracting .handle. Made-with: Cursor * cuda.core: KernelAttributes are properties; per-device via indexing (#1945) Replace the 16 (device_id) accessor methods on KernelAttributes with properties, and expose per-device queries via __getitem__: Old: kernel.attributes.num_regs() kernel.attributes.num_regs(dev) New: kernel.attributes.num_regs # current device kernel.attributes[dev].num_regs # specific device The default view returned by Kernel.attributes is bound to the current device (resolved at attribute-access time, so it follows context changes). Indexing returns a sibling view bound to the given Device or device ordinal; the underlying cache (keyed by (device_id, attr_enum)) is shared across views of the same kernel, so a value queried through one view is visible through the others. Internals: _device_id == -1 sentinel marks the "current device" view. _effective_device_id() resolves it via Device().device_id per access. _view_for_device() builds a sibling view that aliases _cache. _resolve_device_id() is removed (no longer needed). The 6 in-tree call sites (5 in test_module.py, 3 in test_graph_definition_lifetime.py) just drop the (); two new tests exercise the per-device view shape and reject invalid device arguments. Made-with: Cursor * fix: update test_read_only_kernel_attributes for property API (#1945) The parametrized test in test_module.py exercised the previous KernelAttributes(method, optional device_id) shape via getattr() + call. Rewrite it to use property access on the default view and on the per-device views returned by kernel.attributes[device], for both Device-object and ordinal-int forms. Type-check the value at every access path rather than just the last one. Made-with: Cursor * cuda.core: remove GraphCondition.__int__ (#1945) Address review feedback on #1986: drop the implicit int() conversion on GraphCondition. Passing a GraphCondition directly to launch() is still supported; the kernel-arg handler now reaches into _c_handle directly instead of going through __int__. Made-with: Cursor * cuda.core: rename EventOptions to bare-adjective form (#1945) Address review feedback on #1986: align EventOptions field names with the convention used elsewhere in the API (option name = bare adjective, property name = is_/uses_ prefix). - EventOptions.enable_timing -> EventOptions.timing_enabled (matches Event.is_timing_enabled) - EventOptions.use_blocking_sync -> EventOptions.blocking_sync (matches Event.uses_blocking_sync) Same pattern as ipc_enabled / Event.is_ipc_enabled and StreamOptions.nonblocking / Stream.is_nonblocking. Made-with: Cursor * cuda.core: rename Event.uses_blocking_sync to is_blocking_sync (#1945) Adopt the is_-prefix convention used everywhere else (is_ipc_enabled, is_timing_enabled, is_nonblocking) and eliminate the lone uses_- form. Keep the _sync suffix so the name disambiguates from Stream's separate "blocking" concept and stays close to the underlying CU_EVENT_BLOCKING_SYNC driver flag. EventOptions.blocking_sync is unchanged. Made-with: Cursor * cuda.core: fix int(condition) call sites missed when removing GraphCondition.__int__ (#1945) The conditional builder methods (if_then, if_else, switch, while_loop) were still calling int(condition) on the GraphCondition object after __int__ was removed in the prior commit. Use the public .handle property instead, which mirrors the kernel argument handler fix. This was caught by CI: 26 conditional graph tests failed with TypeError in cuda/core/graph/_graph_builder.pyx:549. Made-with: Cursor --- cuda_core/cuda/core/_cpp/resource_handles.cpp | 24 +-- cuda_core/cuda/core/_cpp/resource_handles.hpp | 10 +- cuda_core/cuda/core/_event.pyx | 99 ++++++------ cuda_core/cuda/core/_kernel_arg_handler.pyx | 11 ++ cuda_core/cuda/core/_launch_config.pxd | 2 +- cuda_core/cuda/core/_launch_config.pyx | 18 +-- cuda_core/cuda/core/_launcher.pyx | 2 +- cuda_core/cuda/core/_memory/_buffer.pyx | 7 +- .../core/_memory/_device_memory_resource.pyx | 33 ++-- .../core/_memory/_pinned_memory_resource.pyx | 11 +- cuda_core/cuda/core/_module.pxd | 8 +- cuda_core/cuda/core/_module.pyx | 122 ++++++++++----- cuda_core/cuda/core/_resource_handles.pxd | 8 +- cuda_core/cuda/core/_resource_handles.pyx | 8 +- cuda_core/cuda/core/graph/_graph_builder.pyx | 101 +++++++----- .../cuda/core/graph/_graph_definition.pxd | 3 + .../cuda/core/graph/_graph_definition.pyx | 53 ++++--- cuda_core/cuda/core/graph/_graph_node.pyx | 10 +- cuda_core/cuda/core/graph/_subclasses.pyx | 12 +- cuda_core/docs/source/release/0.3.0-notes.rst | 2 +- cuda_core/docs/source/release/0.4.0-notes.rst | 2 +- cuda_core/docs/source/release/1.0.0-notes.rst | 82 ++++++++++ cuda_core/tests/graph/test_graph_builder.py | 2 +- .../graph/test_graph_builder_conditional.py | 24 +-- .../tests/graph/test_graph_definition.py | 148 +++++++++--------- .../graph/test_graph_definition_errors.py | 22 +-- .../test_graph_definition_integration.py | 48 +++--- .../graph/test_graph_definition_lifetime.py | 58 +++---- cuda_core/tests/graph/test_graph_update.py | 4 +- cuda_core/tests/graph/test_options.py | 6 +- cuda_core/tests/memory_ipc/test_errors.py | 2 +- cuda_core/tests/memory_ipc/test_event_ipc.py | 22 +-- .../memory_ipc/test_ipc_duplicate_import.py | 4 +- cuda_core/tests/memory_ipc/test_leaks.py | 8 +- cuda_core/tests/memory_ipc/test_memory_ipc.py | 8 +- cuda_core/tests/memory_ipc/test_serialize.py | 8 +- cuda_core/tests/memory_ipc/test_workerpool.py | 2 +- cuda_core/tests/test_event.py | 34 ++-- cuda_core/tests/test_launcher.py | 6 +- cuda_core/tests/test_memory.py | 12 +- cuda_core/tests/test_module.py | 51 ++++-- .../tests/test_multiprocessing_warning.py | 2 +- cuda_core/tests/test_object_protocols.py | 50 +++--- 43 files changed, 680 insertions(+), 469 deletions(-) diff --git a/cuda_core/cuda/core/_cpp/resource_handles.cpp b/cuda_core/cuda/core/_cpp/resource_handles.cpp index a21cd8a8aa5..5eb4716b981 100644 --- a/cuda_core/cuda/core/_cpp/resource_handles.cpp +++ b/cuda_core/cuda/core/_cpp/resource_handles.cpp @@ -353,8 +353,8 @@ StreamHandle get_per_thread_stream() { namespace { struct EventBox { CUevent resource; - bool timing_disabled; - bool busy_waited; + bool timing_enabled; + bool is_blocking_sync; bool ipc_enabled; int device_id; ContextHandle h_context; @@ -368,12 +368,12 @@ static const EventBox* get_box(const EventHandle& h) { ); } -bool get_event_timing_disabled(const EventHandle& h) noexcept { - return h ? get_box(h)->timing_disabled : true; +bool get_event_timing_enabled(const EventHandle& h) noexcept { + return h ? get_box(h)->timing_enabled : false; } -bool get_event_busy_waited(const EventHandle& h) noexcept { - return h ? get_box(h)->busy_waited : false; +bool get_event_is_blocking_sync(const EventHandle& h) noexcept { + return h ? get_box(h)->is_blocking_sync : false; } bool get_event_ipc_enabled(const EventHandle& h) noexcept { @@ -392,7 +392,7 @@ ContextHandle get_event_context(const EventHandle& h) noexcept { static HandleRegistry event_registry; EventHandle create_event_handle(const ContextHandle& h_ctx, unsigned int flags, - bool timing_disabled, bool busy_waited, + bool timing_enabled, bool is_blocking_sync, bool ipc_enabled, int device_id) { GILReleaseGuard gil; CUevent event; @@ -401,7 +401,7 @@ EventHandle create_event_handle(const ContextHandle& h_ctx, unsigned int flags, } auto box = std::shared_ptr( - new EventBox{event, timing_disabled, busy_waited, ipc_enabled, device_id, h_ctx}, + new EventBox{event, timing_enabled, is_blocking_sync, ipc_enabled, device_id, h_ctx}, [h_ctx](const EventBox* b) { event_registry.unregister_handle(b->resource); GILReleaseGuard gil; @@ -415,19 +415,19 @@ EventHandle create_event_handle(const ContextHandle& h_ctx, unsigned int flags, } EventHandle create_event_handle_noctx(unsigned int flags) { - return create_event_handle(ContextHandle{}, flags, true, false, false, -1); + return create_event_handle(ContextHandle{}, flags, false, false, false, -1); } EventHandle create_event_handle_ref(CUevent event) { if (auto h = event_registry.lookup(event)) { return h; } - auto box = std::make_shared(EventBox{event, true, false, false, -1, {}}); + auto box = std::make_shared(EventBox{event, false, false, false, -1, {}}); return EventHandle(box, &box->resource); } EventHandle create_event_handle_ipc(const CUipcEventHandle& ipc_handle, - bool busy_waited) { + bool is_blocking_sync) { GILReleaseGuard gil; CUevent event; if (CUDA_SUCCESS != (err = p_cuIpcOpenEventHandle(&event, ipc_handle))) { @@ -435,7 +435,7 @@ EventHandle create_event_handle_ipc(const CUipcEventHandle& ipc_handle, } auto box = std::shared_ptr( - new EventBox{event, true, busy_waited, true, -1, {}}, + new EventBox{event, false, is_blocking_sync, true, -1, {}}, [](const EventBox* b) { event_registry.unregister_handle(b->resource); GILReleaseGuard gil; diff --git a/cuda_core/cuda/core/_cpp/resource_handles.hpp b/cuda_core/cuda/core/_cpp/resource_handles.hpp index d63fb869973..2e6ebb6271c 100644 --- a/cuda_core/cuda/core/_cpp/resource_handles.hpp +++ b/cuda_core/cuda/core/_cpp/resource_handles.hpp @@ -211,7 +211,7 @@ StreamHandle get_per_thread_stream(); // When the last reference is released, cuEventDestroy is called automatically. // Returns empty handle on error (caller must check). EventHandle create_event_handle(const ContextHandle& h_ctx, unsigned int flags, - bool timing_disabled, bool busy_waited, + bool timing_enabled, bool is_blocking_sync, bool ipc_enabled, int device_id); // Create an owning event handle without context dependency. @@ -225,17 +225,17 @@ EventHandle create_event_handle_noctx(unsigned int flags); // When the last reference is released, cuEventDestroy is called automatically. // Returns empty handle on error (caller must check). EventHandle create_event_handle_ipc(const CUipcEventHandle& ipc_handle, - bool busy_waited); + bool is_blocking_sync); // Create a non-owning event handle (references existing event). // Use for events that are managed by the CUDA graph or another owner. // The event will NOT be destroyed when the handle is released. -// Metadata defaults to unknown (timing_disabled=true, device_id=-1). +// Metadata defaults to unknown (timing_enabled=false, device_id=-1). EventHandle create_event_handle_ref(CUevent event); // Event metadata accessors (read from EventBox via pointer arithmetic) -bool get_event_timing_disabled(const EventHandle& h) noexcept; -bool get_event_busy_waited(const EventHandle& h) noexcept; +bool get_event_timing_enabled(const EventHandle& h) noexcept; +bool get_event_is_blocking_sync(const EventHandle& h) noexcept; bool get_event_ipc_enabled(const EventHandle& h) noexcept; int get_event_device_id(const EventHandle& h) noexcept; ContextHandle get_event_context(const EventHandle& h) noexcept; diff --git a/cuda_core/cuda/core/_event.pyx b/cuda_core/cuda/core/_event.pyx index 4a0491d8650..5fde724d21a 100644 --- a/cuda_core/cuda/core/_event.pyx +++ b/cuda_core/cuda/core/_event.pyx @@ -13,8 +13,8 @@ from cuda.core._resource_handles cimport ( EventHandle, create_event_handle, create_event_handle_ipc, - get_event_timing_disabled, - get_event_busy_waited, + get_event_timing_enabled, + get_event_is_blocking_sync, get_event_ipc_enabled, get_event_device_id, get_event_context, @@ -44,22 +44,22 @@ cdef class EventOptions: Attributes ---------- - enable_timing : bool, optional + timing_enabled : bool, optional Event will record timing data. (Default to False) - busy_waited_sync : bool, optional - If True, event will use blocking synchronization. When a CPU - thread calls synchronize, the call will block until the event - has actually been completed. - Otherwise, the CPU thread will busy-wait until the event has - been completed. (Default to False) + blocking_sync : bool, optional + If True, the event uses blocking synchronization: a CPU + thread that calls :meth:`Event.sync` blocks (yields) until + the event has completed. Otherwise (the default), the CPU + thread busy-waits until the event has completed. + (Default to False) ipc_enabled : bool, optional Event will be suitable for interprocess use. - Note that enable_timing must be False. (Default to False) + Note that timing_enabled must be False. (Default to False) """ - enable_timing: bool | None = False - busy_waited_sync: bool | None = False + timing_enabled: bool | None = False + blocking_sync: bool | None = False ipc_enabled: bool | None = False @@ -79,8 +79,8 @@ cdef class Event: # To create events and record the timing: s = Device().create_stream() - e1 = Device().create_event({"enable_timing": True}) - e2 = Device().create_event({"enable_timing": True}) + e1 = Device().create_event({"timing_enabled": True}) + e2 = Device().create_event({"timing_enabled": True}) s.record(e1) # ... run some GPU works ... s.record(e2) @@ -100,16 +100,16 @@ cdef class Event: cdef Event self = cls.__new__(cls) cdef EventOptions opts = check_or_create_options(EventOptions, options, "Event options") cdef unsigned int flags = 0x0 - cdef bint timing_disabled = False - cdef bint busy_waited = False + cdef bint timing_enabled = True + cdef bint is_blocking_sync = False cdef bint ipc_enabled = False self._ipc_descriptor = None - if not opts.enable_timing: + if not opts.timing_enabled: flags |= cydriver.CUevent_flags.CU_EVENT_DISABLE_TIMING - timing_disabled = True - if opts.busy_waited_sync: + timing_enabled = False + if opts.blocking_sync: flags |= cydriver.CUevent_flags.CU_EVENT_BLOCKING_SYNC - busy_waited = True + is_blocking_sync = True if opts.ipc_enabled: if is_free: raise TypeError( @@ -117,23 +117,24 @@ cdef class Event: ) flags |= cydriver.CUevent_flags.CU_EVENT_INTERPROCESS ipc_enabled = True - if not timing_disabled: + if timing_enabled: raise TypeError("IPC-enabled events cannot use timing.") cdef EventHandle h_event = create_event_handle( - h_context, flags, timing_disabled, busy_waited, ipc_enabled, device_id) + h_context, flags, timing_enabled, is_blocking_sync, ipc_enabled, device_id) if not h_event: raise RuntimeError("Failed to create CUDA event") self._h_event = h_event if ipc_enabled: - self.get_ipc_descriptor() + _ = self.ipc_descriptor # eagerly populate the descriptor cache return self @staticmethod cdef Event _from_handle(EventHandle h_event): """Create an Event wrapping an existing EventHandle. - Metadata (timing, busy_waited, ipc, device_id) is read from the - EventBox via pointer arithmetic — no fields are cached on Event. + Metadata (timing, blocking_sync, ipc, device_id) is read from + the EventBox via pointer arithmetic — no fields are cached on + Event. """ cdef Event self = Event.__new__(Event) self._h_event = h_event @@ -163,10 +164,10 @@ cdef class Event: return timing else: if err == cydriver.CUresult.CUDA_ERROR_INVALID_HANDLE: - if self.is_timing_disabled or other.is_timing_disabled: + if not self.is_timing_enabled or not other.is_timing_enabled: explanation = ( "Both Events must be created with timing enabled in order to subtract them; " - "use EventOptions(enable_timing=True) when creating both events." + "use EventOptions(timing_enabled=True) when creating both events." ) else: explanation = ( @@ -196,8 +197,9 @@ cdef class Event: def __repr__(self) -> str: return f"" - def get_ipc_descriptor(self) -> IPCEventDescriptor: - """Export an event allocated for sharing between processes.""" + @property + def ipc_descriptor(self) -> IPCEventDescriptor: + """Descriptor for sharing this event with other processes.""" if self._ipc_descriptor is not None: return self._ipc_descriptor if not self.is_ipc_enabled: @@ -206,7 +208,7 @@ cdef class Event: with nogil: HANDLE_RETURN(cydriver.cuIpcGetEventHandle(&data, as_cu(self._h_event))) cdef bytes data_b = cpython.PyBytes_FromStringAndSize((data.reserved), sizeof(data.reserved)) - self._ipc_descriptor = IPCEventDescriptor._init(data_b, get_event_busy_waited(self._h_event)) + self._ipc_descriptor = IPCEventDescriptor._init(data_b, get_event_is_blocking_sync(self._h_event)) return self._ipc_descriptor @classmethod @@ -215,7 +217,7 @@ cdef class Event: cdef cydriver.CUipcEventHandle data memcpy(data.reserved, (ipc_descriptor._reserved), sizeof(data.reserved)) cdef Event self = Event.__new__(cls) - cdef EventHandle h_event = create_event_handle_ipc(data, ipc_descriptor._busy_waited) + cdef EventHandle h_event = create_event_handle_ipc(data, ipc_descriptor._is_blocking_sync) if not h_event: raise RuntimeError("Failed to open IPC event handle") self._h_event = h_event @@ -228,23 +230,24 @@ cdef class Event: return get_event_ipc_enabled(self._h_event) @property - def is_timing_disabled(self) -> bool: - """Return True if the event does not record timing data, otherwise False.""" - return get_event_timing_disabled(self._h_event) + def is_timing_enabled(self) -> bool: + """Return True if the event records timing data, otherwise False.""" + return get_event_timing_enabled(self._h_event) @property - def is_sync_busy_waited(self) -> bool: - """Return True if the event synchronization would keep the CPU busy-waiting, otherwise False.""" - return get_event_busy_waited(self._h_event) + def is_blocking_sync(self) -> bool: + """Return True if the event uses blocking synchronization (the CPU + thread blocks on :meth:`sync` instead of busy-waiting), otherwise False. + """ + return get_event_is_blocking_sync(self._h_event) def sync(self): """Synchronize until the event completes. - If the event was created with busy_waited_sync, then the - calling CPU thread will block until the event has been - completed by the device. - Otherwise the CPU thread will busy-wait until the event - has been completed. + If the event was created with ``blocking_sync=True``, the + calling CPU thread blocks (yields) until the event has been + completed by the device. Otherwise (the default) the CPU + thread busy-waits until the event has completed. """ with nogil: @@ -302,28 +305,28 @@ cdef class IPCEventDescriptor: cdef: bytes _reserved - bint _busy_waited + bint _is_blocking_sync def __init__(self, *arg, **kwargs): raise RuntimeError("IPCEventDescriptor objects cannot be instantiated directly. Please use Event APIs.") @staticmethod - def _init(reserved: bytes, busy_waited: cython.bint): + def _init(reserved: bytes, is_blocking_sync: cython.bint): cdef IPCEventDescriptor self = IPCEventDescriptor.__new__(IPCEventDescriptor) self._reserved = reserved - self._busy_waited = busy_waited + self._is_blocking_sync = is_blocking_sync return self def __eq__(self, IPCEventDescriptor rhs): - # No need to check self._busy_waited. + # No need to check self._is_blocking_sync. return self._reserved == rhs._reserved def __reduce__(self): - return IPCEventDescriptor._init, (self._reserved, self._busy_waited) + return IPCEventDescriptor._init, (self._reserved, self._is_blocking_sync) def _reduce_event(event): check_multiprocessing_start_method() - return event.from_ipc_descriptor, (event.get_ipc_descriptor(),) + return event.from_ipc_descriptor, (event.ipc_descriptor,) multiprocessing.reduction.register(Event, _reduce_event) diff --git a/cuda_core/cuda/core/_kernel_arg_handler.pyx b/cuda_core/cuda/core/_kernel_arg_handler.pyx index 35eea2de473..42ad612787c 100644 --- a/cuda_core/cuda/core/_kernel_arg_handler.pyx +++ b/cuda_core/cuda/core/_kernel_arg_handler.pyx @@ -18,6 +18,7 @@ import numpy from cuda.core._memory import Buffer from cuda.core._tensor_map import TensorMapDescriptor as _TensorMapDescriptor_py from cuda.core._tensor_map cimport TensorMapDescriptor +from cuda.core.graph._graph_definition cimport GraphCondition from cuda.core._utils.cuda_utils import driver from cuda.bindings cimport cydriver @@ -318,6 +319,11 @@ cdef class ParamHolder: if arg_type is driver.CUgraphConditionalHandle: prepare_arg[cydriver.CUgraphConditionalHandle](self.data, self.data_addresses, int(arg), i) continue + elif arg_type is GraphCondition: + prepare_arg[cydriver.CUgraphConditionalHandle]( + self.data, self.data_addresses, + (arg)._c_handle, i) + continue # If no exact types are found, fallback to slower `isinstance` check elif isinstance(arg, Buffer): if isinstance(arg.handle, int): @@ -341,6 +347,11 @@ cdef class ParamHolder: elif isinstance(arg, driver.CUgraphConditionalHandle): prepare_arg[cydriver.CUgraphConditionalHandle](self.data, self.data_addresses, arg, i) continue + elif isinstance(arg, GraphCondition): + prepare_arg[cydriver.CUgraphConditionalHandle]( + self.data, self.data_addresses, + (arg)._c_handle, i) + continue # TODO: support ctypes/numpy struct raise TypeError("the argument is of unsupported type: " + str(type(arg))) diff --git a/cuda_core/cuda/core/_launch_config.pxd b/cuda_core/cuda/core/_launch_config.pxd index 909c236309a..112007b9cfd 100644 --- a/cuda_core/cuda/core/_launch_config.pxd +++ b/cuda_core/cuda/core/_launch_config.pxd @@ -14,7 +14,7 @@ cdef class LaunchConfig: public tuple cluster public tuple block public int shmem_size - public bint cooperative_launch + public bint is_cooperative vector[cydriver.CUlaunchAttribute] _attrs object __weakref__ diff --git a/cuda_core/cuda/core/_launch_config.pyx b/cuda_core/cuda/core/_launch_config.pyx index 0970ea36c79..b1a9a96cb29 100644 --- a/cuda_core/cuda/core/_launch_config.pyx +++ b/cuda_core/cuda/core/_launch_config.pyx @@ -11,7 +11,7 @@ from cuda.core._utils.cuda_utils import ( driver, ) -_LAUNCH_CONFIG_ATTRS = ('grid', 'cluster', 'block', 'shmem_size', 'cooperative_launch') +_LAUNCH_CONFIG_ATTRS = ('grid', 'cluster', 'block', 'shmem_size', 'is_cooperative') cdef class LaunchConfig: @@ -42,7 +42,7 @@ cdef class LaunchConfig: shmem_size : int, optional Dynamic shared-memory size per thread block in bytes. (Default to size 0) - cooperative_launch : bool, optional + is_cooperative : bool, optional Whether this config can be used to launch a cooperative kernel. """ @@ -50,7 +50,7 @@ cdef class LaunchConfig: # Note: attributes are declared in _launch_config.pxd def __init__(self, grid=None, cluster=None, block=None, - shmem_size=None, cooperative_launch=False): + shmem_size=None, is_cooperative=False): """Initialize LaunchConfig with validation. Parameters @@ -63,7 +63,7 @@ cdef class LaunchConfig: Block dimensions (threads per block) shmem_size : int, optional Dynamic shared memory size in bytes (default: 0) - cooperative_launch : bool, optional + is_cooperative : bool, optional Whether to launch as cooperative kernel (default: False) """ # Convert and validate grid and block dimensions @@ -90,11 +90,9 @@ cdef class LaunchConfig: else: self.shmem_size = shmem_size - # Handle cooperative_launch - self.cooperative_launch = cooperative_launch + self.is_cooperative = is_cooperative - # Validate cooperative launch support - if self.cooperative_launch and not Device().properties.cooperative_launch: + if self.is_cooperative and not Device().properties.cooperative_launch: raise CUDAError("cooperative kernels are not supported on this device") def _identity(self): @@ -136,7 +134,7 @@ cdef class LaunchConfig: drv_cfg.blockDimX, drv_cfg.blockDimY, drv_cfg.blockDimZ = self.block drv_cfg.sharedMemBytes = self.shmem_size - if self.cooperative_launch: + if self.is_cooperative: attr.id = cydriver.CUlaunchAttributeID.CU_LAUNCH_ATTRIBUTE_COOPERATIVE attr.value.cooperative = 1 self._attrs.push_back(attr) @@ -190,7 +188,7 @@ cpdef object _to_native_launch_config(LaunchConfig config): drv_cfg.blockDimX, drv_cfg.blockDimY, drv_cfg.blockDimZ = config.block drv_cfg.sharedMemBytes = config.shmem_size - if config.cooperative_launch: + if config.is_cooperative: attr = driver.CUlaunchAttribute() attr.id = driver.CUlaunchAttributeID.CU_LAUNCH_ATTRIBUTE_COOPERATIVE attr.value.cooperative = 1 diff --git a/cuda_core/cuda/core/_launcher.pyx b/cuda_core/cuda/core/_launcher.pyx index 130b2278418..87d18f2b881 100644 --- a/cuda_core/cuda/core/_launcher.pyx +++ b/cuda_core/cuda/core/_launcher.pyx @@ -52,7 +52,7 @@ def launch(stream: Stream | GraphBuilder | IsStreamT, config: LaunchConfig, kern drv_cfg = conf._to_native_launch_config() drv_cfg.hStream = as_cu(s._h_stream) - if conf.cooperative_launch: + if conf.is_cooperative: _check_cooperative_launch(kernel, conf, s) with nogil: HANDLE_RETURN(cydriver.cuLaunchKernelEx(&drv_cfg, func_handle, args_ptr, NULL)) diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index bb6fd97df6f..7de3c475d5d 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -128,7 +128,7 @@ cdef class Buffer: def __reduce__(self): # Must not serialize the parent's stream! - return Buffer._reduce_helper, (self.memory_resource, self.get_ipc_descriptor()) + return Buffer._reduce_helper, (self.memory_resource, self.ipc_descriptor) @staticmethod def from_handle( @@ -168,8 +168,9 @@ cdef class Buffer: """Import a buffer that was exported from another process.""" return _ipc.Buffer_from_ipc_descriptor(cls, mr, ipc_descriptor, stream) - def get_ipc_descriptor(self) -> IPCBufferDescriptor: - """Export a buffer allocated for sharing between processes.""" + @property + def ipc_descriptor(self) -> IPCBufferDescriptor: + """Descriptor for sharing this buffer with other processes.""" if self._ipc_data is None: self._ipc_data = IPCDataForBuffer(_ipc.Buffer_get_ipc_descriptor(self), False) return self._ipc_data.ipc_descriptor diff --git a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx index 9f8e4bcd534..0f1a7f52e21 100644 --- a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx @@ -89,19 +89,19 @@ cdef class DeviceMemoryResource(_MemPool): :class:`DeviceMemoryResource` and can be distinguished via :attr:`DeviceMemoryResource.is_mapped`. - An MR is shared via an allocation handle obtained by calling - :meth:`DeviceMemoryResource.get_allocation_handle`. The allocation handle - has a platform-specific interpretation; however, memory IPC is currently - only supported for Linux, and in that case allocation handles are file - descriptors. After sending an allocation handle to another process, it can - be used to create an MMR by invoking + An MR is shared via an allocation handle accessed through the + :attr:`DeviceMemoryResource.allocation_handle` property. The allocation + handle has a platform-specific interpretation; however, memory IPC is + currently only supported for Linux, and in that case allocation handles + are file descriptors. After sending an allocation handle to another + process, it can be used to create an MMR by invoking :meth:`DeviceMemoryResource.from_allocation_handle`. - Buffers can be shared as serializable descriptors obtained by calling - :meth:`Buffer.get_ipc_descriptor`. In a receiving process, a shared buffer is - created by invoking :meth:`Buffer.from_ipc_descriptor` with an MMR and - buffer descriptor, where the MMR corresponds to the MR that created the - described buffer. + Buffers can be shared as serializable descriptors accessed through the + :attr:`Buffer.ipc_descriptor` property. In a receiving process, a shared + buffer is created by invoking :meth:`Buffer.from_ipc_descriptor` with an + MMR and buffer descriptor, where the MMR corresponds to the MR that + created the described buffer. To help manage the association between memory resources and buffers, a registry is provided. Every MR has a unique identifier (UUID). MMRs can be @@ -194,15 +194,12 @@ cdef class DeviceMemoryResource(_MemPool): mr._peer_accessible_by = () return mr - def get_allocation_handle(self) -> IPCAllocationHandle: - """Export the memory pool handle to be shared (requires IPC). + @property + def allocation_handle(self) -> IPCAllocationHandle: + """Shareable handle for this memory pool (requires IPC). The handle can be used to share the memory pool with other processes. The handle is cached in this `MemoryResource` and owned by it. - - Returns - ------- - The shareable handle for the memory pool. """ if not self.is_ipc_enabled: raise RuntimeError("Memory resource is not IPC-enabled") @@ -404,7 +401,7 @@ def _deep_reduce_device_memory_resource(mr): check_multiprocessing_start_method() from .._device import Device device = Device(mr.device_id) - alloc_handle = mr.get_allocation_handle() + alloc_handle = mr.allocation_handle return mr.from_allocation_handle, (device, alloc_handle) diff --git a/cuda_core/cuda/core/_memory/_pinned_memory_resource.pyx b/cuda_core/cuda/core/_memory/_pinned_memory_resource.pyx index 64ebcc7bc5d..0b18a1f7e3d 100644 --- a/cuda_core/cuda/core/_memory/_pinned_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_pinned_memory_resource.pyx @@ -148,15 +148,12 @@ cdef class PinnedMemoryResource(_MemPool): _ipc.MP_from_allocation_handle(cls, alloc_handle)) return mr - def get_allocation_handle(self) -> IPCAllocationHandle: - """Export the memory pool handle to be shared (requires IPC). + @property + def allocation_handle(self) -> IPCAllocationHandle: + """Shareable handle for this memory pool (requires IPC). The handle can be used to share the memory pool with other processes. The handle is cached in this `MemoryResource` and owned by it. - - Returns - ------- - The shareable handle for the memory pool. """ if not self.is_ipc_enabled: raise RuntimeError("Memory resource is not IPC-enabled") @@ -242,7 +239,7 @@ cdef inline _PMR_init(PinnedMemoryResource self, options): def _deep_reduce_pinned_memory_resource(mr): check_multiprocessing_start_method() - alloc_handle = mr.get_allocation_handle() + alloc_handle = mr.allocation_handle return mr.from_allocation_handle, (alloc_handle,) diff --git a/cuda_core/cuda/core/_module.pxd b/cuda_core/cuda/core/_module.pxd index 1d3a0772c30..78f871b5ba2 100644 --- a/cuda_core/cuda/core/_module.pxd +++ b/cuda_core/cuda/core/_module.pxd @@ -48,10 +48,16 @@ cdef class KernelOccupancy: cdef class KernelAttributes: cdef: KernelHandle _h_kernel + # _device_id == -1 means "current device" (resolved per access). + # _device_id >= 0 means this view is bound to that specific device. + int _device_id + # Cache is shared across views for the same Kernel: the per-device + # view returned by __getitem__ inherits the parent's dict. dict _cache @staticmethod cdef KernelAttributes _init(KernelHandle h_kernel) + cdef KernelAttributes _view_for_device(self, int device_id) + cdef inline int _effective_device_id(self) except? -1 cdef int _get_cached_attribute(self, int device_id, cydriver.CUfunction_attribute attribute) except? -1 - cdef int _resolve_device_id(self, device_id) except? -1 diff --git a/cuda_core/cuda/core/_module.pyx b/cuda_core/cuda/core/_module.pyx index 2eaff7fb11b..aa865382345 100644 --- a/cuda_core/cuda/core/_module.pyx +++ b/cuda_core/cuda/core/_module.pyx @@ -39,7 +39,15 @@ __all__ = ["Kernel", "ObjectCode"] cdef class KernelAttributes: - """Provides access to kernel attributes.""" + """Read-only view of a kernel's per-device attributes. + + The default view returned by :attr:`Kernel.attributes` is bound to + the current device, resolved at attribute-access time. Use + ``kernel.attributes[device]`` to obtain a view bound to a specific + device (an :class:`int` device ordinal or :class:`Device`). Per-device + views share the underlying cache so a value queried through one view + is visible through the others. + """ def __init__(self, *args, **kwargs): raise RuntimeError("KernelAttributes cannot be instantiated directly. Please use Kernel APIs.") @@ -48,9 +56,22 @@ cdef class KernelAttributes: cdef KernelAttributes _init(KernelHandle h_kernel): cdef KernelAttributes self = KernelAttributes.__new__(KernelAttributes) self._h_kernel = h_kernel + self._device_id = -1 self._cache = {} return self + cdef KernelAttributes _view_for_device(self, int device_id): + cdef KernelAttributes view = KernelAttributes.__new__(KernelAttributes) + view._h_kernel = self._h_kernel + view._device_id = device_id + view._cache = self._cache + return view + + cdef inline int _effective_device_id(self) except? -1: + if self._device_id >= 0: + return self._device_id + return Device().device_id + cdef int _get_cached_attribute(self, int device_id, cydriver.CUfunction_attribute attribute) except? -1: """Helper function to get a cached attribute or fetch and cache it if not present.""" cdef tuple cache_key = (device_id, attribute) @@ -63,121 +84,150 @@ cdef class KernelAttributes: self._cache[cache_key] = result return result - cdef inline int _resolve_device_id(self, device_id) except? -1: - """Convert Device or int to device_id int.""" - return Device(device_id).device_id + def __getitem__(self, device) -> KernelAttributes: + """Return a view of these attributes bound to a specific device. - def max_threads_per_block(self, device_id: Device | int = None) -> int: + Parameters + ---------- + device : Device or int + The device whose attributes to query. Accepts a :class:`Device` + or a device ordinal (:class:`int`). + + Returns + ------- + KernelAttributes + A view bound to ``device`` that shares the underlying cache + with this view. + """ + return self._view_for_device(Device(device).device_id) + + @property + def max_threads_per_block(self) -> int: """int : The maximum number of threads per block. This attribute is read-only.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK ) - def shared_size_bytes(self, device_id: Device | int = None) -> int: + @property + def shared_size_bytes(self) -> int: """int : The size in bytes of statically-allocated shared memory required by this function. This attribute is read-only.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES ) - def const_size_bytes(self, device_id: Device | int = None) -> int: + @property + def const_size_bytes(self) -> int: """int : The size in bytes of user-allocated constant memory required by this function. This attribute is read-only.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_CONST_SIZE_BYTES + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_CONST_SIZE_BYTES ) - def local_size_bytes(self, device_id: Device | int = None) -> int: + @property + def local_size_bytes(self) -> int: """int : The size in bytes of local memory used by each thread of this function. This attribute is read-only.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES ) - def num_regs(self, device_id: Device | int = None) -> int: + @property + def num_regs(self) -> int: """int : The number of registers used by each thread of this function. This attribute is read-only.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_NUM_REGS + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_NUM_REGS ) - def ptx_version(self, device_id: Device | int = None) -> int: + @property + def ptx_version(self) -> int: """int : The PTX virtual architecture version for which the function was compiled. This attribute is read-only.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_PTX_VERSION + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_PTX_VERSION ) - def binary_version(self, device_id: Device | int = None) -> int: + @property + def binary_version(self) -> int: """int : The binary architecture version for which the function was compiled. This attribute is read-only.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_BINARY_VERSION + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_BINARY_VERSION ) - def cache_mode_ca(self, device_id: Device | int = None) -> bool: + @property + def cache_mode_ca(self) -> bool: """bool : Whether the function has been compiled with user specified option "-Xptxas --dlcm=ca" set. This attribute is read-only.""" return bool( self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_CACHE_MODE_CA + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_CACHE_MODE_CA ) ) - def max_dynamic_shared_size_bytes(self, device_id: Device | int = None) -> int: + @property + def max_dynamic_shared_size_bytes(self) -> int: """int : The maximum size in bytes of dynamically-allocated shared memory that can be used by this function.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES ) - def preferred_shared_memory_carveout(self, device_id: Device | int = None) -> int: + @property + def preferred_shared_memory_carveout(self) -> int: """int : The shared memory carveout preference, in percent of the total shared memory.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_PREFERRED_SHARED_MEMORY_CARVEOUT + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_PREFERRED_SHARED_MEMORY_CARVEOUT ) - def cluster_size_must_be_set(self, device_id: Device | int = None) -> bool: + @property + def cluster_size_must_be_set(self) -> bool: """bool : The kernel must launch with a valid cluster size specified. This attribute is read-only.""" return bool( self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_CLUSTER_SIZE_MUST_BE_SET + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_CLUSTER_SIZE_MUST_BE_SET ) ) - def required_cluster_width(self, device_id: Device | int = None) -> int: + @property + def required_cluster_width(self) -> int: """int : The required cluster width in blocks.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_REQUIRED_CLUSTER_WIDTH + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_REQUIRED_CLUSTER_WIDTH ) - def required_cluster_height(self, device_id: Device | int = None) -> int: + @property + def required_cluster_height(self) -> int: """int : The required cluster height in blocks.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_REQUIRED_CLUSTER_HEIGHT + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_REQUIRED_CLUSTER_HEIGHT ) - def required_cluster_depth(self, device_id: Device | int = None) -> int: + @property + def required_cluster_depth(self) -> int: """int : The required cluster depth in blocks.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), cydriver.CU_FUNC_ATTRIBUTE_REQUIRED_CLUSTER_DEPTH + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_REQUIRED_CLUSTER_DEPTH ) - def non_portable_cluster_size_allowed(self, device_id: Device | int = None) -> bool: + @property + def non_portable_cluster_size_allowed(self) -> bool: """bool : Whether the function can be launched with non-portable cluster size.""" return bool( self._get_cached_attribute( - self._resolve_device_id(device_id), + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_NON_PORTABLE_CLUSTER_SIZE_ALLOWED, ) ) - def cluster_scheduling_policy_preference(self, device_id: Device | int = None) -> int: + @property + def cluster_scheduling_policy_preference(self) -> int: """int : The block scheduling policy of a function.""" return self._get_cached_attribute( - self._resolve_device_id(device_id), + self._effective_device_id(), cydriver.CU_FUNC_ATTRIBUTE_CLUSTER_SCHEDULING_POLICY_PREFERENCE, ) diff --git a/cuda_core/cuda/core/_resource_handles.pxd b/cuda_core/cuda/core/_resource_handles.pxd index 9e7307e821b..0d7d20e574c 100644 --- a/cuda_core/cuda/core/_resource_handles.pxd +++ b/cuda_core/cuda/core/_resource_handles.pxd @@ -121,16 +121,16 @@ cdef StreamHandle get_per_thread_stream() except+ nogil # Event handles cdef EventHandle create_event_handle( const ContextHandle& h_ctx, unsigned int flags, - bint timing_disabled, bint busy_waited, + bint timing_enabled, bint is_blocking_sync, bint ipc_enabled, int device_id) except+ nogil cdef EventHandle create_event_handle_noctx(unsigned int flags) except+ nogil cdef EventHandle create_event_handle_ref(cydriver.CUevent event) except+ nogil cdef EventHandle create_event_handle_ipc( - const cydriver.CUipcEventHandle& ipc_handle, bint busy_waited) except+ nogil + const cydriver.CUipcEventHandle& ipc_handle, bint is_blocking_sync) except+ nogil # Event metadata getters -cdef bint get_event_timing_disabled(const EventHandle& h) noexcept nogil -cdef bint get_event_busy_waited(const EventHandle& h) noexcept nogil +cdef bint get_event_timing_enabled(const EventHandle& h) noexcept nogil +cdef bint get_event_is_blocking_sync(const EventHandle& h) noexcept nogil cdef bint get_event_ipc_enabled(const EventHandle& h) noexcept nogil cdef int get_event_device_id(const EventHandle& h) noexcept nogil cdef ContextHandle get_event_context(const EventHandle& h) noexcept nogil diff --git a/cuda_core/cuda/core/_resource_handles.pyx b/cuda_core/cuda/core/_resource_handles.pyx index 2090f5026d0..d30993cc5e8 100644 --- a/cuda_core/cuda/core/_resource_handles.pyx +++ b/cuda_core/cuda/core/_resource_handles.pyx @@ -72,19 +72,19 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": # Event handles (note: _create_event_handle* are internal due to C++ overloading) EventHandle create_event_handle "cuda_core::create_event_handle" ( const ContextHandle& h_ctx, unsigned int flags, - bint timing_disabled, bint busy_waited, + bint timing_enabled, bint is_blocking_sync, bint ipc_enabled, int device_id) except+ nogil EventHandle create_event_handle_noctx "cuda_core::create_event_handle_noctx" ( unsigned int flags) except+ nogil EventHandle create_event_handle_ref "cuda_core::create_event_handle_ref" ( cydriver.CUevent event) except+ nogil EventHandle create_event_handle_ipc "cuda_core::create_event_handle_ipc" ( - const cydriver.CUipcEventHandle& ipc_handle, bint busy_waited) except+ nogil + const cydriver.CUipcEventHandle& ipc_handle, bint is_blocking_sync) except+ nogil # Event metadata getters - bint get_event_timing_disabled "cuda_core::get_event_timing_disabled" ( + bint get_event_timing_enabled "cuda_core::get_event_timing_enabled" ( const EventHandle& h) noexcept nogil - bint get_event_busy_waited "cuda_core::get_event_busy_waited" ( + bint get_event_is_blocking_sync "cuda_core::get_event_is_blocking_sync" ( const EventHandle& h) noexcept nogil bint get_event_ipc_enabled "cuda_core::get_event_ipc_enabled" ( const EventHandle& h) noexcept nogil diff --git a/cuda_core/cuda/core/graph/_graph_builder.pyx b/cuda_core/cuda/core/graph/_graph_builder.pyx index 5b304a24d38..526c95e04ad 100644 --- a/cuda_core/cuda/core/graph/_graph_builder.pyx +++ b/cuda_core/cuda/core/graph/_graph_builder.pyx @@ -9,6 +9,7 @@ from libc.stdint cimport intptr_t from cuda.bindings cimport cydriver +from cuda.core.graph._graph_definition cimport GraphCondition from cuda.core.graph._utils cimport _attach_host_callback_to_graph from cuda.core._resource_handles cimport as_cu from cuda.core._stream cimport Stream @@ -441,19 +442,24 @@ class GraphBuilder: def _get_conditional_context(self) -> driver.CUcontext: return self._mnff.stream.context.handle - def create_conditional_handle(self, default_value=None) -> driver.CUgraphConditionalHandle: - """Creates a conditional handle for the graph builder. + def create_condition(self, default_value=None) -> GraphCondition: + """Create a condition variable for use with conditional nodes. + + The returned :class:`GraphCondition` object is passed to conditional-node + builder methods (:meth:`if_then`, :meth:`if_else`, :meth:`while_loop`, + :meth:`switch`). Its value is controlled at runtime by device code via + ``cudaGraphSetConditional``. Parameters ---------- default_value : int, optional - The default value to assign to the conditional handle. + The default value to assign to the condition. If None, no + default is assigned. Returns ------- - handle : driver.CUgraphConditionalHandle - The newly created conditional handle. - + GraphCondition + A condition variable for controlling conditional execution. """ if cy_driver_version() < (12, 3, 0): raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional handles") @@ -467,11 +473,12 @@ class GraphBuilder: status, _, graph, *_, _ = handle_return(driver.cuStreamGetCaptureInfo(self._mnff.stream.handle)) if status != driver.CUstreamCaptureStatus.CU_STREAM_CAPTURE_STATUS_ACTIVE: - raise RuntimeError("Cannot create a conditional handle when graph is not being built") + raise RuntimeError("Cannot create a condition when graph is not being built") - return handle_return( + raw_handle = handle_return( driver.cuGraphConditionalHandleCreate(graph, self._get_conditional_context(), default_value, flags) ) + return GraphCondition._from_handle(int(raw_handle)) def _cond_with_params(self, node_params) -> tuple: # Get current capture info to ensure we're in a valid state @@ -509,18 +516,19 @@ class GraphBuilder: ] ) - def if_cond(self, handle: driver.CUgraphConditionalHandle) -> GraphBuilder: + def if_then(self, condition: GraphCondition) -> GraphBuilder: """Adds an if condition branch and returns a new graph builder for it. - The resulting if graph will only execute the branch if the conditional - handle evaluates to true at runtime. + The resulting if graph will only execute the branch if the + condition evaluates to true at runtime. The new builder inherits work dependencies from the original builder. Parameters ---------- - handle : driver.CUgraphConditionalHandle - The handle to use for the if conditional. + condition : :class:`~graph.GraphCondition` + The condition variable from :meth:`create_condition` controlling + whether the branch executes. Returns ------- @@ -532,26 +540,31 @@ class GraphBuilder: raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional if") if cy_binding_version() < (12, 3, 0): raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional if") + if not isinstance(condition, GraphCondition): + raise TypeError( + f"condition must be a GraphCondition object (from " + f"GraphBuilder.create_condition()), got {type(condition).__name__}") node_params = driver.CUgraphNodeParams() node_params.type = driver.CUgraphNodeType.CU_GRAPH_NODE_TYPE_CONDITIONAL - node_params.conditional.handle = handle + node_params.conditional.handle = condition.handle node_params.conditional.type = driver.CUgraphConditionalNodeType.CU_GRAPH_COND_TYPE_IF node_params.conditional.size = 1 node_params.conditional.ctx = self._get_conditional_context() return self._cond_with_params(node_params)[0] - def if_else(self, handle: driver.CUgraphConditionalHandle) -> tuple[GraphBuilder, GraphBuilder]: + def if_else(self, condition: GraphCondition) -> tuple[GraphBuilder, GraphBuilder]: """Adds an if-else condition branch and returns new graph builders for both branches. - The resulting if graph will execute the branch if the conditional handle + The resulting if graph will execute the branch if the condition evaluates to true at runtime, otherwise the else branch will execute. The new builders inherit work dependencies from the original builder. Parameters ---------- - handle : driver.CUgraphConditionalHandle - The handle to use for the if-else conditional. + condition : :class:`~graph.GraphCondition` + The condition variable from :meth:`create_condition` controlling + which branch executes. Returns ------- @@ -563,27 +576,32 @@ class GraphBuilder: raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional if-else") if cy_binding_version() < (12, 8, 0): raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional if-else") + if not isinstance(condition, GraphCondition): + raise TypeError( + f"condition must be a GraphCondition object (from " + f"GraphBuilder.create_condition()), got {type(condition).__name__}") node_params = driver.CUgraphNodeParams() node_params.type = driver.CUgraphNodeType.CU_GRAPH_NODE_TYPE_CONDITIONAL - node_params.conditional.handle = handle + node_params.conditional.handle = condition.handle node_params.conditional.type = driver.CUgraphConditionalNodeType.CU_GRAPH_COND_TYPE_IF node_params.conditional.size = 2 node_params.conditional.ctx = self._get_conditional_context() return self._cond_with_params(node_params) - def switch(self, handle: driver.CUgraphConditionalHandle, count: int) -> tuple[GraphBuilder, ...]: + def switch(self, condition: GraphCondition, count: int) -> tuple[GraphBuilder, ...]: """Adds a switch condition branch and returns new graph builders for all cases. - The resulting switch graph will execute the branch that matches the - case index of the conditional handle at runtime. If no match is found, no branch - will be executed. + The resulting switch graph will execute the branch whose case index + matches the value of the condition at runtime. If no match is found, no + branch will be executed. The new builders inherit work dependencies from the original builder. Parameters ---------- - handle : driver.CUgraphConditionalHandle - The handle to use for the switch conditional. + condition : :class:`~graph.GraphCondition` + The condition variable from :meth:`create_condition` selecting + which case executes. count : int The number of cases to add to the switch conditional. @@ -597,26 +615,31 @@ class GraphBuilder: raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional switch") if cy_binding_version() < (12, 8, 0): raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional switch") + if not isinstance(condition, GraphCondition): + raise TypeError( + f"condition must be a GraphCondition object (from " + f"GraphBuilder.create_condition()), got {type(condition).__name__}") node_params = driver.CUgraphNodeParams() node_params.type = driver.CUgraphNodeType.CU_GRAPH_NODE_TYPE_CONDITIONAL - node_params.conditional.handle = handle + node_params.conditional.handle = condition.handle node_params.conditional.type = driver.CUgraphConditionalNodeType.CU_GRAPH_COND_TYPE_SWITCH node_params.conditional.size = count node_params.conditional.ctx = self._get_conditional_context() return self._cond_with_params(node_params) - def while_loop(self, handle: driver.CUgraphConditionalHandle) -> GraphBuilder: + def while_loop(self, condition: GraphCondition) -> GraphBuilder: """Adds a while loop and returns a new graph builder for it. The resulting while loop graph will execute the branch repeatedly at runtime - until the conditional handle evaluates to false. + until the condition evaluates to false. The new builder inherits work dependencies from the original builder. Parameters ---------- - handle : driver.CUgraphConditionalHandle - The handle to use for the while loop. + condition : :class:`~graph.GraphCondition` + The condition variable from :meth:`create_condition` controlling + loop continuation. Returns ------- @@ -628,9 +651,13 @@ class GraphBuilder: raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional while loop") if cy_binding_version() < (12, 3, 0): raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional while loop") + if not isinstance(condition, GraphCondition): + raise TypeError( + f"condition must be a GraphCondition object (from " + f"GraphBuilder.create_condition()), got {type(condition).__name__}") node_params = driver.CUgraphNodeParams() node_params.type = driver.CUgraphNodeType.CU_GRAPH_NODE_TYPE_CONDITIONAL - node_params.conditional.handle = handle + node_params.conditional.handle = condition.handle node_params.conditional.type = driver.CUgraphConditionalNodeType.CU_GRAPH_COND_TYPE_WHILE node_params.conditional.size = 1 node_params.conditional.ctx = self._get_conditional_context() @@ -645,17 +672,15 @@ class GraphBuilder: """ self._mnff.close() - def add_child(self, child_graph: GraphBuilder): - """Adds the child :obj:`~graph.GraphBuilder` builder into self. - - The child graph builder will be added as a child node to the parent graph builder. + def embed(self, child: GraphBuilder): + """Embed a previously-built :obj:`~graph.GraphBuilder` as a child node. Parameters ---------- - child_graph : :obj:`~graph.GraphBuilder` + child : :obj:`~graph.GraphBuilder` The child graph builder. Must have finished building. """ - if not child_graph._building_ended: + if not child._building_ended: raise ValueError("Child graph has not finished building.") if not self.is_building: @@ -673,7 +698,7 @@ class GraphBuilder: [ handle_return( driver.cuGraphAddChildGraphNode( - graph_out, *deps_info_trimmed, num_dependencies_out, child_graph._mnff.graph + graph_out, *deps_info_trimmed, num_dependencies_out, child._mnff.graph ) ) ] diff --git a/cuda_core/cuda/core/graph/_graph_definition.pxd b/cuda_core/cuda/core/graph/_graph_definition.pxd index b414568e986..6c15643c2fe 100644 --- a/cuda_core/cuda/core/graph/_graph_definition.pxd +++ b/cuda_core/cuda/core/graph/_graph_definition.pxd @@ -11,6 +11,9 @@ cdef class GraphCondition: cydriver.CUgraphConditionalHandle _c_handle object __weakref__ + @staticmethod + cdef GraphCondition _from_handle(cydriver.CUgraphConditionalHandle c_handle) + cdef class GraphDefinition: cdef: diff --git a/cuda_core/cuda/core/graph/_graph_definition.pyx b/cuda_core/cuda/core/graph/_graph_definition.pyx index 56b0af5d9ec..9a08232c556 100644 --- a/cuda_core/cuda/core/graph/_graph_definition.pyx +++ b/cuda_core/cuda/core/graph/_graph_definition.pyx @@ -33,12 +33,25 @@ __all__ = ['GraphCondition', 'GraphAllocOptions', 'GraphDefinition'] cdef class GraphCondition: """A condition variable for conditional graph nodes. - Created by :meth:`GraphDefinition.create_condition` and passed to - conditional-node builder methods (``if_cond``, ``if_else``, - ``while_loop``, ``switch``). The underlying value is set at + Created by :meth:`GraphDefinition.create_condition` (or + :meth:`GraphBuilder.create_condition`) and passed to + conditional-node builder methods (:meth:`~GraphDefinition.if_then`, + :meth:`~GraphDefinition.if_else`, :meth:`~GraphDefinition.while_loop`, + :meth:`~GraphDefinition.switch`). The underlying value is set at runtime by device code via ``cudaGraphSetConditional``. + + A :class:`GraphCondition` may be passed directly as a kernel + argument to ``launch()``: the launcher unwraps it to the underlying + ``CUgraphConditionalHandle`` value so device code can update the + condition. """ + @staticmethod + cdef GraphCondition _from_handle(cydriver.CUgraphConditionalHandle c_handle): + cdef GraphCondition self = GraphCondition.__new__(GraphCondition) + self._c_handle = c_handle + return self + def __repr__(self) -> str: return f"self._c_handle:x}>" @@ -132,19 +145,19 @@ cdef class GraphDefinition: n._h_node = create_graph_node_handle(NULL, self._h_graph) return n - def alloc(self, size_t size, options: GraphAllocOptions | None = None) -> "AllocNode": + def allocate(self, size_t size, options: GraphAllocOptions | None = None) -> "AllocNode": """Add an entry-point memory allocation node (no dependencies). - See :meth:`GraphNode.alloc` for full documentation. + See :meth:`GraphNode.allocate` for full documentation. """ - return self._entry.alloc(size, options) + return self._entry.allocate(size, options) - def free(self, dptr) -> "FreeNode": + def deallocate(self, dptr) -> "FreeNode": """Add an entry-point memory free node (no dependencies). - See :meth:`GraphNode.free` for full documentation. + See :meth:`GraphNode.deallocate` for full documentation. """ - return self._entry.free(dptr) + return self._entry.deallocate(dptr) def memset(self, dst, value, size_t width, size_t height=1, size_t pitch=0) -> "MemsetNode": """Add an entry-point memset node (no dependencies). @@ -199,19 +212,19 @@ cdef class GraphDefinition: """ return self._entry.embed(child) - def record_event(self, event) -> "EventRecordNode": + def record(self, event) -> "EventRecordNode": """Add an entry-point event record node (no dependencies). - See :meth:`GraphNode.record_event` for full documentation. + See :meth:`GraphNode.record` for full documentation. """ - return self._entry.record_event(event) + return self._entry.record(event) - def wait_event(self, event) -> "EventWaitNode": + def wait(self, event) -> "EventWaitNode": """Add an entry-point event wait node (no dependencies). - See :meth:`GraphNode.wait_event` for full documentation. + See :meth:`GraphNode.wait` for full documentation. """ - return self._entry.wait_event(event) + return self._entry.wait(event) def callback(self, fn, *, user_data=None) -> "HostCallbackNode": """Add an entry-point host callback node (no dependencies). @@ -252,16 +265,14 @@ cdef class GraphDefinition: HANDLE_RETURN(cydriver.cuGraphConditionalHandleCreate( &c_handle, as_cu(self._h_graph), ctx, default_val, flags)) - cdef GraphCondition cond = GraphCondition.__new__(GraphCondition) - cond._c_handle = c_handle - return cond + return GraphCondition._from_handle(c_handle) - def if_cond(self, condition: GraphCondition) -> "IfNode": + def if_then(self, condition: GraphCondition) -> "IfNode": """Add an entry-point if-conditional node (no dependencies). - See :meth:`GraphNode.if_cond` for full documentation. + See :meth:`GraphNode.if_then` for full documentation. """ - return self._entry.if_cond(condition) + return self._entry.if_then(condition) def if_else(self, condition: GraphCondition) -> "IfElseNode": """Add an entry-point if-else conditional node (no dependencies). diff --git a/cuda_core/cuda/core/graph/_graph_node.pyx b/cuda_core/cuda/core/graph/_graph_node.pyx index bd10bfa007f..36401776600 100644 --- a/cuda_core/cuda/core/graph/_graph_node.pyx +++ b/cuda_core/cuda/core/graph/_graph_node.pyx @@ -215,7 +215,7 @@ cdef class GraphNode: """ return GN_join(self, nodes) - def alloc(self, size_t size, options=None) -> AllocNode: + def allocate(self, size_t size, options=None) -> AllocNode: """Add a memory allocation node depending on this node. Parameters @@ -233,7 +233,7 @@ cdef class GraphNode: """ return GN_alloc(self, size, options) - def free(self, dptr: int) -> FreeNode: + def deallocate(self, dptr: int) -> FreeNode: """Add a memory free node depending on this node. Parameters @@ -317,7 +317,7 @@ cdef class GraphNode: """ return GN_embed(self, child) - def record_event(self, event: Event) -> EventRecordNode: + def record(self, event: Event) -> EventRecordNode: """Add an event record node depending on this node. Parameters @@ -332,7 +332,7 @@ cdef class GraphNode: """ return GN_record_event(self, event) - def wait_event(self, event: Event) -> EventWaitNode: + def wait(self, event: Event) -> EventWaitNode: """Add an event wait node depending on this node. Parameters @@ -382,7 +382,7 @@ cdef class GraphNode: """ return GN_callback(self, fn, user_data) - def if_cond(self, condition: GraphCondition) -> IfNode: + def if_then(self, condition: GraphCondition) -> IfNode: """Add an if-conditional node depending on this node. The body graph executes only when the condition evaluates to diff --git a/cuda_core/cuda/core/graph/_subclasses.pyx b/cuda_core/cuda/core/graph/_subclasses.pyx index 86cf9eea53e..25b648bacef 100644 --- a/cuda_core/cuda/core/graph/_subclasses.pyx +++ b/cuda_core/cuda/core/graph/_subclasses.pyx @@ -151,7 +151,7 @@ cdef class KernelNode(GraphNode): def config(self) -> LaunchConfig: """A LaunchConfig reconstructed from this node's grid, block, and shmem_size. - Note: cluster dimensions and cooperative_launch are not preserved + Note: cluster dimensions and is_cooperative are not preserved by the CUDA driver's kernel node params, so they are not included. """ return LaunchConfig(grid=self._grid, block=self._block, @@ -181,7 +181,7 @@ cdef class AllocNode(GraphNode): cdef AllocNode _create_with_params(GraphNodeHandle h_node, cydriver.CUdeviceptr dptr, size_t bytesize, int device_id, str memory_type, tuple peer_access): - """Create from known params (called by alloc() builder).""" + """Create from known params (called by allocate() builder).""" cdef AllocNode n = AllocNode.__new__(AllocNode) n._h_node = h_node n._dptr = dptr @@ -275,7 +275,7 @@ cdef class FreeNode(GraphNode): @staticmethod cdef FreeNode _create_with_params(GraphNodeHandle h_node, cydriver.CUdeviceptr dptr): - """Create from known params (called by free() builder).""" + """Create from known params (called by deallocate() builder).""" cdef FreeNode n = FreeNode.__new__(FreeNode) n._h_node = h_node n._dptr = dptr @@ -504,7 +504,7 @@ cdef class EventRecordNode(GraphNode): @staticmethod cdef EventRecordNode _create_with_params(GraphNodeHandle h_node, EventHandle h_event): - """Create from known params (called by record_event() builder).""" + """Create from known params (called by record() builder).""" cdef EventRecordNode n = EventRecordNode.__new__(EventRecordNode) n._h_node = h_node n._h_event = h_event @@ -542,7 +542,7 @@ cdef class EventWaitNode(GraphNode): @staticmethod cdef EventWaitNode _create_with_params(GraphNodeHandle h_node, EventHandle h_event): - """Create from known params (called by wait_event() builder).""" + """Create from known params (called by wait() builder).""" cdef EventWaitNode n = EventWaitNode.__new__(EventWaitNode) n._h_node = h_node n._h_event = h_event @@ -621,7 +621,7 @@ cdef class HostCallbackNode(GraphNode): cdef class ConditionalNode(GraphNode): """Base class for conditional nodes. - When created via builder methods (if_cond, if_else, while_loop, switch), + When created via builder methods (if_then, if_else, while_loop, switch), a specific subclass (IfNode, IfElseNode, WhileNode, SwitchNode) is returned. When reconstructed from the driver on CUDA 13.2+, the correct subclass is determined via cuGraphNodeGetParams. On older diff --git a/cuda_core/docs/source/release/0.3.0-notes.rst b/cuda_core/docs/source/release/0.3.0-notes.rst index 379559e6c53..d59f912743d 100644 --- a/cuda_core/docs/source/release/0.3.0-notes.rst +++ b/cuda_core/docs/source/release/0.3.0-notes.rst @@ -32,7 +32,7 @@ New features - :class:`~_module.Kernel` adds :attr:`~_module.Kernel.num_arguments` and :attr:`~_module.Kernel.arguments_info` for introspection of kernel arguments. (#612) - Add pythonic access to kernel occupancy calculation functions via :attr:`Kernel.occupancy`. (#648) -- Support launching cooperative kernels by setting :attr:`LaunchConfig.cooperative_launch` to ``True``. +- Support launching cooperative kernels by setting ``LaunchConfig.cooperative_launch`` to ``True``. - A name can be assigned to :class:`ObjectCode` instances generated by both :class:`Program` and :class:`Linker` through their respective options. - Expose :class:`Buffer`, :class:`DeviceMemoryResource`, :class:`LegacyPinnedMemoryResource`, and :class:`MemoryResource` to the top namespace. - Before this release, the internal :class:`Buffer` class had an ``__init__()`` constructor. To align with the design of cuda.core objects, diff --git a/cuda_core/docs/source/release/0.4.0-notes.rst b/cuda_core/docs/source/release/0.4.0-notes.rst index 929ce138f99..cbe3d432400 100644 --- a/cuda_core/docs/source/release/0.4.0-notes.rst +++ b/cuda_core/docs/source/release/0.4.0-notes.rst @@ -49,7 +49,7 @@ Fixes and enhancements - Improved :class:`DeviceMemoryResource` allocation performance when there are no active allocations by setting a higher release threshold (addresses issue #771). - Improved :class:`StridedMemoryView` creation time performance by optimizing shape and strides tuple creation using Python/C API (addresses issue #449). - Fix :class:`LaunchConfig` grid unit conversion when cluster is set (addresses issue #867). -- Fixed a bug in :class:`GraphBuilder.add_child` where dependencies extracted from capturing stream were passed inconsistently with num_dependencies parameter (addresses issue #843). +- Fixed a bug in ``GraphBuilder.add_child`` where dependencies extracted from capturing stream were passed inconsistently with num_dependencies parameter (addresses issue #843). - Make :class:`Buffer` creation more performant. - Enabled :class:`MemoryResource` subclasses to accept :class:`Device` objects, in addition to previously supported device ordinals. - Fixed a bug in :class:`Stream` and other classes where object cleanup would error during interpreter shutdown. diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst index 7a93eff4696..3f61a30ec1e 100644 --- a/cuda_core/docs/source/release/1.0.0-notes.rst +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -30,6 +30,88 @@ Breaking changes to follow the ``Graph*`` prefix convention used by ``GraphBuilder``, ``GraphDefinition``, ``GraphNode``. (`#1945 `__) +- Converted no-argument deterministic getters to properties for consistency + with the rest of the API + (`#1945 `__): + + - :meth:`Buffer.get_ipc_descriptor` -> :attr:`Buffer.ipc_descriptor` + - :meth:`Event.get_ipc_descriptor` -> :attr:`Event.ipc_descriptor` + - :meth:`DeviceMemoryResource.get_allocation_handle` -> + :attr:`DeviceMemoryResource.allocation_handle` + - :meth:`PinnedMemoryResource.get_allocation_handle` -> + :attr:`PinnedMemoryResource.allocation_handle` + +- Renamed boolean / non-noun properties for clearer naming + (`#1945 `__): + + - ``LaunchConfig.cooperative_launch`` -> :attr:`LaunchConfig.is_cooperative` + (also renames the constructor keyword argument). + - ``Event.is_timing_disabled`` -> :attr:`Event.is_timing_enabled`. + - ``Event.is_sync_busy_waited`` -> :attr:`Event.is_blocking_sync`. + - ``EventOptions.enable_timing`` -> ``EventOptions.timing_enabled`` + and ``EventOptions.busy_waited_sync`` -> ``EventOptions.blocking_sync``. + +- Renamed graph allocation methods to match :meth:`MemoryResource.allocate` / + :meth:`MemoryResource.deallocate` + (`#1945 `__): + + - ``GraphDefinition.alloc`` -> :meth:`graph.GraphDefinition.allocate` + - ``GraphDefinition.free`` -> :meth:`graph.GraphDefinition.deallocate` + - ``GraphNode.alloc`` -> :meth:`graph.GraphNode.allocate` + - ``GraphNode.free`` -> :meth:`graph.GraphNode.deallocate` + +- Cross-API consistency for graph builders + (`#1945 `__): + + - ``GraphBuilder.add_child`` -> :meth:`graph.GraphBuilder.embed` + (matches :meth:`graph.GraphDefinition.embed` and + :meth:`graph.GraphNode.embed`). + - ``GraphDefinition.record_event`` / ``wait_event`` -> + :meth:`graph.GraphDefinition.record` / :meth:`graph.GraphDefinition.wait` + and the same on :class:`~graph.GraphNode`, matching + :meth:`Stream.record` / :meth:`Stream.wait`. + +- :class:`KernelAttributes` methods are now properties; per-device queries + use indexing + (`#1945 `__): + + - The 17 attribute methods (``max_threads_per_block``, ``num_regs``, + ``shared_size_bytes``, ``cluster_scheduling_policy_preference``, etc.) + that previously took a ``device_id`` argument are now properties on + the view returned by :attr:`Kernel.attributes`. The view is bound to + the current device by default; ``kernel.attributes[device]`` returns + a view bound to a specific :class:`Device` or device ordinal. The + cache is shared across views of the same kernel. + - Old: ``kernel.attributes.num_regs()`` and + ``kernel.attributes.num_regs(some_dev)`` + - New: ``kernel.attributes.num_regs`` and + ``kernel.attributes[some_dev].num_regs`` + +- Unified the conditional graph API on :class:`~graph.GraphCondition` + and consistent verbs + (`#1945 `__): + + - ``GraphBuilder.create_conditional_handle`` -> + :meth:`graph.GraphBuilder.create_condition`. The new factory returns a + :class:`~graph.GraphCondition` (matching + :meth:`graph.GraphDefinition.create_condition`) instead of a raw + ``CUgraphConditionalHandle``. The four conditional builder methods + (:meth:`~graph.GraphBuilder.if_then`, + :meth:`~graph.GraphBuilder.if_else`, + :meth:`~graph.GraphBuilder.while_loop`, + :meth:`~graph.GraphBuilder.switch`) now accept a + :class:`~graph.GraphCondition` instead of a raw handle. + - ``GraphBuilder.if_cond`` / ``GraphDefinition.if_cond`` / + ``GraphNode.if_cond`` -> :meth:`graph.GraphBuilder.if_then` / + :meth:`graph.GraphDefinition.if_then` / + :meth:`graph.GraphNode.if_then`. The new name parallels the existing + ``if_else``, ``while_loop``, and ``switch`` methods (verb describing the + control-flow construct, not an abbreviation of "condition") and matches + Python's own ``if/then/else`` vocabulary. + - A :class:`~graph.GraphCondition` may be passed directly as a kernel + argument to ``launch()``; the launcher unwraps it to the underlying + ``CUgraphConditionalHandle`` value. Previously, ``.handle`` had to be + extracted explicitly. Fixes and enhancements diff --git a/cuda_core/tests/graph/test_graph_builder.py b/cuda_core/tests/graph/test_graph_builder.py index aca5a83ecfc..c0299df5661 100644 --- a/cuda_core/tests/graph/test_graph_builder.py +++ b/cuda_core/tests/graph/test_graph_builder.py @@ -234,7 +234,7 @@ def test_graph_child_graph(init_cuda): ## Add child try: - gb_parent.add_child(gb_child) + gb_parent.embed(gb_child) except NotImplementedError as e: with pytest.raises( NotImplementedError, diff --git a/cuda_core/tests/graph/test_graph_builder_conditional.py b/cuda_core/tests/graph/test_graph_builder_conditional.py index 1446b8b3c4f..de65848c1a0 100644 --- a/cuda_core/tests/graph/test_graph_builder_conditional.py +++ b/cuda_core/tests/graph/test_graph_builder_conditional.py @@ -35,17 +35,17 @@ def test_graph_conditional_if(init_cuda, condition_value): # Add Node A (sets condition) try: - handle = gb.create_conditional_handle() + condition = gb.create_condition() except RuntimeError as e: with pytest.raises(RuntimeError, match="^Driver version"): raise e gb.end_building() b.close() pytest.skip("Driver does not support conditional handle") - launch(gb, LaunchConfig(grid=1, block=1), set_handle, handle, condition_value) + launch(gb, LaunchConfig(grid=1, block=1), set_handle, condition, condition_value) # Add Node B (if condition) - gb_if = gb.if_cond(handle).begin_building() + gb_if = gb.if_then(condition).begin_building() launch(gb_if, LaunchConfig(grid=1, block=1), add_one, arr.ctypes.data) gb_if_0, gb_if_1 = gb_if.split(2) launch(gb_if_0, LaunchConfig(grid=1, block=1), add_one, arr.ctypes.data) @@ -98,12 +98,12 @@ def test_graph_conditional_if_else(init_cuda, condition_value): gb = Device().create_graph_builder().begin_building() # Add Node A (sets condition) - handle = gb.create_conditional_handle() - launch(gb, LaunchConfig(grid=1, block=1), set_handle, handle, condition_value) + condition = gb.create_condition() + launch(gb, LaunchConfig(grid=1, block=1), set_handle, condition, condition_value) # Add Node B (if condition) try: - gb_if, gb_else = gb.if_else(handle) + gb_if, gb_else = gb.if_else(condition) except RuntimeError as e: with pytest.raises(RuntimeError, match="^(Driver|Binding) version"): raise e @@ -171,12 +171,12 @@ def test_graph_conditional_switch(init_cuda, condition_value): gb = Device().create_graph_builder().begin_building() # Add Node A (sets condition) - handle = gb.create_conditional_handle() - launch(gb, LaunchConfig(grid=1, block=1), set_handle, handle, condition_value) + condition = gb.create_condition() + launch(gb, LaunchConfig(grid=1, block=1), set_handle, condition, condition_value) # Add Node B (while condition) try: - gb_case = list(gb.switch(handle, 3)) + gb_case = list(gb.switch(condition, 3)) except RuntimeError as e: with pytest.raises(RuntimeError, match="^(Driver|Binding) version"): raise e @@ -261,14 +261,14 @@ def test_graph_conditional_while(init_cuda, condition_value): gb = Device().create_graph_builder().begin_building() # Node A is skipped because we can instead use a non-zero default value - handle = gb.create_conditional_handle(default_value=condition_value) + condition = gb.create_condition(default_value=condition_value) # Add Node B (while condition) - gb_while = gb.while_loop(handle) + gb_while = gb.while_loop(condition) gb_while.begin_building() launch(gb_while, LaunchConfig(grid=1, block=1), add_one, arr.ctypes.data) launch(gb_while, LaunchConfig(grid=1, block=1), add_one, arr.ctypes.data) - launch(gb_while, LaunchConfig(grid=1, block=1), loop_kernel, handle) + launch(gb_while, LaunchConfig(grid=1, block=1), loop_kernel, condition) gb_while.end_building() # Add Node C (...) diff --git a/cuda_core/tests/graph/test_graph_definition.py b/cuda_core/tests/graph/test_graph_definition.py index 4e0f28e22dc..e85645e0305 100644 --- a/cuda_core/tests/graph/test_graph_definition.py +++ b/cuda_core/tests/graph/test_graph_definition.py @@ -91,7 +91,7 @@ def _build_empty(): def _build_single(): """One alloc node, no edges.""" g = GraphDefinition() - a = g.alloc(ALLOC_SIZE) + a = g.allocate(ALLOC_SIZE) return GraphSpec( "single", g, @@ -105,9 +105,9 @@ def _build_single(): def _build_chain(): """Linear chain: a -> b -> c.""" g = GraphDefinition() - a = g.alloc(ALLOC_SIZE) - b = a.alloc(ALLOC_SIZE) - c = b.alloc(ALLOC_SIZE) + a = g.allocate(ALLOC_SIZE) + b = a.allocate(ALLOC_SIZE) + c = b.allocate(ALLOC_SIZE) return GraphSpec( "chain", g, @@ -121,10 +121,10 @@ def _build_chain(): def _build_fan_out(): """One node feeds three: a -> {b, c, d}.""" g = GraphDefinition() - a = g.alloc(ALLOC_SIZE) - b = a.alloc(ALLOC_SIZE) - c = a.alloc(ALLOC_SIZE) - d = a.alloc(ALLOC_SIZE) + a = g.allocate(ALLOC_SIZE) + b = a.allocate(ALLOC_SIZE) + c = a.allocate(ALLOC_SIZE) + d = a.allocate(ALLOC_SIZE) return GraphSpec( "fan_out", g, @@ -138,9 +138,9 @@ def _build_fan_out(): def _build_fan_in(): """Three entry nodes merge: {a, b, c} -> d (join).""" g = GraphDefinition() - a = g.alloc(ALLOC_SIZE) - b = g.alloc(ALLOC_SIZE) - c = g.alloc(ALLOC_SIZE) + a = g.allocate(ALLOC_SIZE) + b = g.allocate(ALLOC_SIZE) + c = g.allocate(ALLOC_SIZE) d = g.join(a, b, c) return GraphSpec( "fan_in", @@ -155,9 +155,9 @@ def _build_fan_in(): def _build_diamond(): """Diamond: a -> {b, c} -> d (join).""" g = GraphDefinition() - a = g.alloc(ALLOC_SIZE) - b = a.alloc(ALLOC_SIZE) - c = a.alloc(ALLOC_SIZE) + a = g.allocate(ALLOC_SIZE) + b = a.allocate(ALLOC_SIZE) + c = a.allocate(ALLOC_SIZE) d = b.join(c) return GraphSpec( "diamond", @@ -172,8 +172,8 @@ def _build_diamond(): def _build_disconnected(): """Two independent entry nodes: a, b.""" g = GraphDefinition() - a = g.alloc(ALLOC_SIZE) - b = g.alloc(ALLOC_SIZE) + a = g.allocate(ALLOC_SIZE) + b = g.allocate(ALLOC_SIZE) return GraphSpec( "disconnected", g, @@ -238,8 +238,8 @@ def roundtrip_class(self): def _build_empty_node(g): - a = g.alloc(ALLOC_SIZE) - b = g.alloc(ALLOC_SIZE) + a = g.allocate(ALLOC_SIZE) + b = g.allocate(ALLOC_SIZE) return g.join(a, b), {} @@ -247,7 +247,7 @@ def _build_kernel_node(g): mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=(2, 3, 1), block=(32, 4, 1), shmem_size=128) - entry = g.alloc(ALLOC_SIZE) + entry = g.allocate(ALLOC_SIZE) node = entry.launch(config, kernel) return node, { "grid": (2, 3, 1), @@ -260,8 +260,8 @@ def _build_kernel_node(g): def _build_alloc_node(g): device_id = Device().device_id - entry = g.alloc(ALLOC_SIZE) - node = entry.alloc(ALLOC_SIZE) + entry = g.allocate(ALLOC_SIZE) + node = entry.allocate(ALLOC_SIZE) return node, { "dptr": lambda v: v != 0, "bytesize": ALLOC_SIZE, @@ -276,8 +276,8 @@ def _build_alloc_managed_node(g): _skip_if_no_managed_mempool() device_id = Device().device_id options = GraphAllocOptions(memory_type="managed") - entry = g.alloc(ALLOC_SIZE) - node = entry.alloc(ALLOC_SIZE, options) + entry = g.allocate(ALLOC_SIZE) + node = entry.allocate(ALLOC_SIZE, options) return node, { "dptr": lambda v: v != 0, "bytesize": ALLOC_SIZE, @@ -289,15 +289,15 @@ def _build_alloc_managed_node(g): def _build_free_node(g): - alloc = g.alloc(ALLOC_SIZE) - node = alloc.free(alloc.dptr) + alloc = g.allocate(ALLOC_SIZE) + node = alloc.deallocate(alloc.dptr) return node, { "dptr": alloc.dptr, } def _build_memset_node(g): - alloc = g.alloc(ALLOC_SIZE) + alloc = g.allocate(ALLOC_SIZE) node = alloc.memset(alloc.dptr, 42, ALLOC_SIZE) return node, { "dptr": alloc.dptr, @@ -310,7 +310,7 @@ def _build_memset_node(g): def _build_memset_node_u16(g): - alloc = g.alloc(ALLOC_SIZE) + alloc = g.allocate(ALLOC_SIZE) node = alloc.memset(alloc.dptr, b"\xab\xcd", ALLOC_SIZE // 2) return node, { "dptr": alloc.dptr, @@ -323,7 +323,7 @@ def _build_memset_node_u16(g): def _build_memset_node_u32(g): - alloc = g.alloc(ALLOC_SIZE) + alloc = g.allocate(ALLOC_SIZE) node = alloc.memset(alloc.dptr, b"\x01\x02\x03\x04", ALLOC_SIZE // 4) return node, { "dptr": alloc.dptr, @@ -338,7 +338,7 @@ def _build_memset_node_u32(g): def _build_memset_node_2d(g): rows = 4 cols = ALLOC_SIZE // rows - alloc = g.alloc(ALLOC_SIZE) + alloc = g.allocate(ALLOC_SIZE) node = alloc.memset(alloc.dptr, 0xFF, cols, height=rows, pitch=cols) return node, { "dptr": alloc.dptr, @@ -352,8 +352,8 @@ def _build_memset_node_2d(g): def _build_event_record_node(g): event = Device().create_event() - entry = g.alloc(ALLOC_SIZE) - node = entry.record_event(event) + entry = g.allocate(ALLOC_SIZE) + node = entry.record(event) return node, { "event": event, } @@ -361,16 +361,16 @@ def _build_event_record_node(g): def _build_event_wait_node(g): event = Device().create_event() - entry = g.alloc(ALLOC_SIZE) - node = entry.wait_event(event) + entry = g.allocate(ALLOC_SIZE) + node = entry.wait(event) return node, { "event": event, } def _build_memcpy_node(g): - src_alloc = g.alloc(ALLOC_SIZE) - dst_alloc = g.alloc(ALLOC_SIZE) + src_alloc = g.allocate(ALLOC_SIZE) + dst_alloc = g.allocate(ALLOC_SIZE) dep = g.join(src_alloc, dst_alloc) node = dep.memcpy(dst_alloc.dptr, src_alloc.dptr, ALLOC_SIZE) return node, { @@ -416,9 +416,9 @@ def _build_child_graph_node(g): } -def _build_if_cond_node(g): +def _build_if_then_node(g): condition = try_create_condition(g) - node = g.if_cond(condition) + node = g.if_then(condition) return node, { "condition": condition, "cond_type": "if", @@ -514,14 +514,14 @@ def _build_switch_node(g): ), pytest.param( NodeSpec( - "if_cond", + "if_then", IfNode, "CU_GRAPH_NODE_TYPE_CONDITIONAL", - _build_if_cond_node, + _build_if_then_node, reconstructed_class=IfNode if _HAS_NODE_GET_PARAMS else ConditionalNode, needs_mempool=False, ), - id="if_cond", + id="if_then", ), pytest.param( NodeSpec( @@ -800,24 +800,24 @@ def test_alloc_zero_size_fails(sample_graphdef): from cuda.core._utils.cuda_utils import CUDAError with pytest.raises(CUDAError): - sample_graphdef.alloc(0) + sample_graphdef.allocate(0) def test_free_creates_dependency(sample_graphdef): """Free node depends on its predecessor.""" _skip_if_no_mempool() - alloc = sample_graphdef.alloc(ALLOC_SIZE) - free = alloc.free(alloc.dptr) + alloc = sample_graphdef.allocate(ALLOC_SIZE) + free = alloc.deallocate(alloc.dptr) assert alloc in free.pred def test_alloc_free_chain(sample_graphdef): """Alloc and free can be chained.""" _skip_if_no_mempool() - a1 = sample_graphdef.alloc(ALLOC_SIZE) - a2 = a1.alloc(ALLOC_SIZE) - f2 = a2.free(a2.dptr) - f1 = f2.free(a1.dptr) + a1 = sample_graphdef.allocate(ALLOC_SIZE) + a2 = a1.allocate(ALLOC_SIZE) + f2 = a2.deallocate(a2.dptr) + f1 = f2.deallocate(a1.dptr) assert a1 in a2.pred assert a2 in f2.pred assert f2 in f1.pred @@ -832,7 +832,7 @@ def test_alloc_memory_type_invalid(sample_graphdef): """Invalid memory type raises ValueError.""" options = GraphAllocOptions(memory_type="invalid") with pytest.raises(ValueError, match="Invalid memory_type"): - sample_graphdef.alloc(ALLOC_SIZE, options) + sample_graphdef.allocate(ALLOC_SIZE, options) @pytest.mark.parametrize( @@ -847,7 +847,7 @@ def test_alloc_device_option(sample_graphdef, device_spec): _skip_if_no_mempool() device = Device() options = GraphAllocOptions(device=device_spec(device)) - node = sample_graphdef.alloc(ALLOC_SIZE, options) + node = sample_graphdef.allocate(ALLOC_SIZE, options) assert node.dptr != 0 @@ -856,7 +856,7 @@ def test_alloc_peer_access(mempool_device_x2): d0, d1 = mempool_device_x2 g = GraphDefinition() options = GraphAllocOptions(device=d0.device_id, peer_access=[d1.device_id]) - node = g.alloc(ALLOC_SIZE, options) + node = g.allocate(ALLOC_SIZE, options) assert d1.device_id in node.peer_access @@ -869,7 +869,7 @@ def test_alloc_peer_access(mempool_device_x2): def test_join_merges_branches(sample_graphdef, num_branches): """join() with multiple branches creates correct dependencies.""" _skip_if_no_mempool() - branches = [sample_graphdef.alloc(ALLOC_SIZE) for _ in range(num_branches)] + branches = [sample_graphdef.allocate(ALLOC_SIZE) for _ in range(num_branches)] joined = sample_graphdef.join(*branches) assert isinstance(joined, EmptyNode) assert set(joined.pred) == set(branches) @@ -962,8 +962,8 @@ def test_instantiate_empty_graph(sample_graphdef, inst_kwargs): def test_instantiate_with_nodes(sample_graphdef, inst_kwargs): """Graph with nodes can be instantiated.""" _skip_if_no_mempool() - sample_graphdef.alloc(ALLOC_SIZE) - sample_graphdef.alloc(ALLOC_SIZE) + sample_graphdef.allocate(ALLOC_SIZE) + sample_graphdef.allocate(ALLOC_SIZE) graph = _instantiate(sample_graphdef, inst_kwargs) assert graph is not None @@ -1003,8 +1003,8 @@ def test_instantiate_and_execute_kernel(sample_graphdef, inst_kwargs): def test_instantiate_and_execute_alloc_free(sample_graphdef, inst_kwargs): """Graph with alloc/free can be executed.""" _skip_if_no_mempool() - alloc = sample_graphdef.alloc(ALLOC_SIZE) - alloc.free(alloc.dptr) + alloc = sample_graphdef.allocate(ALLOC_SIZE) + alloc.deallocate(alloc.dptr) stream = Device().create_stream() graph = _instantiate_and_upload(sample_graphdef, inst_kwargs, stream) @@ -1016,9 +1016,9 @@ def test_instantiate_and_execute_alloc_free(sample_graphdef, inst_kwargs): def test_instantiate_and_execute_memset(sample_graphdef, inst_kwargs): """Graph with alloc/memset/free can be executed.""" _skip_if_no_mempool() - alloc = sample_graphdef.alloc(ALLOC_SIZE) + alloc = sample_graphdef.allocate(ALLOC_SIZE) ms = alloc.memset(alloc.dptr, 0xAB, ALLOC_SIZE) - ms.free(alloc.dptr) + ms.deallocate(alloc.dptr) stream = Device().create_stream() graph = _instantiate_and_upload(sample_graphdef, inst_kwargs, stream) @@ -1032,12 +1032,12 @@ def test_instantiate_and_execute_memcpy(sample_graphdef, inst_kwargs): _skip_if_no_mempool() import ctypes - src_alloc = sample_graphdef.alloc(ALLOC_SIZE) - dst_alloc = sample_graphdef.alloc(ALLOC_SIZE) + src_alloc = sample_graphdef.allocate(ALLOC_SIZE) + dst_alloc = sample_graphdef.allocate(ALLOC_SIZE) dep = sample_graphdef.join(src_alloc, dst_alloc) ms = dep.memset(src_alloc.dptr, 0xAB, ALLOC_SIZE) cp = ms.memcpy(dst_alloc.dptr, src_alloc.dptr, ALLOC_SIZE) - cp.free(src_alloc.dptr) + cp.deallocate(src_alloc.dptr) stream = Device().create_stream() graph = _instantiate_and_upload(sample_graphdef, inst_kwargs, stream) @@ -1139,8 +1139,8 @@ def test_host_callback_user_data_rejected_for_python_callable(sample_graphdef): def test_instantiate_and_execute_event_record_wait(sample_graphdef): """Graph with event record and wait nodes can be executed.""" event = Device().create_event() - rec = sample_graphdef.record_event(event) - rec.wait_event(event) + rec = sample_graphdef.record(event) + rec.wait(event) graph = sample_graphdef.instantiate() stream = Device().create_stream() @@ -1159,7 +1159,7 @@ def _skip_unless_cc_90(): pytest.skip("Conditional node execution requires CC >= 9.0 (Hopper)") -def test_instantiate_and_execute_if_cond(sample_graphdef): +def test_instantiate_and_execute_if_then(sample_graphdef): """If-conditional node: body executes only when condition is non-zero.""" _skip_unless_cc_90() _skip_if_no_mempool() @@ -1172,10 +1172,10 @@ def test_instantiate_and_execute_if_cond(sample_graphdef): set_handle = mod.get_kernel("set_handle") add_one = mod.get_kernel("add_one") - alloc = sample_graphdef.alloc(ctypes.sizeof(ctypes.c_int)) + alloc = sample_graphdef.allocate(ctypes.sizeof(ctypes.c_int)) ms = alloc.memset(alloc.dptr, 0, ctypes.sizeof(ctypes.c_int)) - setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition.handle, 1) - if_node = setter.if_cond(condition) + setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition, 1) + if_node = setter.if_then(condition) if_node.then.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) graph = sample_graphdef.instantiate() @@ -1204,9 +1204,9 @@ def test_instantiate_and_execute_if_else(sample_graphdef): set_handle = mod.get_kernel("set_handle") add_one = mod.get_kernel("add_one") - alloc = sample_graphdef.alloc(ctypes.sizeof(ctypes.c_int)) + alloc = sample_graphdef.allocate(ctypes.sizeof(ctypes.c_int)) ms = alloc.memset(alloc.dptr, 0, ctypes.sizeof(ctypes.c_int)) - setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition.handle, 0) + setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition, 0) ie_node = setter.if_else(condition) ie_node.then.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) n1 = ie_node.else_.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) @@ -1238,9 +1238,9 @@ def test_instantiate_and_execute_switch(sample_graphdef): set_handle = mod.get_kernel("set_handle") add_one = mod.get_kernel("add_one") - alloc = sample_graphdef.alloc(ctypes.sizeof(ctypes.c_int)) + alloc = sample_graphdef.allocate(ctypes.sizeof(ctypes.c_int)) ms = alloc.memset(alloc.dptr, 0, ctypes.sizeof(ctypes.c_int)) - setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition.handle, 2) + setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition, 2) sw_node = setter.switch(condition, 4) for branch in sw_node.branches: branch.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) @@ -1261,7 +1261,7 @@ def test_instantiate_and_execute_switch(sample_graphdef): def test_conditional_node_type_preserved_by_nodes(sample_graphdef): """Conditional nodes appear as ConditionalNode base when read back from graph.""" condition = try_create_condition(sample_graphdef) - if_node = sample_graphdef.if_cond(condition) + if_node = sample_graphdef.if_then(condition) assert isinstance(if_node, IfNode) all_nodes = sample_graphdef.nodes() @@ -1278,7 +1278,7 @@ def test_conditional_node_type_preserved_by_nodes(sample_graphdef): def test_debug_dot_print_creates_file(sample_graphdef, dot_file): """debug_dot_print writes a DOT file.""" _skip_if_no_mempool() - sample_graphdef.alloc(ALLOC_SIZE) + sample_graphdef.allocate(ALLOC_SIZE) sample_graphdef.debug_dot_print(str(dot_file)) assert dot_file.exists() content = dot_file.read_text() @@ -1288,7 +1288,7 @@ def test_debug_dot_print_creates_file(sample_graphdef, dot_file): def test_debug_dot_print_with_options(sample_graphdef, dot_file): """debug_dot_print accepts GraphDebugPrintOptions.""" _skip_if_no_mempool() - sample_graphdef.alloc(ALLOC_SIZE) + sample_graphdef.allocate(ALLOC_SIZE) options = GraphDebugPrintOptions(verbose=True, handles=True) sample_graphdef.debug_dot_print(str(dot_file), options) assert dot_file.exists() @@ -1297,6 +1297,6 @@ def test_debug_dot_print_with_options(sample_graphdef, dot_file): def test_debug_dot_print_invalid_options(sample_graphdef, dot_file): """debug_dot_print rejects invalid options type.""" _skip_if_no_mempool() - sample_graphdef.alloc(ALLOC_SIZE) + sample_graphdef.allocate(ALLOC_SIZE) with pytest.raises(TypeError, match="options must be a GraphDebugPrintOptions"): sample_graphdef.debug_dot_print(str(dot_file), "invalid") diff --git a/cuda_core/tests/graph/test_graph_definition_errors.py b/cuda_core/tests/graph/test_graph_definition_errors.py index 2f6935d6eb0..40f181e5db1 100644 --- a/cuda_core/tests/graph/test_graph_definition_errors.py +++ b/cuda_core/tests/graph/test_graph_definition_errors.py @@ -33,7 +33,7 @@ def _skip_if_no_mempool(): @pytest.mark.parametrize( "method, args", [ - pytest.param("if_cond", (42,), id="if_cond_int"), + pytest.param("if_then", (42,), id="if_then_int"), pytest.param("if_else", ("not a condition",), id="if_else_str"), pytest.param("while_loop", (None,), id="while_loop_none"), pytest.param("switch", ([1, 2, 3], 4), id="switch_list"), @@ -62,14 +62,14 @@ def test_free_null_pointer(init_cuda): """free(0) raises a CUDA error.""" g = GraphDefinition() with pytest.raises(CUDAError): - g.free(0) + g.deallocate(0) def test_memset_invalid_value_size(init_cuda): """memset with 3-byte value (not 1, 2, or 4) raises ValueError.""" _skip_if_no_mempool() g = GraphDefinition() - alloc = g.alloc(1024) + alloc = g.allocate(1024) with pytest.raises(ValueError): alloc.memset(alloc.dptr, b"\x01\x02\x03", 100) @@ -93,7 +93,7 @@ def test_condition_from_different_graph(init_cuda): g2 = GraphDefinition() condition = try_create_condition(g1) with pytest.raises(CUDAError): - g2.if_cond(condition) + g2.if_then(condition) # ============================================================================= @@ -113,7 +113,7 @@ def test_join_single_predecessor(init_cuda): """node.join() with no extra args creates a single-dep empty node.""" _skip_if_no_mempool() g = GraphDefinition() - a = g.alloc(1024) + a = g.allocate(1024) joined = a.join() assert isinstance(joined, EmptyNode) assert set(joined.pred) == {a} @@ -136,7 +136,7 @@ def test_unmatched_alloc_succeeds(init_cuda): """Alloc without corresponding free is valid (graph-scoped lifetime).""" _skip_if_no_mempool() g = GraphDefinition() - g.alloc(1024) + g.allocate(1024) graph = g.instantiate() stream = Device().create_stream() graph.launch(stream) @@ -174,7 +174,7 @@ def test_while_loop_zero_iterations(init_cuda): g = GraphDefinition() condition = g.create_condition(default_value=0) - alloc = g.alloc(SIZEOF_INT) + alloc = g.allocate(SIZEOF_INT) ms = alloc.memset(alloc.dptr, 0, SIZEOF_INT) loop = ms.while_loop(condition) loop.body.launch(cfg, add_one, alloc.dptr) @@ -191,7 +191,7 @@ def test_while_loop_zero_iterations(init_cuda): assert result[0] == 0, "Body should not have executed" -def test_if_cond_false_skips_body(init_cuda): +def test_if_then_false_skips_body(init_cuda): """If conditional with default_value=0 does not execute its body.""" _skip_unless_cc_90() _skip_if_no_mempool() @@ -202,9 +202,9 @@ def test_if_cond_false_skips_body(init_cuda): g = GraphDefinition() condition = g.create_condition(default_value=0) - alloc = g.alloc(SIZEOF_INT) + alloc = g.allocate(SIZEOF_INT) ms = alloc.memset(alloc.dptr, 0, SIZEOF_INT) - if_node = ms.if_cond(condition) + if_node = ms.if_then(condition) if_node.then.launch(cfg, add_one, alloc.dptr) graph = g.instantiate() @@ -230,7 +230,7 @@ def test_switch_oob_skips_all_branches(init_cuda): g = GraphDefinition() condition = g.create_condition(default_value=99) - alloc = g.alloc(SIZEOF_INT) + alloc = g.allocate(SIZEOF_INT) ms = alloc.memset(alloc.dptr, 0, SIZEOF_INT) sw = ms.switch(condition, 3) for branch in sw.branches: diff --git a/cuda_core/tests/graph/test_graph_definition_integration.py b/cuda_core/tests/graph/test_graph_definition_integration.py index 1c5eecf40b6..b33b23d8860 100644 --- a/cuda_core/tests/graph/test_graph_definition_integration.py +++ b/cuda_core/tests/graph/test_graph_definition_integration.py @@ -215,8 +215,8 @@ def _run_heat_graph(dev, k_heat, k_countdown, host_ptr): # Definitions g = GraphDefinition() condition = g.create_condition(default_value=1) - event_start = dev.create_event(EventOptions(enable_timing=True)) - event_end = dev.create_event(EventOptions(enable_timing=True)) + event_start = dev.create_event(EventOptions(timing_enabled=True)) + event_end = dev.create_event(EventOptions(timing_enabled=True)) results = {} def capture_result(): @@ -230,9 +230,9 @@ def capture_result(): # fmt: off # Phase 1 — Allocate device memory - a_curr = g.alloc(_HEAT_N * SIZEOF_FLOAT) - a_next = g.alloc(_HEAT_N * SIZEOF_FLOAT) - a_ctr = g.alloc(SIZEOF_INT) + a_curr = g.allocate(_HEAT_N * SIZEOF_FLOAT) + a_next = g.allocate(_HEAT_N * SIZEOF_FLOAT) + a_ctr = g.allocate(SIZEOF_INT) # Phase 2 — Initialise buffers m_curr = a_curr.memset(a_curr.dptr, 0, _HEAT_N * SIZEOF_FLOAT) @@ -247,23 +247,23 @@ def capture_result(): .graph p = g.join(m_curr, m_next, m_ctr) \ .embed(bc) \ - .record_event(event_start) + .record(event_start) # Phase 4 — Iterate loop = p.while_loop(condition) loop.body.launch(heat_cfg, k_heat, a_next.dptr, a_curr.dptr, np.int32(_HEAT_N), _HEAT_ALPHA) \ .memcpy(a_curr.dptr, a_next.dptr, _HEAT_N * SIZEOF_FLOAT) \ - .launch(tick_cfg, k_countdown, condition.handle, a_ctr.dptr) + .launch(tick_cfg, k_countdown, condition, a_ctr.dptr) # Phase 5 — After loop: timing end, readback, verify, free memory - loop.wait_event(event_start) \ - .record_event(event_end) \ + loop.wait(event_start) \ + .record(event_end) \ .memcpy(host_ptr, a_curr.dptr, _HEAT_N * SIZEOF_FLOAT) \ .callback(capture_result) \ - .free(a_curr.dptr) \ - .free(a_next.dptr) \ - .free(a_ctr.dptr) + .deallocate(a_curr.dptr) \ + .deallocate(a_next.dptr) \ + .deallocate(a_ctr.dptr) # fmt: on # Phase 6 — Instantiate, launch, verify @@ -332,9 +332,9 @@ def capture_result(): # fmt: off # Allocate and initialise: a = 0.0, b = 2.0, counter = ITERS - a = g.alloc(SIZEOF_FLOAT) - b = g.alloc(SIZEOF_FLOAT) - ctr = g.alloc(SIZEOF_INT) + a = g.allocate(SIZEOF_FLOAT) + b = g.allocate(SIZEOF_FLOAT) + ctr = g.allocate(SIZEOF_INT) p = g.join(a.memset(a.dptr, np.float32(0.0), 1), b.memset(b.dptr, np.float32(2.0), 1), @@ -345,23 +345,23 @@ def capture_result(): ie_cond = g.create_condition(default_value=0) loop = p.while_loop(while_cond) - ie = loop.body.launch(cfg, k_eval, a.dptr, b.dptr, ie_cond.handle) \ + ie = loop.body.launch(cfg, k_eval, a.dptr, b.dptr, ie_cond) \ .if_else(ie_cond) ie.then.launch(cfg, k_hi, a.dptr, b.dptr) ie.else_.launch(cfg, k_lo, a.dptr, b.dptr) - ie.launch(cfg, k_cd, while_cond.handle, ctr.dptr) + ie.launch(cfg, k_cd, while_cond, ctr.dptr) # Post-loop: Newton refinement (IfNode), readback, free if_cond = g.create_condition(default_value=0) - if_node = loop.launch(cfg, k_check, a.dptr, b.dptr, if_cond.handle) \ - .if_cond(if_cond) + if_node = loop.launch(cfg, k_check, a.dptr, b.dptr, if_cond) \ + .if_then(if_cond) if_node.then.launch(cfg, k_newton, a.dptr, b.dptr) if_node.memcpy(host_ptr, a.dptr, SIZEOF_FLOAT) \ .callback(capture_result) \ - .free(a.dptr) \ - .free(b.dptr) \ - .free(ctr.dptr) + .deallocate(a.dptr) \ + .deallocate(b.dptr) \ + .deallocate(ctr.dptr) # fmt: on # Instantiate, launch, verify @@ -430,7 +430,7 @@ def _run_switch_graph(dev, mode, k_negate, k_double, k_square, host_ptr): cfg = LaunchConfig(grid=1, block=1) # fmt: off - x = g.alloc(SIZEOF_INT) + x = g.allocate(SIZEOF_INT) sw_cond = g.create_condition(default_value=mode) sw = x.memset(x.dptr, np.int32(_SWITCH_VALUE), 1) \ .switch(sw_cond, 4) @@ -441,7 +441,7 @@ def _run_switch_graph(dev, mode, k_negate, k_double, k_square, host_ptr): # branch 3: identity (no kernel — value unchanged) sw.memcpy(host_ptr, x.dptr, SIZEOF_INT) \ - .free(x.dptr) + .deallocate(x.dptr) # fmt: on graph = g.instantiate() diff --git a/cuda_core/tests/graph/test_graph_definition_lifetime.py b/cuda_core/tests/graph/test_graph_definition_lifetime.py index cfa538539f6..e231016c8ac 100644 --- a/cuda_core/tests/graph/test_graph_definition_lifetime.py +++ b/cuda_core/tests/graph/test_graph_definition_lifetime.py @@ -29,7 +29,7 @@ def _skip_if_no_mempool(): def _make_if(g, cond): - node = g.if_cond(cond) + node = g.if_then(cond) return [node.then] @@ -172,10 +172,10 @@ def test_event_record_node_keeps_event_alive(init_cuda): _skip_if_no_mempool() dev = Device() g = GraphDefinition() - alloc = g.alloc(1024) + alloc = g.allocate(1024) - event = dev.create_event(EventOptions(enable_timing=False)) - node = alloc.record_event(event) + event = dev.create_event(EventOptions(timing_enabled=False)) + node = alloc.record(event) del event gc.collect() @@ -189,10 +189,10 @@ def test_event_wait_node_keeps_event_alive(init_cuda): _skip_if_no_mempool() dev = Device() g = GraphDefinition() - alloc = g.alloc(1024) + alloc = g.allocate(1024) - event = dev.create_event(EventOptions(enable_timing=False)) - node = alloc.wait_event(event) + event = dev.create_event(EventOptions(timing_enabled=False)) + node = alloc.wait(event) del event gc.collect() @@ -206,12 +206,12 @@ def test_event_record_node_preserves_metadata(init_cuda): dev = Device() g = GraphDefinition() - event = dev.create_event(EventOptions(enable_timing=True, busy_waited_sync=True)) - node = g.record_event(event) + event = dev.create_event(EventOptions(timing_enabled=True, blocking_sync=True)) + node = g.record(event) reconstructed = node.event - assert reconstructed.is_timing_disabled is False - assert reconstructed.is_sync_busy_waited is True + assert reconstructed.is_timing_enabled is True + assert reconstructed.is_blocking_sync is True assert reconstructed.is_ipc_enabled is False assert reconstructed.device is not None @@ -221,12 +221,12 @@ def test_event_wait_node_preserves_metadata(init_cuda): dev = Device() g = GraphDefinition() - event = dev.create_event(EventOptions(enable_timing=False)) - node = g.wait_event(event) + event = dev.create_event(EventOptions(timing_enabled=False)) + node = g.wait(event) reconstructed = node.event - assert reconstructed.is_timing_disabled is True - assert reconstructed.is_sync_busy_waited is False + assert reconstructed.is_timing_enabled is False + assert reconstructed.is_blocking_sync is False assert reconstructed.device is not None @@ -235,15 +235,15 @@ def test_event_metadata_survives_gc(init_cuda): dev = Device() g = GraphDefinition() - event = dev.create_event(EventOptions(enable_timing=True, busy_waited_sync=True)) - node = g.record_event(event) + event = dev.create_event(EventOptions(timing_enabled=True, blocking_sync=True)) + node = g.record(event) del event gc.collect() retrieved = node.event - assert retrieved.is_timing_disabled is False - assert retrieved.is_sync_busy_waited is True + assert retrieved.is_timing_enabled is True + assert retrieved.is_blocking_sync is True assert retrieved.is_done is True @@ -252,9 +252,9 @@ def test_event_survives_graph_instantiation_and_execution(init_cuda): dev = Device() g = GraphDefinition() - event = dev.create_event(EventOptions(enable_timing=False)) - rec = g.record_event(event) - rec.wait_event(event) + event = dev.create_event(EventOptions(timing_enabled=False)) + rec = g.record(event) + rec.wait(event) del event gc.collect() @@ -277,9 +277,9 @@ def test_event_survives_graph_clone_and_execution(init_cuda): dev = Device() g = GraphDefinition() - event = dev.create_event(EventOptions(enable_timing=False)) - rec = g.record_event(event) - rec.wait_event(event) + event = dev.create_event(EventOptions(timing_enabled=False)) + rec = g.record(event) + rec.wait(event) cloned_cu_graph = handle_return(driver.cuGraphClone(driver.CUgraph(g.handle))) @@ -390,7 +390,7 @@ def test_kernel_node_keeps_kernel_alive(init_cuda): gc.collect() retrieved = node.kernel - assert retrieved.attributes.max_threads_per_block() > 0 + assert retrieved.attributes.max_threads_per_block > 0 def test_kernel_survives_graph_instantiation_and_execution(init_cuda): @@ -455,7 +455,7 @@ def test_kernel_from_handle_recovers_library(init_cuda): del kernel, mod gc.collect() - assert reconstructed.attributes.max_threads_per_block() > 0 + assert reconstructed.attributes.max_threads_per_block > 0 def test_kernel_node_reconstruction_preserves_validity(init_cuda): @@ -469,7 +469,7 @@ def test_kernel_node_reconstruction_preserves_validity(init_cuda): kernel_node = g.launch(config, kernel) # Chain a second node so we can reconstruct the kernel node via pred event = Device().create_event() - successor = kernel_node.record_event(event) + successor = kernel_node.record(event) del kernel, mod gc.collect() @@ -479,7 +479,7 @@ def test_kernel_node_reconstruction_preserves_validity(init_cuda): # -> create_kernel_handle_ref -> handle recovery reconstructed = next(iter(successor.pred)) assert isinstance(reconstructed, KernelNode) - assert reconstructed.kernel.attributes.max_threads_per_block() > 0 + assert reconstructed.kernel.attributes.max_threads_per_block > 0 graph = g.instantiate() stream = Device().create_stream() diff --git a/cuda_core/tests/graph/test_graph_update.py b/cuda_core/tests/graph/test_graph_update.py index 01d0183de5c..206fabcd587 100644 --- a/cuda_core/tests/graph/test_graph_update.py +++ b/cuda_core/tests/graph/test_graph_update.py @@ -79,11 +79,11 @@ def build_graph(condition_value): gb = Device().create_graph_builder().begin_building() # Add Node A (sets condition) - handle = gb.create_conditional_handle(default_value=condition_value) + condition = gb.create_condition(default_value=condition_value) # Add Node B (while condition) try: - gb_case = list(gb.switch(handle, 3)) + gb_case = list(gb.switch(condition, 3)) except Exception as e: with pytest.raises(RuntimeError, match="^(Driver|Binding) version"): raise e diff --git a/cuda_core/tests/graph/test_options.py b/cuda_core/tests/graph/test_options.py index 0d10db459d6..2002c1b7006 100644 --- a/cuda_core/tests/graph/test_options.py +++ b/cuda_core/tests/graph/test_options.py @@ -18,11 +18,11 @@ def test_graph_dot_print_options(init_cuda, tmp_path): gb = Device().create_graph_builder().begin_building() # Add Node A (sets condition) - handle = gb.create_conditional_handle() - launch(gb, LaunchConfig(grid=1, block=1), set_handle, handle, False) + condition = gb.create_condition() + launch(gb, LaunchConfig(grid=1, block=1), set_handle, condition, False) # Add Node B (if condition) - gb_if = gb.if_cond(handle).begin_building() + gb_if = gb.if_then(condition).begin_building() launch(gb_if, LaunchConfig(grid=1, block=1), empty_kernel) gb_if_0, gb_if_1 = gb_if.split(2) launch(gb_if_0, LaunchConfig(grid=1, block=1), empty_kernel) diff --git a/cuda_core/tests/memory_ipc/test_errors.py b/cuda_core/tests/memory_ipc/test_errors.py index a607f897b90..f82164ca37c 100644 --- a/cuda_core/tests/memory_ipc/test_errors.py +++ b/cuda_core/tests/memory_ipc/test_errors.py @@ -85,7 +85,7 @@ def PARENT_ACTION(self, queue): mr2 = DeviceMemoryResource(self.device, options=options) self._extra_mrs.append(mr2) buffer = mr2.allocate(NBYTES) - queue.put([self.mr, buffer.get_ipc_descriptor()]) # Note: mr does not own this buffer + queue.put([self.mr, buffer.ipc_descriptor]) # Note: mr does not own this buffer def CHILD_ACTION(self, queue): mr, buffer_desc = queue.get(timeout=CHILD_TIMEOUT_SEC) diff --git a/cuda_core/tests/memory_ipc/test_event_ipc.py b/cuda_core/tests/memory_ipc/test_event_ipc.py index 61fa7ca8536..e1bb45efcfb 100644 --- a/cuda_core/tests/memory_ipc/test_event_ipc.py +++ b/cuda_core/tests/memory_ipc/test_event_ipc.py @@ -114,7 +114,7 @@ def test_event_is_monadic(ipc_device): @pytest.mark.flaky(reruns=2) @pytest.mark.parametrize( - "options", [{"ipc_enabled": True, "enable_timing": True}, EventOptions(ipc_enabled=True, enable_timing=True)] + "options", [{"ipc_enabled": True, "timing_enabled": True}, EventOptions(ipc_enabled=True, timing_enabled=True)] ) def test_event_timing_disabled(ipc_device, options): """Check that IPC-enabled events cannot be created with timing enabled.""" @@ -131,10 +131,10 @@ class TestIpcEventProperties: """ @pytest.mark.flaky(reruns=2) - @pytest.mark.parametrize("busy_waited_sync", [True, False]) + @pytest.mark.parametrize("blocking_sync", [True, False]) @pytest.mark.parametrize("use_options_cls", [True, False]) @pytest.mark.parametrize("use_option_kw", [True, False]) - def test_main(self, ipc_device, busy_waited_sync, use_options_cls, use_option_kw): + def test_main(self, ipc_device, blocking_sync, use_options_cls, use_option_kw): device = ipc_device stream = device.create_stream() @@ -145,19 +145,19 @@ def test_main(self, ipc_device, busy_waited_sync, use_options_cls, use_option_kw # Create an event and send it. options = ( - EventOptions(ipc_enabled=True, busy_waited_sync=busy_waited_sync) + EventOptions(ipc_enabled=True, blocking_sync=blocking_sync) if use_options_cls - else {"ipc_enabled": True, "busy_waited_sync": busy_waited_sync} + else {"ipc_enabled": True, "blocking_sync": blocking_sync} ) e = stream.record(options=options) if use_option_kw else stream.record(None, options) q_out.put(e) # Check its properties. props = q_in.get(timeout=CHILD_TIMEOUT_SEC) - assert props[0] == e.get_ipc_descriptor() + assert props[0] == e.ipc_descriptor assert props[1] == e.is_ipc_enabled - assert props[2] == e.is_timing_disabled - assert props[3] == e.is_sync_busy_waited + assert props[2] == e.is_timing_enabled + assert props[3] == e.is_blocking_sync assert props[4] is None assert props[5] is None @@ -173,10 +173,10 @@ def child_main(self, q_in, q_out): # Send its properties. props = ( - e.get_ipc_descriptor(), + e.ipc_descriptor, e.is_ipc_enabled, - e.is_timing_disabled, - e.is_sync_busy_waited, + e.is_timing_enabled, + e.is_blocking_sync, e.device, e.context, ) diff --git a/cuda_core/tests/memory_ipc/test_ipc_duplicate_import.py b/cuda_core/tests/memory_ipc/test_ipc_duplicate_import.py index 0808a191dd7..cab6b44aa3f 100644 --- a/cuda_core/tests/memory_ipc/test_ipc_duplicate_import.py +++ b/cuda_core/tests/memory_ipc/test_ipc_duplicate_import.py @@ -79,8 +79,8 @@ def test_main(self, ipc_device, ipc_memory_resource): # Send the memory resource and buffer descriptor twice. log("sending mr and buffer descriptors") queue.put(mr) - queue.put(buffer.get_ipc_descriptor()) - queue.put(buffer.get_ipc_descriptor()) + queue.put(buffer.ipc_descriptor) + queue.put(buffer.ipc_descriptor) log("waiting for child") process.join(timeout=CHILD_TIMEOUT_SEC) diff --git a/cuda_core/tests/memory_ipc/test_leaks.py b/cuda_core/tests/memory_ipc/test_leaks.py index b89f198699a..8debd71c3f9 100644 --- a/cuda_core/tests/memory_ipc/test_leaks.py +++ b/cuda_core/tests/memory_ipc/test_leaks.py @@ -26,10 +26,10 @@ @pytest.mark.flaky(reruns=2) @skip_if_unrunnable def test_alloc_handle(ipc_memory_resource): - """Check for fd leaks in get_allocation_handle.""" + """Check for fd leaks in allocation_handle.""" mr = ipc_memory_resource with CheckFDLeaks(): - [mr.get_allocation_handle() for _ in range(10)] + [mr.allocation_handle for _ in range(10)] def exec_success(obj, number=1): @@ -84,10 +84,10 @@ def __reduce__(self): @pytest.mark.parametrize( "getobject", [ - lambda mr: mr.get_allocation_handle(), + lambda mr: mr.allocation_handle, lambda mr: mr, lambda mr: mr.allocate(NBYTES), - lambda mr: mr.allocate(NBYTES).get_ipc_descriptor(), + lambda mr: mr.allocate(NBYTES).ipc_descriptor, ], ids=["alloc_handle", "mr", "buffer", "buffer_desc"], ) diff --git a/cuda_core/tests/memory_ipc/test_memory_ipc.py b/cuda_core/tests/memory_ipc/test_memory_ipc.py index 1b320fa6f2c..0996c71d2cc 100644 --- a/cuda_core/tests/memory_ipc/test_memory_ipc.py +++ b/cuda_core/tests/memory_ipc/test_memory_ipc.py @@ -117,7 +117,7 @@ def test_main(self, ipc_device, ipc_memory_resource): # Set up the IPC-enabled memory pool and share it using one handle. device = ipc_device mr = ipc_memory_resource - alloc_handle = mr.get_allocation_handle() + alloc_handle = mr.allocation_handle # Start children. q1, q2 = (mp.Queue() for _ in range(2)) @@ -129,8 +129,8 @@ def test_main(self, ipc_device, ipc_memory_resource): # Allocate and share memory. buffer1 = mr.allocate(NBYTES) buffer2 = mr.allocate(NBYTES) - q1.put(buffer1.get_ipc_descriptor()) - q2.put(buffer2.get_ipc_descriptor()) + q1.put(buffer1.ipc_descriptor) + q2.put(buffer2.ipc_descriptor) # Wait for children. p1.join(timeout=CHILD_TIMEOUT_SEC) @@ -167,7 +167,7 @@ def test_main(self, ipc_device, ipc_memory_resource): """ device = ipc_device mr = ipc_memory_resource - alloc_handle = mr.get_allocation_handle() + alloc_handle = mr.allocation_handle # Start children. q1, q2 = (mp.Queue() for _ in range(2)) diff --git a/cuda_core/tests/memory_ipc/test_serialize.py b/cuda_core/tests/memory_ipc/test_serialize.py index bd6a880fdc0..63e6ccf1dfd 100644 --- a/cuda_core/tests/memory_ipc/test_serialize.py +++ b/cuda_core/tests/memory_ipc/test_serialize.py @@ -34,7 +34,7 @@ def test_main(self, ipc_device, ipc_memory_resource): process.start() # Send a memory resource by allocation handle. - alloc_handle = mr.get_allocation_handle() + alloc_handle = mr.allocation_handle mp.reduction.send_handle(parent_conn, alloc_handle.handle, process.pid) # Send a buffer. @@ -42,7 +42,7 @@ def test_main(self, ipc_device, ipc_memory_resource): parent_conn.send(buffer1) # directly buffer2 = mr.allocate(NBYTES) - parent_conn.send(buffer2.get_ipc_descriptor()) # by descriptor + parent_conn.send(buffer2.ipc_descriptor) # by descriptor # Wait for the child process. process.join(timeout=CHILD_TIMEOUT_SEC) @@ -140,9 +140,9 @@ def test_main(self, ipc_device, ipc_memory_resource): # Define the objects. device = ipc_device mr = ipc_memory_resource - alloc_handle = mr.get_allocation_handle() + alloc_handle = mr.allocation_handle buffer = mr.allocate(NBYTES) - buffer_desc = buffer.get_ipc_descriptor() + buffer_desc = buffer.ipc_descriptor pgen = PatternGen(device, NBYTES) pgen.fill_buffer(buffer, seed=False) diff --git a/cuda_core/tests/memory_ipc/test_workerpool.py b/cuda_core/tests/memory_ipc/test_workerpool.py index 1fa235a4c97..cfaa776ac9e 100644 --- a/cuda_core/tests/memory_ipc/test_workerpool.py +++ b/cuda_core/tests/memory_ipc/test_workerpool.py @@ -82,7 +82,7 @@ def test_main(self, ipc_device, nmrs): with mp.Pool(NWORKERS, initializer=self.init_worker, initargs=(mrs,)) as pool: pool.starmap( self.process_buffer, - [(mrs.index(buffer.memory_resource), buffer.get_ipc_descriptor()) for buffer in buffers], + [(mrs.index(buffer.memory_resource), buffer.ipc_descriptor) for buffer in buffers], ) pgen = PatternGen(device, NBYTES) diff --git a/cuda_core/tests/test_event.py b/cuda_core/tests/test_event.py index 9c1b0d1e28f..4870c5081e7 100644 --- a/cuda_core/tests/test_event.py +++ b/cuda_core/tests/test_event.py @@ -23,7 +23,7 @@ def test_event_init_disabled(): @pytest.mark.skipif(Device().compute_capability.major < 7, reason="__nanosleep is only available starting Volta (sm70)") def test_timing_success(init_cuda): - options = EventOptions(enable_timing=True) + options = EventOptions(timing_enabled=True) device = Device() stream = device.create_stream() @@ -48,20 +48,20 @@ def test_timing_success(init_cuda): assert elapsed_time_ms > 10 -def test_is_sync_busy_waited(init_cuda): - options = EventOptions(enable_timing=False, busy_waited_sync=True) +def test_is_blocking_sync(init_cuda): + options = EventOptions(timing_enabled=False, blocking_sync=True) stream = Device().create_stream() event = stream.record(options=options) - assert event.is_sync_busy_waited is True + assert event.is_blocking_sync is True - options = EventOptions(enable_timing=False) + options = EventOptions(timing_enabled=False) stream = Device().create_stream() event = stream.record(options=options) - assert event.is_sync_busy_waited is False + assert event.is_blocking_sync is False def test_sync(init_cuda): - options = EventOptions(enable_timing=False) + options = EventOptions(timing_enabled=False) stream = Device().create_stream() event = stream.record(options=options) event.sync() @@ -69,7 +69,7 @@ def test_sync(init_cuda): def test_is_done(init_cuda): - options = EventOptions(enable_timing=False) + options = EventOptions(timing_enabled=False) stream = Device().create_stream() event = stream.record(options=options) # Without a sync, the captured work might not have yet completed @@ -80,14 +80,14 @@ def test_is_done(init_cuda): def test_error_timing_disabled(): device = Device() device.set_current() - enabled = EventOptions(enable_timing=True) - disabled = EventOptions(enable_timing=False) + enabled = EventOptions(timing_enabled=True) + disabled = EventOptions(timing_enabled=False) stream = device.create_stream() event1 = stream.record(options=enabled) event2 = stream.record(options=disabled) - assert not event1.is_timing_disabled - assert event2.is_timing_disabled + assert event1.is_timing_enabled + assert not event2.is_timing_enabled stream.sync() with pytest.raises(RuntimeError, match="^Both Events must be created with timing enabled"): event2 - event1 @@ -102,7 +102,7 @@ def test_error_timing_disabled(): def test_error_timing_recorded(): device = Device() device.set_current() - enabled = EventOptions(enable_timing=True) + enabled = EventOptions(timing_enabled=True) stream = device.create_stream() event1 = stream.record(options=enabled) @@ -123,7 +123,7 @@ def test_error_timing_incomplete(): device = Device() device.set_current() latch = LatchKernel(device) - enabled = EventOptions(enable_timing=True) + enabled = EventOptions(timing_enabled=True) stream = device.create_stream() event1 = stream.record(options=enabled) @@ -213,13 +213,13 @@ def test_event_rsub_not_implemented(init_cuda): assert result is NotImplemented -def test_event_get_ipc_descriptor_non_ipc(init_cuda): - """get_ipc_descriptor raises RuntimeError on a non-IPC event.""" +def test_event_ipc_descriptor_non_ipc(init_cuda): + """ipc_descriptor raises RuntimeError on a non-IPC event.""" device = Device() stream = device.create_stream() event = stream.record() with pytest.raises(RuntimeError, match="not IPC-enabled"): - event.get_ipc_descriptor() + _ = event.ipc_descriptor def test_event_is_done_false(init_cuda): diff --git a/cuda_core/tests/test_launcher.py b/cuda_core/tests/test_launcher.py index 7bd480edf23..b3461f5a371 100644 --- a/cuda_core/tests/test_launcher.py +++ b/cuda_core/tests/test_launcher.py @@ -262,7 +262,7 @@ def test_cooperative_launch(): prog = Program(code, code_type="c++", options=pro_opts) ker = prog.compile("cubin").get_kernel("test_grid_sync") - # # Launch without setting cooperative_launch + # # Launch without setting is_cooperative # # Commented out as this seems to be a sticky error... # config = LaunchConfig(grid=1, block=1) # launch(s, config, ker) @@ -273,12 +273,12 @@ def test_cooperative_launch(): # Crazy grid sizes would not work block = 128 - config = LaunchConfig(grid=dev.properties.max_grid_dim_x // block + 1, block=block, cooperative_launch=True) + config = LaunchConfig(grid=dev.properties.max_grid_dim_x // block + 1, block=block, is_cooperative=True) with pytest.raises(ValueError): launch(s, config, ker) # This works just fine - config = LaunchConfig(grid=1, block=1, cooperative_launch=True) + config = LaunchConfig(grid=1, block=1, is_cooperative=True) launch(s, config, ker) s.sync() diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index 57de22bb9a0..85dd4a7ea2b 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -1223,10 +1223,10 @@ def test_mempool_ipc_errors(mempool_device): ipc_error_msg = "Memory resource is not IPC-enabled" with pytest.raises(RuntimeError, match=ipc_error_msg): - mr.get_allocation_handle() + _ = mr.allocation_handle with pytest.raises(RuntimeError, match=ipc_error_msg): - buffer.get_ipc_descriptor() + _ = buffer.ipc_descriptor with pytest.raises(RuntimeError, match=ipc_error_msg): handle = IPCBufferDescriptor._init(b"", 0) @@ -1258,7 +1258,7 @@ def test_pinned_mempool_ipc_basic(): assert mr.numa_id >= 0 # IPC requires a concrete NUMA node # Test allocation handle export - alloc_handle = mr.get_allocation_handle() + alloc_handle = mr.allocation_handle assert alloc_handle is not None # Test buffer allocation @@ -1268,7 +1268,7 @@ def test_pinned_mempool_ipc_basic(): assert buffer.is_host_accessible # Test IPC descriptor - ipc_desc = buffer.get_ipc_descriptor() + ipc_desc = buffer.ipc_descriptor assert ipc_desc is not None assert ipc_desc.size == 1024 @@ -1294,10 +1294,10 @@ def test_pinned_mempool_ipc_errors(): ipc_error_msg = "Memory resource is not IPC-enabled" with pytest.raises(RuntimeError, match=ipc_error_msg): - mr.get_allocation_handle() + _ = mr.allocation_handle with pytest.raises(RuntimeError, match=ipc_error_msg): - buffer.get_ipc_descriptor() + _ = buffer.ipc_descriptor with pytest.raises(RuntimeError, match=ipc_error_msg): handle = IPCBufferDescriptor._init(b"", 0) diff --git a/cuda_core/tests/test_module.py b/cuda_core/tests/test_module.py index d3ffa0ca2b6..58f09564971 100644 --- a/cuda_core/tests/test_module.py +++ b/cuda_core/tests/test_module.py @@ -57,6 +57,31 @@ def test_kernel_attributes_init_disabled(): cuda.core._module.KernelAttributes() # Ensure back door is locked. +def test_kernel_attributes_per_device_view(get_saxpy_kernel_cubin): + """kernel.attributes[device] returns a per-device view; values match.""" + kernel, _ = get_saxpy_kernel_cubin + dev = Device() + + default_view = kernel.attributes + int_view = kernel.attributes[dev.device_id] + dev_view = kernel.attributes[dev] + + # Same value via every access path (default view = current device). + assert default_view.num_regs == int_view.num_regs == dev_view.num_regs + assert default_view.max_threads_per_block == int_view.max_threads_per_block + + # The bound views are distinct objects from the default view. + assert int_view is not default_view + assert int_view is not dev_view + + +def test_kernel_attributes_indexing_rejects_invalid_device(get_saxpy_kernel_cubin): + """kernel.attributes[bad] raises through the Device(...) constructor.""" + kernel, _ = get_saxpy_kernel_cubin + with pytest.raises((TypeError, ValueError, OverflowError)): + kernel.attributes["not a device"] + + def test_kernel_occupancy_init_disabled(): with pytest.raises(RuntimeError, match=r"^KernelOccupancy cannot be instantiated directly\."): cuda.core._module.KernelOccupancy() # Ensure back door is locked. @@ -182,16 +207,18 @@ def test_get_kernel(init_cuda): ) def test_read_only_kernel_attributes(get_saxpy_kernel_cubin, attr, expected_type): kernel, _ = get_saxpy_kernel_cubin - method = getattr(kernel.attributes, attr) - # get the value without providing a device ordinal - value = method() + + # Default view: property access on the current-device view. + value = getattr(kernel.attributes, attr) assert value is not None + assert isinstance(value, expected_type), f"Expected {attr} to be of type {expected_type}, but got {type(value)}" - # get the value for each device on the system, using either the device object or ordinal + # Per-device views via __getitem__: each device, both Device and ordinal forms. for device in Device.get_all_devices(): - value = method(device) - value = method(device.device_id) - assert isinstance(value, expected_type), f"Expected {attr} to be of type {expected_type}, but got {type(value)}" + value = getattr(kernel.attributes[device], attr) + assert isinstance(value, expected_type), f"Expected {attr} to be of type {expected_type}, but got {type(value)}" + value = getattr(kernel.attributes[device.device_id], attr) + assert isinstance(value, expected_type), f"Expected {attr} to be of type {expected_type}, but got {type(value)}" def test_object_code_load_ptx(get_saxpy_kernel_ptx): @@ -384,7 +411,7 @@ def test_occupancy_max_active_block_per_multiprocessor(get_saxpy_kernel_cubin, b kernel_smem_size_per_sm = num_blocks_per_sm * smem_size_per_block assert kernel_threads_per_sm <= dev_props.max_threads_per_multiprocessor assert kernel_smem_size_per_sm <= dev_props.max_shared_memory_per_multiprocessor - assert kernel.attributes.num_regs() * num_blocks_per_sm <= dev_props.max_registers_per_multiprocessor + assert kernel.attributes.num_regs * num_blocks_per_sm <= dev_props.max_registers_per_multiprocessor @pytest.mark.parametrize("block_size_limit", [32, 64, 96, 120, 128, 256, 0]) @@ -506,7 +533,7 @@ def test_kernel_from_handle(get_saxpy_kernel_cubin): assert isinstance(kernel_from_handle, Kernel) # Verify we can access kernel attributes - max_threads = kernel_from_handle.attributes.max_threads_per_block() + max_threads = kernel_from_handle.attributes.max_threads_per_block assert isinstance(max_threads, int) assert max_threads > 0 @@ -524,7 +551,7 @@ def test_kernel_from_handle_no_module(get_saxpy_kernel_cubin): assert isinstance(kernel_from_handle, Kernel) # Verify we can still access kernel attributes - max_threads = kernel_from_handle.attributes.max_threads_per_block() + max_threads = kernel_from_handle.attributes.max_threads_per_block assert isinstance(max_threads, int) assert max_threads > 0 @@ -599,7 +626,7 @@ def test_kernel_from_handle_library_mismatch_warning(init_cuda): assert len(w) == 1 assert "does not match" in str(w[0].message) - assert k.attributes.max_threads_per_block() > 0 + assert k.attributes.max_threads_per_block > 0 def test_kernel_from_handle_foreign_kernel(init_cuda): @@ -615,7 +642,7 @@ def test_kernel_from_handle_foreign_kernel(init_cuda): handle = int(cu_kernel) k = Kernel.from_handle(handle) - assert k.attributes.max_threads_per_block() > 0 + assert k.attributes.max_threads_per_block > 0 def test_kernel_keeps_library_alive(init_cuda): diff --git a/cuda_core/tests/test_multiprocessing_warning.py b/cuda_core/tests/test_multiprocessing_warning.py index 0eb47daaee4..0f96e0abfbc 100644 --- a/cuda_core/tests/test_multiprocessing_warning.py +++ b/cuda_core/tests/test_multiprocessing_warning.py @@ -52,7 +52,7 @@ def test_warn_on_fork_method_allocation_handle(ipc_device): device.set_current() options = DeviceMemoryResourceOptions(max_size=2097152, ipc_enabled=True) mr = DeviceMemoryResource(device, options=options) - alloc_handle = mr.get_allocation_handle() + alloc_handle = mr.allocation_handle with patch("multiprocessing.get_start_method", return_value="fork"), warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") diff --git a/cuda_core/tests/test_object_protocols.py b/cuda_core/tests/test_object_protocols.py index b9f396310d7..457debc0903 100644 --- a/cuda_core/tests/test_object_protocols.py +++ b/cuda_core/tests/test_object_protocols.py @@ -232,7 +232,7 @@ def sample_ipc_buffer_descriptor(ipc_device): options = DeviceMemoryResourceOptions(max_size=POOL_SIZE, ipc_enabled=True) mr = DeviceMemoryResource(ipc_device, options=options) buf = mr.allocate(64) - return buf.get_ipc_descriptor() + return buf.ipc_descriptor @pytest.fixture @@ -240,7 +240,7 @@ def sample_ipc_event_descriptor(ipc_device): """An IPCEventDescriptor.""" stream = ipc_device.create_stream() e = stream.record(options={"ipc_enabled": True}) - return e.get_ipc_descriptor() + return e.ipc_descriptor # ============================================================================= @@ -278,8 +278,8 @@ def sample_root_node_alt(sample_graphdef_alt): def sample_empty_node(sample_graphdef): """An EmptyNode created by merging two branches.""" _skip_if_no_mempool() - a = sample_graphdef.alloc(ALLOC_SIZE) - b = sample_graphdef.alloc(ALLOC_SIZE) + a = sample_graphdef.allocate(ALLOC_SIZE) + b = sample_graphdef.allocate(ALLOC_SIZE) return sample_graphdef.join(a, b) @@ -287,8 +287,8 @@ def sample_empty_node(sample_graphdef): def sample_empty_node_alt(sample_graphdef): """An alternate EmptyNode from same graph.""" _skip_if_no_mempool() - c = sample_graphdef.alloc(ALLOC_SIZE) - d = sample_graphdef.alloc(ALLOC_SIZE) + c = sample_graphdef.allocate(ALLOC_SIZE) + d = sample_graphdef.allocate(ALLOC_SIZE) return sample_graphdef.join(c, d) @@ -296,14 +296,14 @@ def sample_empty_node_alt(sample_graphdef): def sample_alloc_node(sample_graphdef): """An AllocNode.""" _skip_if_no_mempool() - return sample_graphdef.alloc(ALLOC_SIZE) + return sample_graphdef.allocate(ALLOC_SIZE) @pytest.fixture def sample_alloc_node_alt(sample_graphdef): """An alternate AllocNode from same graph.""" _skip_if_no_mempool() - return sample_graphdef.alloc(ALLOC_SIZE) + return sample_graphdef.allocate(ALLOC_SIZE) @pytest.fixture @@ -328,23 +328,23 @@ def sample_kernel_node_alt(sample_graphdef, init_cuda): def sample_free_node(sample_graphdef): """A FreeNode.""" _skip_if_no_mempool() - alloc = sample_graphdef.alloc(ALLOC_SIZE) - return alloc.free(alloc.dptr) + alloc = sample_graphdef.allocate(ALLOC_SIZE) + return alloc.deallocate(alloc.dptr) @pytest.fixture def sample_free_node_alt(sample_graphdef): """An alternate FreeNode from same graph.""" _skip_if_no_mempool() - alloc = sample_graphdef.alloc(ALLOC_SIZE) - return alloc.free(alloc.dptr) + alloc = sample_graphdef.allocate(ALLOC_SIZE) + return alloc.deallocate(alloc.dptr) @pytest.fixture def sample_memset_node(sample_graphdef): """A MemsetNode.""" _skip_if_no_mempool() - alloc = sample_graphdef.alloc(ALLOC_SIZE) + alloc = sample_graphdef.allocate(ALLOC_SIZE) return alloc.memset(alloc.dptr, 0, ALLOC_SIZE) @@ -352,7 +352,7 @@ def sample_memset_node(sample_graphdef): def sample_memset_node_alt(sample_graphdef): """An alternate MemsetNode from same graph.""" _skip_if_no_mempool() - alloc = sample_graphdef.alloc(ALLOC_SIZE) + alloc = sample_graphdef.allocate(ALLOC_SIZE) return alloc.memset(alloc.dptr, 0, ALLOC_SIZE) @@ -360,8 +360,8 @@ def sample_memset_node_alt(sample_graphdef): def sample_memcpy_node(sample_graphdef): """A MemcpyNode.""" _skip_if_no_mempool() - src = sample_graphdef.alloc(ALLOC_SIZE) - dst = sample_graphdef.alloc(ALLOC_SIZE) + src = sample_graphdef.allocate(ALLOC_SIZE) + dst = sample_graphdef.allocate(ALLOC_SIZE) dep = sample_graphdef.join(src, dst) return dep.memcpy(dst.dptr, src.dptr, ALLOC_SIZE) @@ -370,8 +370,8 @@ def sample_memcpy_node(sample_graphdef): def sample_memcpy_node_alt(sample_graphdef): """An alternate MemcpyNode from same graph.""" _skip_if_no_mempool() - src = sample_graphdef.alloc(ALLOC_SIZE) - dst = sample_graphdef.alloc(ALLOC_SIZE) + src = sample_graphdef.allocate(ALLOC_SIZE) + dst = sample_graphdef.allocate(ALLOC_SIZE) dep = sample_graphdef.join(src, dst) return dep.memcpy(dst.dptr, src.dptr, ALLOC_SIZE) @@ -400,28 +400,28 @@ def sample_child_graph_node_alt(sample_graphdef): def sample_event_record_node(sample_graphdef, sample_device): """An EventRecordNode.""" event = sample_device.create_event() - return sample_graphdef.record_event(event) + return sample_graphdef.record(event) @pytest.fixture def sample_event_record_node_alt(sample_graphdef, sample_device): """An alternate EventRecordNode from same graph.""" event = sample_device.create_event() - return sample_graphdef.record_event(event) + return sample_graphdef.record(event) @pytest.fixture def sample_event_wait_node(sample_graphdef, sample_device): """An EventWaitNode.""" event = sample_device.create_event() - return sample_graphdef.wait_event(event) + return sample_graphdef.wait(event) @pytest.fixture def sample_event_wait_node_alt(sample_graphdef, sample_device): """An alternate EventWaitNode from same graph.""" event = sample_device.create_event() - return sample_graphdef.wait_event(event) + return sample_graphdef.wait(event) @pytest.fixture @@ -460,14 +460,14 @@ def sample_condition_alt(sample_graphdef): def sample_if_node(sample_graphdef): """An IfNode.""" condition = try_create_condition(sample_graphdef) - return sample_graphdef.if_cond(condition) + return sample_graphdef.if_then(condition) @pytest.fixture def sample_if_node_alt(sample_graphdef): """An alternate IfNode from same graph.""" condition = try_create_condition(sample_graphdef) - return sample_graphdef.if_cond(condition) + return sample_graphdef.if_then(condition) @pytest.fixture @@ -670,7 +670,7 @@ def sample_switch_node_alt(sample_graphdef): ( "sample_launch_config", r"LaunchConfig\(grid=\(\d+, \d+, \d+\), cluster=.+, block=\(\d+, \d+, \d+\), " - r"shmem_size=\d+, cooperative_launch=(?:True|False)\)", + r"shmem_size=\d+, is_cooperative=(?:True|False)\)", ), ("sample_kernel", r""), # ObjectCode variations (by code_type) From f77e0c823e3bce21ebd06a4b186ad573dd25cad7 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Thu, 30 Apr 2026 16:01:07 -0400 Subject: [PATCH 053/244] Fix #1840: Remove numba_emm_plugin.py example (#1996) --- cuda_bindings/README.md | 3 - cuda_bindings/docs/source/examples.rst | 3 - .../examples/extra/numba_emm_plugin.py | 165 ------------------ cuda_bindings/tests/test_examples.py | 6 - 4 files changed, 177 deletions(-) delete mode 100644 cuda_bindings/examples/extra/numba_emm_plugin.py diff --git a/cuda_bindings/README.md b/cuda_bindings/README.md index 2a18f5a2df2..cc6a8948db3 100644 --- a/cuda_bindings/README.md +++ b/cuda_bindings/README.md @@ -58,9 +58,6 @@ In addition, extra examples are included: launch a kernel on the device. Includes device memory allocation / deallocation, transfers between host and device, creation and usage of streams, and context management. -* `examples/extra/numba_emm_plugin.py`: Implements a Numba External Memory Management - plugin, showing that this CUDA Python Driver API can coexist with other - wrappers of the driver API. To run these samples: * `python -m pytest tests/cython/` against editable installations diff --git a/cuda_bindings/docs/source/examples.rst b/cuda_bindings/docs/source/examples.rst index 96f790a287a..fb0e12e75d2 100644 --- a/cuda_bindings/docs/source/examples.rst +++ b/cuda_bindings/docs/source/examples.rst @@ -63,6 +63,3 @@ Advanced and interoperability - `jit_program.py `_ JIT-compiles a SAXPY kernel with NVRTC and launches it through the Driver API. -- `numba_emm_plugin.py `_ - shows how to back Numba's EMM interface with the NVIDIA CUDA Python Driver - API. diff --git a/cuda_bindings/examples/extra/numba_emm_plugin.py b/cuda_bindings/examples/extra/numba_emm_plugin.py deleted file mode 100644 index cd8fcfcc55c..00000000000 --- a/cuda_bindings/examples/extra/numba_emm_plugin.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright 2021-2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -# /// script -# dependencies = ["cuda_bindings>13.2.1", "numba-cuda"] -# /// - -"""Numba EMM Plugin using the CUDA Python Driver API. - -This example provides an External Memory Management (EMM) Plugin for Numba (see -https://numba.readthedocs.io/en/stable/cuda/external-memory.html) that uses the -NVIDIA CUDA Python Driver API for all on-device allocations and frees. For -other operations interacting with the driver, Numba uses its internal ctypes -wrapper. This serves as an example of interoperability between the NVIDIA CUDA -Python Driver API, and other implementations of driver API wrappers (in this -case Numba's ctypes wrapper), and demonstrates an on-ramp to using the NVIDIA -CUDA Python Driver API wrapper by showing that it can co-exist with other -wrappers - it is not necessary to replace all wrappers in all libraries to -start using the NVIDIA wrapper. - -The current version of Numba passes all tests using this plugin (with a small -patch to recognize CUDA 11.3 as a supported version). The Numba test suite can -be run with the plugin by executing: - - NUMBA_CUDA_MEMORY_MANAGER=numba_emm_plugin \\ - python -m numba.runtests numba.cuda.tests -vf -m - -when the directory containing this example is on the PYTHONPATH. When tests are -run, the test summary is expected to be close to: - - Ran 1121 tests in 159.572s - - OK (skipped=17, expected failures=1) - -The number of tests may vary with changes between commits in Numba, but the -main result is that there are no unexpected failures. - -This example can also be run standalone with: - - python numba_emm_plugin.py - -in which case it sets up Numba to use the included EMM plugin, then creates and -destroys a device array. When run standalone, the output may look like: - - Free before creating device array: 50781159424 - Free after creating device array: 50779062272 - Free after freeing device array: 50781159424 - -The initial value may vary, but the expectation is that 2097152 bytes (2MB) -should be taken up by the device array creation, and the original value should -be restored after freeing it. -""" - -from ctypes import c_size_t - -from numba.cuda import ( - GetIpcHandleMixin, - HostOnlyCUDAMemoryManager, - MemoryInfo, - MemoryPointer, -) - -from cuda.bindings import driver as cuda -from cuda.bindings import driver as cuda_driver - -# Python functions for allocation, deallocation, and memory info via the NVIDIA -# CUDA Python Driver API - - -def driver_alloc(size): - """ - Allocate `size` bytes of device memory and return a device pointer to the - allocated memory. - """ - err, ptr = cuda_driver.cuMemAlloc(size) - if err != cuda_driver.CUresult.CUDA_SUCCESS: - raise RuntimeError(f"Unexpected error code {err} from cuMemAlloc") - return ptr - - -def driver_free(ptr): - """ - Free device memory pointed to by `ptr`. - """ - (err,) = cuda_driver.cuMemFree(ptr) - if err != cuda_driver.CUresult.CUDA_SUCCESS: - raise RuntimeError(f"Unexpected error code {err} from cuMemFree") - - -def driver_memory_info(): - """ - Return the free and total amount of device memory in bytes as a tuple. - """ - err, free, total = cuda_driver.cuMemGetInfo() - if err != cuda_driver.CUresult.CUDA_SUCCESS: - raise RuntimeError(f"Unexpected error code {err} from cuMemGetInfo") - return free, total - - -# EMM Plugin implementation. For documentation of the methods implemented here, -# see: -# -# https://numba.readthedocs.io/en/stable/cuda/external-memory.html#numba.cuda.BaseCUDAMemoryManager - - -class DriverEMMPlugin(GetIpcHandleMixin, HostOnlyCUDAMemoryManager): - def memalloc(self, size): - ptr = driver_alloc(size) - ctx = self.context - finalizer = make_finalizer(ptr) - # We wrap the pointer value in a c_size_t because Numba expects ctypes - # objects - wrapped_ptr = c_size_t(int(ptr)) - return MemoryPointer(ctx, wrapped_ptr, size, finalizer=finalizer) - - def initialize(self): - # No setup required to use the EMM Plugin in a given context - pass - - def get_memory_info(self): - free, total = driver_memory_info() - return MemoryInfo(free=free, total=total) - - @property - def interface_version(self): - return 1 - - -def make_finalizer(ptr): - def finalizer(): - driver_free(ptr) - - return finalizer - - -# If NUMBA_CUDA_MEMORY_MANAGER is set to this module (e.g. -# `NUMBA_CUDA_MEMORY_MANAGER=numba_emm_plugin`), then Numba will look at the -# _numba_memory_manager global to determine what class to use for memory -# management. - -_numba_memory_manager = DriverEMMPlugin - - -def main(): - """ - A simple test / demonstration setting the memory manager and - allocating/deleting an array. - """ - - cuda.set_memory_manager(DriverEMMPlugin) - ctx = cuda.current_context() - print(f"Free before creating device array: {ctx.get_memory_info().free}") - x = cuda.device_array(1000) - print(f"Free after creating device array: {ctx.get_memory_info().free}") - del x - print(f"Free after freeing device array: {ctx.get_memory_info().free}") - - -if __name__ == "__main__": - import argparse - - formatter = argparse.RawDescriptionHelpFormatter - parser = argparse.ArgumentParser(description=__doc__, formatter_class=formatter) - parser.parse_args() - main() diff --git a/cuda_bindings/tests/test_examples.py b/cuda_bindings/tests/test_examples.py index 171d7d37f2f..0c3efe72811 100644 --- a/cuda_bindings/tests/test_examples.py +++ b/cuda_bindings/tests/test_examples.py @@ -14,14 +14,8 @@ examples_files = glob.glob(os.path.join(examples_path, "**/*.py"), recursive=True) -BROKEN_EXAMPLES = {"numba_emm_plugin.py"} - - @pytest.mark.parametrize("example", examples_files) def test_example(example): - if os.path.basename(example) in BROKEN_EXAMPLES: - pytest.skip(f"Skipping broken example: {example}") - has_package_requirements_or_skip(example) env = os.environ.copy() From 4459b306d73f94c6f9adf2dd6d53dca8cec630c2 Mon Sep 17 00:00:00 2001 From: Daniel Rodriguez Date: Thu, 30 Apr 2026 22:11:40 -0500 Subject: [PATCH 054/244] cuda.bindings benchmarks part 6: Add TMA, pointer attributes and skip benchmarks logic (#1990) * Add TMA, pointer attributes and skip benchmarks logic * linting * Fix tensormap, add full overhead column to compareq * Remove legacy benchmarks * Fail C++ runner when no benchmarks produce output --- benchmarks/cuda_bindings/README.md | 10 +- .../benchmarks/bench_ctx_device.py | 17 + .../cuda_bindings/benchmarks/bench_enum.py | 34 ++ .../benchmarks/bench_pointer_attributes.py | 20 ++ .../benchmarks/bench_tensormap.py | 203 +++++++++++ .../benchmarks/cpp/CMakeLists.txt | 1 + .../benchmarks/cpp/bench_ctx_device.cpp | 19 + .../cpp/bench_pointer_attributes.cpp | 23 ++ .../benchmarks/cpp/bench_tensormap.cpp | 218 ++++++++++++ benchmarks/cuda_bindings/compare.py | 68 ++-- benchmarks/cuda_bindings/pixi.toml | 3 - .../cuda_bindings/pytest-legacy/conftest.py | 93 ----- .../cuda_bindings/pytest-legacy/kernels.py | 159 --------- .../cuda_bindings/pytest-legacy/test_cupy.py | 199 ----------- .../pytest-legacy/test_launch_latency.py | 336 ------------------ .../cuda_bindings/pytest-legacy/test_numba.py | 52 --- .../pytest-legacy/test_pointer_attributes.py | 112 ------ benchmarks/cuda_bindings/runner/cpp.py | 9 + benchmarks/cuda_bindings/runner/main.py | 86 ++++- 19 files changed, 679 insertions(+), 983 deletions(-) create mode 100644 benchmarks/cuda_bindings/benchmarks/bench_enum.py create mode 100644 benchmarks/cuda_bindings/benchmarks/bench_tensormap.py create mode 100644 benchmarks/cuda_bindings/benchmarks/cpp/bench_tensormap.cpp delete mode 100644 benchmarks/cuda_bindings/pytest-legacy/conftest.py delete mode 100644 benchmarks/cuda_bindings/pytest-legacy/kernels.py delete mode 100644 benchmarks/cuda_bindings/pytest-legacy/test_cupy.py delete mode 100755 benchmarks/cuda_bindings/pytest-legacy/test_launch_latency.py delete mode 100644 benchmarks/cuda_bindings/pytest-legacy/test_numba.py delete mode 100644 benchmarks/cuda_bindings/pytest-legacy/test_pointer_attributes.py diff --git a/benchmarks/cuda_bindings/README.md b/benchmarks/cuda_bindings/README.md index cffca57bef3..33b2bf30ce1 100644 --- a/benchmarks/cuda_bindings/README.md +++ b/benchmarks/cuda_bindings/README.md @@ -6,9 +6,13 @@ Driver APIs through cuda.bindings, relative to a similar C++ baseline. The goal is to benchmark how much overhead does the Python layer adds to calling CUDA APIs and what operations are not in our target of less than 1us of overhead. -Each Python benchmark has a C++ counterpart, which is used to compare the -operations. We try to make each implementation perform small operations -and nearly the same work as possible and are run under similar conditions. +Most Python benchmarks have a C++ counterpart that is used as a comparative +baseline. We try to make each implementation perform small operations and +nearly the same work as possible and are run under similar conditions. + +A few benchmarks (e.g. in `bench_enum.py`) are intentionally Python-only +because they measure costs with no direct C++ equivalent — such as enum +construction and member access on `cuda.bindings` enum classes. These are **not** throughput benchmarks to measure the overall performance of kernels and applications. diff --git a/benchmarks/cuda_bindings/benchmarks/bench_ctx_device.py b/benchmarks/cuda_bindings/benchmarks/bench_ctx_device.py index 2e2cd11d93b..7f619ae4a39 100644 --- a/benchmarks/cuda_bindings/benchmarks/bench_ctx_device.py +++ b/benchmarks/cuda_bindings/benchmarks/bench_ctx_device.py @@ -13,6 +13,11 @@ _, DEVICE = cuda.cuDeviceGet(0) ATTRIBUTE = cuda.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR +# Outer retain so the benchmarked retain/release pair just bumps the refcount. +_err, _PRIMARY_CTX = cuda.cuDevicePrimaryCtxRetain(DEVICE) +if _err != cuda.CUresult.CUDA_SUCCESS: + raise RuntimeError(f"cuDevicePrimaryCtxRetain failed during setup: {_err}") + def bench_ctx_get_current(loops: int) -> float: _fn = cuda.cuCtxGetCurrent @@ -60,3 +65,15 @@ def bench_device_get_attribute(loops: int) -> float: for _ in range(loops): _fn(_attr, _dev) return time.perf_counter() - t0 + + +def bench_device_primary_ctx_retain(loops: int) -> float: + _retain = cuda.cuDevicePrimaryCtxRetain + _release = cuda.cuDevicePrimaryCtxRelease + _dev = DEVICE + + t0 = time.perf_counter() + for _ in range(loops): + _retain(_dev) + _release(_dev) + return time.perf_counter() - t0 diff --git a/benchmarks/cuda_bindings/benchmarks/bench_enum.py b/benchmarks/cuda_bindings/benchmarks/bench_enum.py new file mode 100644 index 00000000000..42bfd47ed3e --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/bench_enum.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import time + +from cuda.bindings import driver as cuda + + +def bench_curesult_construction(loops: int) -> float: + _cls = cuda.CUresult + + t0 = time.perf_counter() + for _ in range(loops): + _cls(0) + return time.perf_counter() - t0 + + +def bench_curesult_member_access(loops: int) -> float: + _cls = cuda.CUresult + + t0 = time.perf_counter() + for _ in range(loops): + _cls.CUDA_SUCCESS # noqa: B018 + return time.perf_counter() - t0 + + +def bench_device_attribute_construction(loops: int) -> float: + _cls = cuda.CUdevice_attribute + + t0 = time.perf_counter() + for _ in range(loops): + _cls(1) + return time.perf_counter() - t0 diff --git a/benchmarks/cuda_bindings/benchmarks/bench_pointer_attributes.py b/benchmarks/cuda_bindings/benchmarks/bench_pointer_attributes.py index 191da263ee5..3cc7ad015c1 100644 --- a/benchmarks/cuda_bindings/benchmarks/bench_pointer_attributes.py +++ b/benchmarks/cuda_bindings/benchmarks/bench_pointer_attributes.py @@ -12,6 +12,14 @@ PTR = alloc_persistent(1 << 18) ATTRIBUTE = cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_MEMORY_TYPE +ATTRIBUTES = ( + cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_MEMORY_TYPE, + cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_DEVICE_POINTER, + cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_HOST_POINTER, + cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_BUFFER_ID, +) +NUM_ATTRIBUTES = len(ATTRIBUTES) + def bench_pointer_get_attribute(loops: int) -> float: # Local references to avoid global lookups in the hot loop @@ -23,3 +31,15 @@ def bench_pointer_get_attribute(loops: int) -> float: for _ in range(loops): _fn(_attr, _ptr) return time.perf_counter() - t0 + + +def bench_pointer_get_attributes(loops: int) -> float: + _fn = cuda.cuPointerGetAttributes + _num = NUM_ATTRIBUTES + _attrs = ATTRIBUTES + _ptr = PTR + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_num, _attrs, _ptr) + return time.perf_counter() - t0 diff --git a/benchmarks/cuda_bindings/benchmarks/bench_tensormap.py b/benchmarks/cuda_bindings/benchmarks/bench_tensormap.py new file mode 100644 index 00000000000..e2a596820b7 --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/bench_tensormap.py @@ -0,0 +1,203 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import time + +from runner.runtime import alloc_persistent, ensure_context + +from cuda.bindings import driver as cuda + +ensure_context() + +PTR = alloc_persistent(1 << 20) + +cuuint32_t = cuda.cuuint32_t +cuuint64_t = cuda.cuuint64_t + +# Tiled: rank-2 float32, 128x128, 64x64 tile. +TILED_DTYPE = cuda.CUtensorMapDataType.CU_TENSOR_MAP_DATA_TYPE_FLOAT32 +TILED_RANK = 2 +TILED_GLOBAL_DIM = (cuuint64_t(128), cuuint64_t(128)) +TILED_GLOBAL_STRIDES = (cuuint64_t(128 * 4),) +TILED_BOX_DIM = (cuuint32_t(64), cuuint32_t(64)) +TILED_ELEMENT_STRIDES = (cuuint32_t(1), cuuint32_t(1)) +TILED_INTERLEAVE = cuda.CUtensorMapInterleave.CU_TENSOR_MAP_INTERLEAVE_NONE +TILED_SWIZZLE = cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_NONE +TILED_L2 = cuda.CUtensorMapL2promotion.CU_TENSOR_MAP_L2_PROMOTION_NONE +TILED_OOB = cuda.CUtensorMapFloatOOBfill.CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE + +# Im2col: rank-3 float16, 32x64x64. +IM2COL_DTYPE = cuda.CUtensorMapDataType.CU_TENSOR_MAP_DATA_TYPE_FLOAT16 +IM2COL_RANK = 3 +IM2COL_GLOBAL_DIM = (cuuint64_t(32), cuuint64_t(64), cuuint64_t(64)) +IM2COL_GLOBAL_STRIDES = (cuuint64_t(32 * 2), cuuint64_t(32 * 64 * 2)) +IM2COL_PIXEL_BOX_LOWER = (0,) +IM2COL_PIXEL_BOX_UPPER = (0,) +IM2COL_CHANNELS = 32 +IM2COL_PIXELS = 32 +IM2COL_ELEMENT_STRIDES = (cuuint32_t(1), cuuint32_t(1), cuuint32_t(1)) +IM2COL_INTERLEAVE = cuda.CUtensorMapInterleave.CU_TENSOR_MAP_INTERLEAVE_NONE +IM2COL_SWIZZLE = cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_NONE +IM2COL_L2 = cuda.CUtensorMapL2promotion.CU_TENSOR_MAP_L2_PROMOTION_NONE +IM2COL_OOB = cuda.CUtensorMapFloatOOBfill.CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE + +_SUCCESS = cuda.CUresult.CUDA_SUCCESS + +# Resolve bindings once at module load. A missing attribute (old binding that +# predates a TMA API) is the only legitimate reason for a probe to skip — +# everything else (signature mismatches, unexpected TypeError, etc.) should +# surface loudly instead of being reclassified as "unsupported". +_ENCODE_TILED = getattr(cuda, "cuTensorMapEncodeTiled", None) +_ENCODE_IM2COL = getattr(cuda, "cuTensorMapEncodeIm2col", None) +_ENCODE_IM2COL_WIDE = getattr(cuda, "cuTensorMapEncodeIm2colWide", None) +_IM2COL_WIDE_MODE_CLS = getattr(cuda, "CUtensorMapIm2ColWideMode", None) + + +def _probe_tiled() -> bool: + if _ENCODE_TILED is None: + return False + err, _ = _ENCODE_TILED( + TILED_DTYPE, + TILED_RANK, + PTR, + TILED_GLOBAL_DIM, + TILED_GLOBAL_STRIDES, + TILED_BOX_DIM, + TILED_ELEMENT_STRIDES, + TILED_INTERLEAVE, + TILED_SWIZZLE, + TILED_L2, + TILED_OOB, + ) + return err == _SUCCESS + + +def _probe_im2col() -> bool: + if _ENCODE_IM2COL is None: + return False + err, _ = _ENCODE_IM2COL( + IM2COL_DTYPE, + IM2COL_RANK, + PTR, + IM2COL_GLOBAL_DIM, + IM2COL_GLOBAL_STRIDES, + IM2COL_PIXEL_BOX_LOWER, + IM2COL_PIXEL_BOX_UPPER, + IM2COL_CHANNELS, + IM2COL_PIXELS, + IM2COL_ELEMENT_STRIDES, + IM2COL_INTERLEAVE, + IM2COL_SWIZZLE, + IM2COL_L2, + IM2COL_OOB, + ) + return err == _SUCCESS + + +def _probe_im2col_wide() -> bool: + if _ENCODE_IM2COL_WIDE is None or _IM2COL_WIDE_MODE_CLS is None: + return False + mode = _IM2COL_WIDE_MODE_CLS.CU_TENSOR_MAP_IM2COL_WIDE_MODE_W + err, _ = _ENCODE_IM2COL_WIDE( + IM2COL_DTYPE, + IM2COL_RANK, + PTR, + IM2COL_GLOBAL_DIM, + IM2COL_GLOBAL_STRIDES, + 0, + 0, + IM2COL_CHANNELS, + IM2COL_PIXELS, + IM2COL_ELEMENT_STRIDES, + IM2COL_INTERLEAVE, + mode, + cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_128B, + IM2COL_L2, + IM2COL_OOB, + ) + return err == _SUCCESS + + +_TILED_OK = _probe_tiled() +_IM2COL_OK = _probe_im2col() +_IM2COL_WIDE_OK = _probe_im2col_wide() + +if _IM2COL_WIDE_OK: + _IM2COL_WIDE_MODE_W = _IM2COL_WIDE_MODE_CLS.CU_TENSOR_MAP_IM2COL_WIDE_MODE_W + _IM2COL_WIDE_SWIZZLE = cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_128B + +SKIPPED_BENCHMARKS: set[str] = set() +if not _TILED_OK: + SKIPPED_BENCHMARKS.add("bench_tensor_map_encode_tiled") +if not _IM2COL_OK: + SKIPPED_BENCHMARKS.add("bench_tensor_map_encode_im2col") +if not _IM2COL_WIDE_OK: + SKIPPED_BENCHMARKS.add("bench_tensor_map_encode_im2col_wide") + + +def bench_tensor_map_encode_tiled(loops: int) -> float: + _fn = cuda.cuTensorMapEncodeTiled + _dt = TILED_DTYPE + _rank = TILED_RANK + _addr = PTR + _gdim = TILED_GLOBAL_DIM + _gstr = TILED_GLOBAL_STRIDES + _bdim = TILED_BOX_DIM + _estr = TILED_ELEMENT_STRIDES + _inter = TILED_INTERLEAVE + _swz = TILED_SWIZZLE + _l2 = TILED_L2 + _oob = TILED_OOB + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_dt, _rank, _addr, _gdim, _gstr, _bdim, _estr, _inter, _swz, _l2, _oob) + return time.perf_counter() - t0 + + +def bench_tensor_map_encode_im2col(loops: int) -> float: + _fn = cuda.cuTensorMapEncodeIm2col + _dt = IM2COL_DTYPE + _rank = IM2COL_RANK + _addr = PTR + _gdim = IM2COL_GLOBAL_DIM + _gstr = IM2COL_GLOBAL_STRIDES + _lower = IM2COL_PIXEL_BOX_LOWER + _upper = IM2COL_PIXEL_BOX_UPPER + _ch = IM2COL_CHANNELS + _px = IM2COL_PIXELS + _estr = IM2COL_ELEMENT_STRIDES + _inter = IM2COL_INTERLEAVE + _swz = IM2COL_SWIZZLE + _l2 = IM2COL_L2 + _oob = IM2COL_OOB + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_dt, _rank, _addr, _gdim, _gstr, _lower, _upper, _ch, _px, _estr, _inter, _swz, _l2, _oob) + return time.perf_counter() - t0 + + +def bench_tensor_map_encode_im2col_wide(loops: int) -> float: + _fn = _ENCODE_IM2COL_WIDE + _dt = IM2COL_DTYPE + _rank = IM2COL_RANK + _addr = PTR + _gdim = IM2COL_GLOBAL_DIM + _gstr = IM2COL_GLOBAL_STRIDES + _lower_w = 0 + _upper_w = 0 + _ch = IM2COL_CHANNELS + _px = IM2COL_PIXELS + _estr = IM2COL_ELEMENT_STRIDES + _inter = IM2COL_INTERLEAVE + _mode = _IM2COL_WIDE_MODE_W + _swz = _IM2COL_WIDE_SWIZZLE + _l2 = IM2COL_L2 + _oob = IM2COL_OOB + + t0 = time.perf_counter() + for _ in range(loops): + _fn(_dt, _rank, _addr, _gdim, _gstr, _lower_w, _upper_w, _ch, _px, _estr, _inter, _mode, _swz, _l2, _oob) + return time.perf_counter() - t0 diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt b/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt index bc6541e5bb7..9811ec1f099 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt +++ b/benchmarks/cuda_bindings/benchmarks/cpp/CMakeLists.txt @@ -83,6 +83,7 @@ add_driver_benchmark(bench_ctx_device) add_driver_benchmark(bench_stream) add_driver_benchmark(bench_event) add_driver_benchmark(bench_memory) +add_driver_benchmark(bench_tensormap) # NVRTC benchmarks (require nvrtc for kernel compilation) if(NVRTC_INCLUDE_DIR AND NVRTC_LIBRARY) diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_ctx_device.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_ctx_device.cpp index 052df9cc1dd..29358867186 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_ctx_device.cpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_ctx_device.cpp @@ -77,6 +77,25 @@ int main(int argc, char** argv) { }); } + // --- device_primary_ctx_retain --- + // Outer retain so the benchmarked retain/release pair just bumps the refcount. + CUcontext primary_outer = nullptr; + check_cu( + cuDevicePrimaryCtxRetain(&primary_outer, device), + "cuDevicePrimaryCtxRetain (setup) failed" + ); + { + CUcontext primary = nullptr; + suite.run("ctx_device.device_primary_ctx_retain", [&]() { + check_cu(cuDevicePrimaryCtxRetain(&primary, device), "cuDevicePrimaryCtxRetain failed"); + check_cu(cuDevicePrimaryCtxRelease(device), "cuDevicePrimaryCtxRelease failed"); + }); + } + check_cu( + cuDevicePrimaryCtxRelease(device), + "cuDevicePrimaryCtxRelease (teardown) failed" + ); + // Cleanup check_cu(cuCtxDestroy(ctx), "cuCtxDestroy failed"); diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_pointer_attributes.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_pointer_attributes.cpp index 4d9afc6566e..6c6f42a58e1 100644 --- a/benchmarks/cuda_bindings/benchmarks/cpp/bench_pointer_attributes.cpp +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_pointer_attributes.cpp @@ -49,6 +49,29 @@ int main(int argc, char** argv) { }); } + // --- pointer_get_attributes --- + { + unsigned int memory_type = 0; + CUdeviceptr dev_ptr_out = 0; + void* host_ptr_out = nullptr; + unsigned long long buffer_id = 0; + + CUpointer_attribute attrs[4] = { + CU_POINTER_ATTRIBUTE_MEMORY_TYPE, + CU_POINTER_ATTRIBUTE_DEVICE_POINTER, + CU_POINTER_ATTRIBUTE_HOST_POINTER, + CU_POINTER_ATTRIBUTE_BUFFER_ID, + }; + void* data[4] = {&memory_type, &dev_ptr_out, &host_ptr_out, &buffer_id}; + + suite.run("pointer_attributes.pointer_get_attributes", [&]() { + check_cu( + cuPointerGetAttributes(4, attrs, data, ptr), + "cuPointerGetAttributes failed" + ); + }); + } + // Cleanup check_cu(cuMemFree(ptr), "cuMemFree failed"); check_cu(cuCtxDestroy(ctx), "cuCtxDestroy failed"); diff --git a/benchmarks/cuda_bindings/benchmarks/cpp/bench_tensormap.cpp b/benchmarks/cuda_bindings/benchmarks/cpp/bench_tensormap.cpp new file mode 100644 index 00000000000..d2ea5d6b20c --- /dev/null +++ b/benchmarks/cuda_bindings/benchmarks/cpp/bench_tensormap.cpp @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "bench_support.hpp" + +#include +#include + + +static void check_cu(CUresult status, const char* message) { + if (status != CUDA_SUCCESS) { + const char* error_name = nullptr; + cuGetErrorName(status, &error_name); + std::cerr << message << ": " << (error_name ? error_name : "unknown") << '\n'; + std::exit(1); + } +} + + +int main(int argc, char** argv) { + bench::Options options = bench::parse_args(argc, argv); + + // Setup: init CUDA, allocate device memory + check_cu(cuInit(0), "cuInit failed"); + + CUdevice device; + check_cu(cuDeviceGet(&device, 0), "cuDeviceGet failed"); + + CUcontext ctx; + CUctxCreateParams ctxParams = {}; + check_cu(cuCtxCreate(&ctx, &ctxParams, 0, device), "cuCtxCreate failed"); + + CUdeviceptr ptr; + check_cu(cuMemAlloc(&ptr, 1 << 20), "cuMemAlloc failed"); + + // CUtensorMap must be 64-byte aligned. + alignas(64) CUtensorMap tensor_map{}; + + bench::BenchmarkSuite suite(options); + + // --- tensor_map_encode_tiled --- + { + const cuuint64_t global_dim[2] = {128ull, 128ull}; + const cuuint64_t global_strides[1] = {128ull * 4ull}; + const cuuint32_t box_dim[2] = {64u, 64u}; + const cuuint32_t element_strides[2] = {1u, 1u}; + + CUresult probe = cuTensorMapEncodeTiled( + &tensor_map, + CU_TENSOR_MAP_DATA_TYPE_FLOAT32, + 2u, + reinterpret_cast(ptr), + global_dim, + global_strides, + box_dim, + element_strides, + CU_TENSOR_MAP_INTERLEAVE_NONE, + CU_TENSOR_MAP_SWIZZLE_NONE, + CU_TENSOR_MAP_L2_PROMOTION_NONE, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE + ); + if (probe == CUDA_SUCCESS) { + suite.run("tensormap.tensor_map_encode_tiled", [&]() { + check_cu( + cuTensorMapEncodeTiled( + &tensor_map, + CU_TENSOR_MAP_DATA_TYPE_FLOAT32, + 2u, + reinterpret_cast(ptr), + global_dim, + global_strides, + box_dim, + element_strides, + CU_TENSOR_MAP_INTERLEAVE_NONE, + CU_TENSOR_MAP_SWIZZLE_NONE, + CU_TENSOR_MAP_L2_PROMOTION_NONE, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE + ), + "cuTensorMapEncodeTiled failed" + ); + }); + } else { + const char* err_name = nullptr; + cuGetErrorName(probe, &err_name); + std::cerr << "Skipping tensormap.tensor_map_encode_tiled: " + << (err_name ? err_name : "unknown") << '\n'; + } + } + + // --- tensor_map_encode_im2col --- + { + const cuuint64_t global_dim[3] = {32ull, 64ull, 64ull}; + const cuuint64_t global_strides[2] = {32ull * 2ull, 32ull * 64ull * 2ull}; + const int pixel_box_lower[1] = {0}; + const int pixel_box_upper[1] = {0}; + const cuuint32_t element_strides[3] = {1u, 1u, 1u}; + + CUresult probe = cuTensorMapEncodeIm2col( + &tensor_map, + CU_TENSOR_MAP_DATA_TYPE_FLOAT16, + 3u, + reinterpret_cast(ptr), + global_dim, + global_strides, + pixel_box_lower, + pixel_box_upper, + 32u, + 32u, + element_strides, + CU_TENSOR_MAP_INTERLEAVE_NONE, + CU_TENSOR_MAP_SWIZZLE_NONE, + CU_TENSOR_MAP_L2_PROMOTION_NONE, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE + ); + if (probe == CUDA_SUCCESS) { + suite.run("tensormap.tensor_map_encode_im2col", [&]() { + check_cu( + cuTensorMapEncodeIm2col( + &tensor_map, + CU_TENSOR_MAP_DATA_TYPE_FLOAT16, + 3u, + reinterpret_cast(ptr), + global_dim, + global_strides, + pixel_box_lower, + pixel_box_upper, + 32u, + 32u, + element_strides, + CU_TENSOR_MAP_INTERLEAVE_NONE, + CU_TENSOR_MAP_SWIZZLE_NONE, + CU_TENSOR_MAP_L2_PROMOTION_NONE, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE + ), + "cuTensorMapEncodeIm2col failed" + ); + }); + } else { + const char* err_name = nullptr; + cuGetErrorName(probe, &err_name); + std::cerr << "Skipping tensormap.tensor_map_encode_im2col: " + << (err_name ? err_name : "unknown") << '\n'; + } + } + +#if defined(CUDA_VERSION) && CUDA_VERSION >= 12080 + // --- tensor_map_encode_im2col_wide --- + { + const cuuint64_t global_dim[3] = {32ull, 64ull, 64ull}; + const cuuint64_t global_strides[2] = {32ull * 2ull, 32ull * 64ull * 2ull}; + const cuuint32_t element_strides[3] = {1u, 1u, 1u}; + + CUresult probe = cuTensorMapEncodeIm2colWide( + &tensor_map, + CU_TENSOR_MAP_DATA_TYPE_FLOAT16, + 3u, + reinterpret_cast(ptr), + global_dim, + global_strides, + 0, + 0, + 32u, + 32u, + element_strides, + CU_TENSOR_MAP_INTERLEAVE_NONE, + CU_TENSOR_MAP_IM2COL_WIDE_MODE_W, + CU_TENSOR_MAP_SWIZZLE_128B, + CU_TENSOR_MAP_L2_PROMOTION_NONE, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE + ); + + if (probe == CUDA_SUCCESS) { + suite.run("tensormap.tensor_map_encode_im2col_wide", [&]() { + check_cu( + cuTensorMapEncodeIm2colWide( + &tensor_map, + CU_TENSOR_MAP_DATA_TYPE_FLOAT16, + 3u, + reinterpret_cast(ptr), + global_dim, + global_strides, + 0, + 0, + 32u, + 32u, + element_strides, + CU_TENSOR_MAP_INTERLEAVE_NONE, + CU_TENSOR_MAP_IM2COL_WIDE_MODE_W, + CU_TENSOR_MAP_SWIZZLE_128B, + CU_TENSOR_MAP_L2_PROMOTION_NONE, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE + ), + "cuTensorMapEncodeIm2colWide failed" + ); + }); + } else { + const char* err_name = nullptr; + cuGetErrorName(probe, &err_name); + std::cerr << "Skipping tensormap.tensor_map_encode_im2col_wide: " + << (err_name ? err_name : "unknown") << '\n'; + } + } +#else + std::cerr << "Skipping tensormap.tensor_map_encode_im2col_wide: " + "built against CUDA_VERSION < 12080.\n"; +#endif + + // Cleanup + check_cu(cuMemFree(ptr), "cuMemFree failed"); + check_cu(cuCtxDestroy(ctx), "cuCtxDestroy failed"); + + suite.write(); + + return 0; +} diff --git a/benchmarks/cuda_bindings/compare.py b/benchmarks/cuda_bindings/compare.py index cb1c2fdede3..ca1b398ceb2 100644 --- a/benchmarks/cuda_bindings/compare.py +++ b/benchmarks/cuda_bindings/compare.py @@ -51,10 +51,23 @@ def fmt_rsd(rsd: float | None) -> str: def fmt_ns(seconds: float) -> str: - ns = seconds * 1e9 - if ns >= 1000: - return f"{ns / 1000:.2f} us" - return f"{ns:.0f} ns" + """Format a duration in nanoseconds with a thousands separator. + + Using a single unit across the whole table makes side-by-side comparison + easier, even when some entries get into the multi-microsecond range. + """ + return f"{seconds * 1e9:,.0f}" + + +def fmt_overhead_ns(py_mean: float, cpp_mean: float) -> str: + return f"{(py_mean - cpp_mean) * 1e9:+,.0f}" + + +def fmt_overhead_pct(py_mean: float, cpp_mean: float) -> str: + if cpp_mean <= 0.0: + return "-" + pct = (py_mean - cpp_mean) / cpp_mean * 100 + return f"{pct:+,.0f}%" def main() -> None: @@ -90,22 +103,29 @@ def main() -> None: name_width = max(len(n) for n in all_names) name_width = max(name_width, len("Benchmark")) + # Right-aligned numeric columns. Widths chosen so header text fits and + # multi-microsecond ns values with thousands separators still align. + cpp_w = 12 + py_w = 12 + rsd_w = 8 + oh_ns_w = 12 + oh_pct_w = 10 + # Header if cpp_benchmarks: header = ( - f"{'Benchmark':<{name_width}} {'C++ (mean)':>12} {'C++ RSD':>8} " - f"{'Python (mean)':>14} {'Py RSD':>7} {'Overhead':>10}" + f"{'Benchmark':<{name_width}} " + f"{'C++ (ns)':>{cpp_w}} {'C++ RSD':>{rsd_w}} " + f"{'Python (ns)':>{py_w}} {'Py RSD':>{rsd_w}} " + f"{'Overhead ns':>{oh_ns_w}} {'Overhead %':>{oh_pct_w}}" ) - sep = "-" * len(header) - print(sep) - print(header) - print(sep) else: - header = f"{'Benchmark':<{name_width}} {'Python (mean)':>14} {'Py RSD':>7}" - sep = "-" * len(header) - print(sep) - print(header) - print(sep) + header = f"{'Benchmark':<{name_width}} {'Python (ns)':>{py_w}} {'Py RSD':>{rsd_w}}" + + sep = "-" * len(header) + print(sep) + print(header) + print(sep) for name in all_names: py_vals = py_benchmarks.get(name) @@ -120,17 +140,21 @@ def main() -> None: cpp_rsd = fmt_rsd(cpp_stats[2]) if cpp_stats else "-" if py_stats and cpp_stats: - py_mean = py_stats[0] - cpp_mean = cpp_stats[0] - overhead_ns = (py_mean - cpp_mean) * 1e9 - overhead_str = f"{overhead_ns:+.0f} ns" + overhead_ns_str = fmt_overhead_ns(py_stats[0], cpp_stats[0]) + overhead_pct_str = fmt_overhead_pct(py_stats[0], cpp_stats[0]) else: - overhead_str = "-" + overhead_ns_str = "-" + overhead_pct_str = "-" if cpp_benchmarks: - print(f"{name:<{name_width}} {cpp_str:>12} {cpp_rsd:>8} {py_str:>14} {py_rsd:>7} {overhead_str:>10}") + print( + f"{name:<{name_width}} " + f"{cpp_str:>{cpp_w}} {cpp_rsd:>{rsd_w}} " + f"{py_str:>{py_w}} {py_rsd:>{rsd_w}} " + f"{overhead_ns_str:>{oh_ns_w}} {overhead_pct_str:>{oh_pct_w}}" + ) else: - print(f"{name:<{name_width}} {py_str:>14} {py_rsd:>7}") + print(f"{name:<{name_width}} {py_str:>{py_w}} {py_rsd:>{rsd_w}}") print(sep) diff --git a/benchmarks/cuda_bindings/pixi.toml b/benchmarks/cuda_bindings/pixi.toml index a265f7f01e7..92b7b2e336f 100644 --- a/benchmarks/cuda_bindings/pixi.toml +++ b/benchmarks/cuda_bindings/pixi.toml @@ -57,9 +57,6 @@ cmd = ["python", "$PIXI_PROJECT_ROOT/run_pyperf.py"] [target.linux.tasks.bench-smoke-test] cmd = ["python", "$PIXI_PROJECT_ROOT/run_pyperf.py", "--debug-single-value"] -[target.linux.tasks.bench-legacy] -cmd = "pytest --benchmark-only --override-ini 'addopts=' $PIXI_PROJECT_ROOT/pytest-legacy/" - [target.linux.tasks.bench-cpp-configure] cmd = [ "cmake", diff --git a/benchmarks/cuda_bindings/pytest-legacy/conftest.py b/benchmarks/cuda_bindings/pytest-legacy/conftest.py deleted file mode 100644 index 5d0cc95e7a0..00000000000 --- a/benchmarks/cuda_bindings/pytest-legacy/conftest.py +++ /dev/null @@ -1,93 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import numpy as np -import pytest - -from cuda.bindings import driver as cuda -from cuda.bindings import nvrtc -from cuda.bindings import runtime as cudart - - -def ASSERT_DRV(err): - if isinstance(err, cuda.CUresult): - if err != cuda.CUresult.CUDA_SUCCESS: - raise RuntimeError(f"Cuda Error: {err}") - elif isinstance(err, cudart.cudaError_t): - if err != cudart.cudaError_t.cudaSuccess: - raise RuntimeError(f"Cudart Error: {err}") - elif isinstance(err, nvrtc.nvrtcResult): - if err != nvrtc.nvrtcResult.NVRTC_SUCCESS: - raise RuntimeError(f"Nvrtc Error: {err}") - else: - raise RuntimeError(f"Unknown error type: {err}") - - -@pytest.fixture -def init_cuda(): - # Initialize - (err,) = cuda.cuInit(0) - ASSERT_DRV(err) - err, device = cuda.cuDeviceGet(0) - ASSERT_DRV(err) - err, ctx = cuda.cuCtxCreate(None, 0, device) - ASSERT_DRV(err) - - # create stream - err, stream = cuda.cuStreamCreate(cuda.CUstream_flags.CU_STREAM_NON_BLOCKING.value) - ASSERT_DRV(err) - - yield device, ctx, stream - - (err,) = cuda.cuStreamDestroy(stream) - ASSERT_DRV(err) - (err,) = cuda.cuCtxDestroy(ctx) - ASSERT_DRV(err) - - -@pytest.fixture -def load_module(): - module = None - - def _load_module(kernel_string, device): - nonlocal module - # Get module - err, major = cuda.cuDeviceGetAttribute( - cuda.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, device - ) - ASSERT_DRV(err) - err, minor = cuda.cuDeviceGetAttribute( - cuda.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, device - ) - ASSERT_DRV(err) - - err, prog = nvrtc.nvrtcCreateProgram(str.encode(kernel_string), b"kernelString.cu", 0, [], []) - ASSERT_DRV(err) - opts = [b"--fmad=false", bytes("--gpu-architecture=sm_" + str(major) + str(minor), "ascii")] - (err,) = nvrtc.nvrtcCompileProgram(prog, 2, opts) - - err_log, logSize = nvrtc.nvrtcGetProgramLogSize(prog) - ASSERT_DRV(err_log) - log = b" " * logSize - (err_log,) = nvrtc.nvrtcGetProgramLog(prog, log) - ASSERT_DRV(err_log) - result = log.decode() - if len(result) > 1: - print(result) - - ASSERT_DRV(err) - err, cubinSize = nvrtc.nvrtcGetCUBINSize(prog) - ASSERT_DRV(err) - cubin = b" " * cubinSize - (err,) = nvrtc.nvrtcGetCUBIN(prog, cubin) - ASSERT_DRV(err) - cubin = np.char.array(cubin) - err, module = cuda.cuModuleLoadData(cubin) - ASSERT_DRV(err) - - return module - - yield _load_module - - (err,) = cuda.cuModuleUnload(module) - ASSERT_DRV(err) diff --git a/benchmarks/cuda_bindings/pytest-legacy/kernels.py b/benchmarks/cuda_bindings/pytest-legacy/kernels.py deleted file mode 100644 index 7e741110a3e..00000000000 --- a/benchmarks/cuda_bindings/pytest-legacy/kernels.py +++ /dev/null @@ -1,159 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -kernel_string = """\ -#define ITEM_PARAM(x, T) T x -#define REP1(x, T) , ITEM_PARAM(x, T) -#define REP2(x, T) REP1(x##0, T) REP1(x##1, T) -#define REP4(x, T) REP2(x##0, T) REP2(x##1, T) -#define REP8(x, T) REP4(x##0, T) REP4(x##1, T) -#define REP16(x, T) REP8(x##0, T) REP8(x##1, T) -#define REP32(x, T) REP16(x##0, T) REP16(x##1, T) -#define REP64(x, T) REP32(x##0, T) REP32(x##1, T) -#define REP128(x, T) REP64(x##0, T) REP64(x##1, T) -#define REP256(x, T) REP128(x##0, T) REP128(x##1, T) - -template -struct KernelFunctionParam -{ - unsigned char p[maxBytes]; -}; - -extern "C" __global__ void small_kernel(float *f) -{ - *f = 0.0f; -} - -extern "C" __global__ void empty_kernel() -{ - return; -} - -extern "C" __global__ -void small_kernel_512_args( - ITEM_PARAM(F, int*) - REP1(A, int*) - REP2(A, int*) - REP4(A, int*) - REP8(A, int*) - REP16(A, int*) - REP32(A, int*) - REP64(A, int*) - REP128(A, int*) - REP256(A, int*)) -{ - *F = 0; -} - -extern "C" __global__ -void small_kernel_512_bools( - ITEM_PARAM(F, bool) - REP1(A, bool) - REP2(A, bool) - REP4(A, bool) - REP8(A, bool) - REP16(A, bool) - REP32(A, bool) - REP64(A, bool) - REP128(A, bool) - REP256(A, bool)) -{ - return; -} - -extern "C" __global__ -void small_kernel_512_ints( - ITEM_PARAM(F, int) - REP1(A, int) - REP2(A, int) - REP4(A, int) - REP8(A, int) - REP16(A, int) - REP32(A, int) - REP64(A, int) - REP128(A, int) - REP256(A, int)) -{ - return; -} - -extern "C" __global__ -void small_kernel_512_doubles( - ITEM_PARAM(F, double) - REP1(A, double) - REP2(A, double) - REP4(A, double) - REP8(A, double) - REP16(A, double) - REP32(A, double) - REP64(A, double) - REP128(A, double) - REP256(A, double)) -{ - return; -} - -extern "C" __global__ -void small_kernel_512_chars( - ITEM_PARAM(F, char) - REP1(A, char) - REP2(A, char) - REP4(A, char) - REP8(A, char) - REP16(A, char) - REP32(A, char) - REP64(A, char) - REP128(A, char) - REP256(A, char)) -{ - return; -} - -extern "C" __global__ -void small_kernel_512_longlongs( - ITEM_PARAM(F, long long) - REP1(A, long long) - REP2(A, long long) - REP4(A, long long) - REP8(A, long long) - REP16(A, long long) - REP32(A, long long) - REP64(A, long long) - REP128(A, long long) - REP256(A, long long)) -{ - return; -} - -extern "C" __global__ -void small_kernel_256_args( - ITEM_PARAM(F, int*) - REP1(A, int*) - REP2(A, int*) - REP4(A, int*) - REP8(A, int*) - REP16(A, int*) - REP32(A, int*) - REP64(A, int*) - REP128(A, int*)) -{ - *F = 0; -} - -extern "C" __global__ -void small_kernel_16_args( - ITEM_PARAM(F, int*) - REP1(A, int*) - REP2(A, int*) - REP4(A, int*) - REP8(A, int*)) -{ - *F = 0; -} - -extern "C" __global__ void small_kernel_2048B(KernelFunctionParam<2048> param) -{ - // Do not touch param to prevent compiler from copying - // the whole structure from const bank to lmem. -} -""" diff --git a/benchmarks/cuda_bindings/pytest-legacy/test_cupy.py b/benchmarks/cuda_bindings/pytest-legacy/test_cupy.py deleted file mode 100644 index 3eea752ce09..00000000000 --- a/benchmarks/cuda_bindings/pytest-legacy/test_cupy.py +++ /dev/null @@ -1,199 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import ctypes - -import pytest - -try: - import cupy - - skip_tests = False -except ImportError: - skip_tests = True - -from kernels import kernel_string - - -def launch(kernel, args=()): - kernel((1,), (1,), args) - - -# Measure launch latency with no parmaeters -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_empty_kernel(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("empty_kernel") - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel) - stream.synchronize() - - -# Measure launch latency with a single parameter -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel") - cupy.cuda.set_allocator() - arg = cupy.cuda.alloc(ctypes.sizeof(ctypes.c_float)) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, (arg,)) - stream.synchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel_512_args(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel_512_args") - cupy.cuda.set_allocator() - - args = [] - for _ in range(512): - args.append(cupy.cuda.alloc(ctypes.sizeof(ctypes.c_int))) - args = tuple(args) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, args) - stream.synchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel_512_bools(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel_512_bools") - cupy.cuda.set_allocator() - - args = [True] * 512 - args = tuple(args) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, args) - stream.synchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel_512_doubles(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel_512_doubles") - cupy.cuda.set_allocator() - - args = [1.2345] * 512 - args = tuple(args) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, args) - stream.synchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel_512_ints(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel_512_ints") - cupy.cuda.set_allocator() - - args = [123] * 512 - args = tuple(args) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, args) - stream.synchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel_512_bytes(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel_512_chars") - cupy.cuda.set_allocator() - - args = [127] * 512 - args = tuple(args) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, args) - stream.synchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel_512_longlongs(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel_512_longlongs") - cupy.cuda.set_allocator() - - args = [9223372036854775806] * 512 - args = tuple(args) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, args) - stream.synchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel_256_args(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel_256_args") - cupy.cuda.set_allocator() - - args = [] - for _ in range(256): - args.append(cupy.cuda.alloc(ctypes.sizeof(ctypes.c_int))) - args = tuple(args) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, args) - stream.synchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.skipif(skip_tests, reason="cupy is not installed") -@pytest.mark.benchmark(group="cupy") -def test_launch_latency_small_kernel_16_args(benchmark): - module = cupy.RawModule(code=kernel_string) - kernel = module.get_function("small_kernel_16_args") - cupy.cuda.set_allocator() - - args = [] - for _ in range(16): - args.append(cupy.cuda.alloc(ctypes.sizeof(ctypes.c_int))) - args = tuple(args) - - stream = cupy.cuda.stream.Stream(non_blocking=True) - - with stream: - benchmark(launch, kernel, args) - stream.synchronize() diff --git a/benchmarks/cuda_bindings/pytest-legacy/test_launch_latency.py b/benchmarks/cuda_bindings/pytest-legacy/test_launch_latency.py deleted file mode 100755 index ad421de382a..00000000000 --- a/benchmarks/cuda_bindings/pytest-legacy/test_launch_latency.py +++ /dev/null @@ -1,336 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import ctypes - -import pytest -from kernels import kernel_string - -from conftest import ASSERT_DRV -from cuda.bindings import driver as cuda - - -def launch(kernel, stream, args=(), arg_types=()): - cuda.cuLaunchKernel( - kernel, - 1, - 1, - 1, # grid dim - 1, - 1, - 1, # block dim - 0, - stream, # shared mem and stream - (args, arg_types), - 0, - ) # arguments - - -def launch_packed(kernel, stream, params): - cuda.cuLaunchKernel( - kernel, - 1, - 1, - 1, # grid dim - 1, - 1, - 1, # block dim - 0, - stream, # shared mem and stream - params, - 0, - ) # arguments - - -# Measure launch latency with no parmaeters -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_empty_kernel(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"empty_kernel") - ASSERT_DRV(err) - - benchmark(launch, func, stream) - - cuda.cuCtxSynchronize() - - -# Measure launch latency with a single parameter -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel") - ASSERT_DRV(err) - - err, f = cuda.cuMemAlloc(ctypes.sizeof(ctypes.c_float)) - ASSERT_DRV(err) - - benchmark(launch, func, stream, args=(f,), arg_types=(None,)) - - cuda.cuCtxSynchronize() - - (err,) = cuda.cuMemFree(f) - ASSERT_DRV(err) - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_512_args(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_512_args") - ASSERT_DRV(err) - - args = [] - arg_types = [None] * 512 - for _ in arg_types: - err, p = cuda.cuMemAlloc(ctypes.sizeof(ctypes.c_int)) - ASSERT_DRV(err) - args.append(p) - - args = tuple(args) - arg_types = tuple(arg_types) - - benchmark(launch, func, stream, args=args, arg_types=arg_types) - - cuda.cuCtxSynchronize() - - for p in args: - (err,) = cuda.cuMemFree(p) - ASSERT_DRV(err) - - -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_512_bools(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_512_bools") - ASSERT_DRV(err) - - args = [True] * 512 - arg_types = [ctypes.c_bool] * 512 - - args = tuple(args) - arg_types = tuple(arg_types) - - benchmark(launch, func, stream, args=args, arg_types=arg_types) - - cuda.cuCtxSynchronize() - - -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_512_doubles(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_512_doubles") - ASSERT_DRV(err) - - args = [1.2345] * 512 - arg_types = [ctypes.c_double] * 512 - - args = tuple(args) - arg_types = tuple(arg_types) - - benchmark(launch, func, stream, args=args, arg_types=arg_types) - - cuda.cuCtxSynchronize() - - -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_512_ints(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_512_ints") - ASSERT_DRV(err) - - args = [123] * 512 - arg_types = [ctypes.c_int] * 512 - - args = tuple(args) - arg_types = tuple(arg_types) - - benchmark(launch, func, stream, args=args, arg_types=arg_types) - - cuda.cuCtxSynchronize() - - -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_512_bytes(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_512_chars") - ASSERT_DRV(err) - - args = [127] * 512 - arg_types = [ctypes.c_byte] * 512 - - args = tuple(args) - arg_types = tuple(arg_types) - - benchmark(launch, func, stream, args=args, arg_types=arg_types) - - cuda.cuCtxSynchronize() - - -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_512_longlongs(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_512_longlongs") - ASSERT_DRV(err) - - args = [9223372036854775806] * 512 - arg_types = [ctypes.c_longlong] * 512 - - args = tuple(args) - arg_types = tuple(arg_types) - - benchmark(launch, func, stream, args=args, arg_types=arg_types) - - cuda.cuCtxSynchronize() - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_256_args(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_256_args") - ASSERT_DRV(err) - - args = [] - arg_types = [None] * 256 - for _ in arg_types: - err, p = cuda.cuMemAlloc(ctypes.sizeof(ctypes.c_int)) - ASSERT_DRV(err) - args.append(p) - - args = tuple(args) - arg_types = tuple(arg_types) - - benchmark(launch, func, stream, args=args, arg_types=arg_types) - - cuda.cuCtxSynchronize() - - for p in args: - (err,) = cuda.cuMemFree(p) - ASSERT_DRV(err) - - -# Measure launch latency with many parameters using builtin parameter packing -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_16_args(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_16_args") - ASSERT_DRV(err) - - args = [] - arg_types = [None] * 16 - for _ in arg_types: - err, p = cuda.cuMemAlloc(ctypes.sizeof(ctypes.c_int)) - ASSERT_DRV(err) - args.append(p) - - args = tuple(args) - arg_types = tuple(arg_types) - - benchmark(launch, func, stream, args=args, arg_types=arg_types) - - cuda.cuCtxSynchronize() - - for p in args: - (err,) = cuda.cuMemFree(p) - ASSERT_DRV(err) - - -# Measure launch latency with many parameters, excluding parameter packing -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_512_args_ctypes(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_512_args") - ASSERT_DRV(err) - - vals = [] - val_ps = [] - for i in range(512): - err, p = cuda.cuMemAlloc(ctypes.sizeof(ctypes.c_int)) - ASSERT_DRV(err) - vals.append(p) - val_ps.append(ctypes.c_void_p(int(vals[i]))) - - packagedParams = (ctypes.c_void_p * 512)() - for i in range(512): - packagedParams[i] = ctypes.addressof(val_ps[i]) - - benchmark(launch_packed, func, stream, packagedParams) - - cuda.cuCtxSynchronize() - - for p in vals: - (err,) = cuda.cuMemFree(p) - ASSERT_DRV(err) - - -def pack_and_launch(kernel, stream, params): - packed_params = (ctypes.c_void_p * len(params))() - ptrs = [0] * len(params) - for i in range(len(params)): - ptrs[i] = ctypes.c_void_p(int(params[i])) - packed_params[i] = ctypes.addressof(ptrs[i]) - - cuda.cuLaunchKernel(kernel, 1, 1, 1, 1, 1, 1, 0, stream, packed_params, 0) - - -# Measure launch latency plus parameter packing using ctypes -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_512_args_ctypes_with_packing(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_512_args") - ASSERT_DRV(err) - - vals = [] - for i in range(512): - err, p = cuda.cuMemAlloc(ctypes.sizeof(ctypes.c_int)) - ASSERT_DRV(err) - vals.append(p) - - benchmark(pack_and_launch, func, stream, vals) - - cuda.cuCtxSynchronize() - - for p in vals: - (err,) = cuda.cuMemFree(p) - ASSERT_DRV(err) - - -# Measure launch latency with a single large struct parameter -@pytest.mark.benchmark(group="launch-latency") -def test_launch_latency_small_kernel_2048B(benchmark, init_cuda, load_module): - device, ctx, stream = init_cuda - module = load_module(kernel_string, device) - - err, func = cuda.cuModuleGetFunction(module, b"small_kernel_2048B") - ASSERT_DRV(err) - - class struct_2048B(ctypes.Structure): - _fields_ = [("values", ctypes.c_uint8 * 2048)] - - benchmark(launch, func, stream, args=(struct_2048B(),), arg_types=(None,)) - - cuda.cuCtxSynchronize() diff --git a/benchmarks/cuda_bindings/pytest-legacy/test_numba.py b/benchmarks/cuda_bindings/pytest-legacy/test_numba.py deleted file mode 100644 index d9ae0cdfeed..00000000000 --- a/benchmarks/cuda_bindings/pytest-legacy/test_numba.py +++ /dev/null @@ -1,52 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import numpy as np -import pytest - -try: - from numba import cuda - - skip_tests = False -except ImportError: - skip_tests = True - - -def launch_empty(kernel, stream): - kernel[1, 1, stream]() - - -def launch(kernel, stream, arg): - kernel[1, 1, stream](arg) - - -# Measure launch latency with no parmaeters -@pytest.mark.skipif(skip_tests, reason="Numba is not installed") -@pytest.mark.benchmark(group="numba", min_rounds=1000) -def test_launch_latency_empty_kernel(benchmark): - stream = cuda.stream() - - @cuda.jit - def empty_kernel(): - return - - benchmark(launch_empty, empty_kernel, stream) - - cuda.synchronize() - - -# Measure launch latency with a single parameter -@pytest.mark.skipif(skip_tests, reason="Numba is not installed") -@pytest.mark.benchmark(group="numba", min_rounds=1000) -def test_launch_latency_small_kernel(benchmark): - stream = cuda.stream() - - arg = cuda.device_array(1, dtype=np.float32, stream=stream) - - @cuda.jit - def small_kernel(array): - array[0] = 0.0 - - benchmark(launch, small_kernel, stream, arg) - - cuda.synchronize() diff --git a/benchmarks/cuda_bindings/pytest-legacy/test_pointer_attributes.py b/benchmarks/cuda_bindings/pytest-legacy/test_pointer_attributes.py deleted file mode 100644 index 6df32ec5118..00000000000 --- a/benchmarks/cuda_bindings/pytest-legacy/test_pointer_attributes.py +++ /dev/null @@ -1,112 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import random - -import pytest - -from conftest import ASSERT_DRV -from cuda.bindings import driver as cuda - -random.seed(0) - -idx = 0 - - -def query_attribute(attribute, ptrs): - global idx - ptr = ptrs[idx] - idx = (idx + 1) % len(ptrs) - - cuda.cuPointerGetAttribute(attribute, ptr) - - -def query_attributes(attributes, ptrs): - global idx - ptr = ptrs[idx] - idx = (idx + 1) % len(ptrs) - - cuda.cuPointerGetAttributes(len(attributes), attributes, ptr) - - -@pytest.mark.benchmark(group="pointer-attributes") -# Measure cuPointerGetAttribute in the same way as C benchmarks -def test_pointer_get_attribute(benchmark, init_cuda): - _ = init_cuda - - ptrs = [] - for _ in range(500): - err, ptr = cuda.cuMemAlloc(1 << 18) - ASSERT_DRV(err) - ptrs.append(ptr) - - random.shuffle(ptrs) - - benchmark(query_attribute, cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_MEMORY_TYPE, ptrs) - - for p in ptrs: - (err,) = cuda.cuMemFree(p) - ASSERT_DRV(err) - - -@pytest.mark.benchmark(group="pointer-attributes") -# Measure cuPointerGetAttributes with all attributes -def test_pointer_get_attributes_all(benchmark, init_cuda): - _ = init_cuda - - ptrs = [] - for _ in range(500): - err, ptr = cuda.cuMemAlloc(1 << 18) - ASSERT_DRV(err) - ptrs.append(ptr) - - random.shuffle(ptrs) - - attributes = [ - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_CONTEXT, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_MEMORY_TYPE, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_HOST_POINTER, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_P2P_TOKENS, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_SYNC_MEMOPS, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_BUFFER_ID, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_IS_MANAGED, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_IS_LEGACY_CUDA_IPC_CAPABLE, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_RANGE_START_ADDR, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_RANGE_SIZE, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_MAPPED, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_ALLOWED_HANDLE_TYPES, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_IS_GPU_DIRECT_RDMA_CAPABLE, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_ACCESS_FLAGS, - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE, - ] - - benchmark(query_attributes, attributes, ptrs) - - for p in ptrs: - (err,) = cuda.cuMemFree(p) - ASSERT_DRV(err) - - -@pytest.mark.benchmark(group="pointer-attributes") -# Measure cuPointerGetAttributes with a single attribute -def test_pointer_get_attributes_single(benchmark, init_cuda): - _ = init_cuda - - ptrs = [] - for _ in range(500): - err, ptr = cuda.cuMemAlloc(1 << 18) - ASSERT_DRV(err) - ptrs.append(ptr) - - random.shuffle(ptrs) - - attributes = [ - cuda.CUpointer_attribute.CU_POINTER_ATTRIBUTE_MEMORY_TYPE, - ] - - benchmark(query_attributes, attributes, ptrs) - - for p in ptrs: - (err,) = cuda.cuMemFree(p) - ASSERT_DRV(err) diff --git a/benchmarks/cuda_bindings/runner/cpp.py b/benchmarks/cuda_bindings/runner/cpp.py index f8c34903813..f752d677100 100644 --- a/benchmarks/cuda_bindings/runner/cpp.py +++ b/benchmarks/cuda_bindings/runner/cpp.py @@ -172,6 +172,15 @@ def main() -> None: count = merge_pyperf_json(individual_files, output_path) print(f"\nResults saved to {output_path} ({count} benchmark(s))") + if not individual_files and not failed: + print( + "No C++ benchmark results were produced. The selected benchmark " + "binaries may have skipped every benchmark on this driver/device. " + "Existing output file (if any) was left untouched.", + file=sys.stderr, + ) + sys.exit(1) + if failed: sys.exit(1) diff --git a/benchmarks/cuda_bindings/runner/main.py b/benchmarks/cuda_bindings/runner/main.py index b0f6e76f417..2e7312c3cef 100644 --- a/benchmarks/cuda_bindings/runner/main.py +++ b/benchmarks/cuda_bindings/runner/main.py @@ -16,12 +16,19 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent BENCH_DIR = PROJECT_ROOT / "benchmarks" DEFAULT_OUTPUT = PROJECT_ROOT / "results-python.json" +# Env var used to propagate the --benchmark filter from the parent to pyperf +# worker subprocesses. pyperf reconstructs worker argv from scratch and drops +# custom flags like --benchmark, so without this the worker would register the +# full bench list and pyperf would run the wrong bench by task index. +BENCH_FILTER_ENV_VAR = "CUDA_BINDINGS_BENCH_FILTER" + PYPERF_INHERITED_ENV_VARS = ( "CUDA_HOME", "CUDA_PATH", "CUDA_VISIBLE_DEVICES", "LD_LIBRARY_PATH", "NVIDIA_VISIBLE_DEVICES", + BENCH_FILTER_ENV_VAR, ) _MODULE_CACHE: dict[Path, ModuleType] = {} @@ -68,9 +75,45 @@ def run(loops: int) -> float: return loaded_function(loops) run.__name__ = function_name + # Expose the binding source so the runner can introspect skip lists before + # handing the benchmark to pyperf. Accessing these attributes does not + # trigger module import — discovery stays lazy. + run._bench_module_path = module_path # type: ignore[attr-defined] + run._bench_function_name = function_name # type: ignore[attr-defined] return run +def _collect_skipped_benchmarks( + bench_ids: list[str], + registry: dict[str, Callable[[int], float]], +) -> set[str]: + """Return bench IDs that the owning module has marked as unsupported. + + Benchmark modules may declare a module-level + ``SKIPPED_BENCHMARKS: set[str]`` containing the names of ``bench_*`` + functions whose underlying API is unavailable on the current driver or + device (e.g. TMA encoders on pre-Hopper GPUs). Loading the module runs + its import-time probe, so this call is the same cost as the first + real invocation would have been. + """ + skipped: set[str] = set() + loaded_modules: dict[Path, ModuleType] = {} + for bench_id in bench_ids: + fn = registry[bench_id] + module_path = getattr(fn, "_bench_module_path", None) + function_name = getattr(fn, "_bench_function_name", None) + if module_path is None or function_name is None: + continue + module = loaded_modules.get(module_path) + if module is None: + module = load_module(module_path) + loaded_modules[module_path] = module + module_skip = getattr(module, "SKIPPED_BENCHMARKS", None) + if module_skip and function_name in module_skip: + skipped.add(bench_id) + return skipped + + def discover_benchmarks() -> dict[str, Callable[[int], float]]: """Discover bench_ functions. @@ -183,13 +226,24 @@ def main() -> None: print(bench_id) return - if parsed.benchmark: - missing = sorted(set(parsed.benchmark) - set(registry)) + # The --benchmark filter must be the same in the parent and in any pyperf + # worker subprocess, otherwise pyperf's task-index bookkeeping points at + # the wrong bench. pyperf drops unknown CLI flags when spawning workers, + # so fall back to an env var carrying the filter. + requested = list(parsed.benchmark) + env_filter = os.environ.get(BENCH_FILTER_ENV_VAR, "") + if not requested and env_filter: + requested = [bid for bid in env_filter.split(",") if bid] + + if requested: + missing = sorted(set(requested) - set(registry)) if missing: known = ", ".join(sorted(registry)) unknown = ", ".join(missing) raise ValueError(f"Unknown benchmark(s): {unknown}. Known benchmarks: {known}") - benchmark_ids = parsed.benchmark + benchmark_ids = requested + # Propagate to any pyperf worker we're about to spawn. + os.environ[BENCH_FILTER_ENV_VAR] = ",".join(benchmark_ids) else: benchmark_ids = sorted(registry) @@ -199,7 +253,31 @@ def main() -> None: remaining_argv = ensure_pyperf_worker_env(remaining_argv) is_worker = "--worker" in remaining_argv - # Delete the file so this run starts fresh. + # Drop benchmarks that the owning module has marked as unavailable on + # this driver/device. Without this step a single unsupported bench + # (e.g. TMA on a pre-Hopper GPU) would abort the whole pyperf run, + # since pyperf treats a raised exception as a fatal worker failure. + skipped = _collect_skipped_benchmarks(benchmark_ids, registry) + if skipped and not is_worker: + for bench_id in sorted(skipped): + print(f"Skipping {bench_id}: unsupported on this driver/device", file=sys.stderr) + benchmark_ids = [bench_id for bench_id in benchmark_ids if bench_id not in skipped] + + # If every selected benchmark was skipped, fail loudly instead of silently + # printing "Results saved" with no output. Leave any existing output file + # untouched so a prior successful run is not destroyed. + if not benchmark_ids: + if not is_worker: + print( + "No benchmarks to run: every selected benchmark is unsupported " + "on this driver/device. Existing output file (if any) was left " + "untouched.", + file=sys.stderr, + ) + sys.exit(1) + + # Delete the file so this run starts fresh. Only destructive once we know + # at least one benchmark will actually run. if not is_worker: output_path.unlink(missing_ok=True) From aac5bf50859420138563947c106283fe2d92c4bc Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Thu, 30 Apr 2026 20:14:54 -0700 Subject: [PATCH 055/244] cuda.core: add C++17 standard flag and use -O2 optimization (#2002) Pass -std=c++17 (Linux) / /std:c++17 (Windows) for all builds, and use -O2 instead of -O3 for non-debug Linux builds. Partially addresses #1882. Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_core/build_hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index f4fb4af01f7..a94120616e0 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -168,15 +168,17 @@ def get_sources(mod_name): extra_link_args = [] extra_cythonize_kwargs = {} if sys.platform == "win32": + extra_compile_args += ["/std:c++17"] if debug: raise RuntimeError("Debuggable builds are not supported on Windows.") else: + extra_compile_args += ["-std=c++17"] if debug: extra_cythonize_kwargs["gdb_debug"] = True extra_compile_args += ["-g", "-O0"] extra_compile_args += ["-D _GLIBCXX_ASSERTIONS"] else: - extra_compile_args += ["-O3"] + extra_compile_args += ["-O2"] extra_link_args += ["-Wl,--strip-all"] if COMPILE_FOR_COVERAGE: # CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not From 0f850618eae5a5ddf282d8bc2d823df3dda84d6d Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Fri, 1 May 2026 07:26:48 -0700 Subject: [PATCH 056/244] cuda.core: fix _arr_is_c_contiguous discriminator for numba arrays (#1998) `_arr_is_c_contiguous` checked `hasattr(arr, "flags")`, which is True for both numpy arrays and numba `DeviceNDArray`. For numba `arr.flags` is a plain dict, so the truthy branch falls into `arr.flags.c_contiguous` and raises `AttributeError: 'dict' object has no attribute 'c_contiguous'`. Discriminate on the flags object instead, mirroring the sibling `_arr_is_writeable` helper. Unblocks six numba-cuda parametrizations: - TestViewGPU::test_args_viewable_as_strided_memory_gpu[numba-cuda-{int8,float32}] - TestViewGPU::test_strided_memory_view_cpu[numba-cuda-{int8,float32}] - TestViewGPU::test_strided_memory_view_init[numba-cuda-{int8,float32}] Made-with: Cursor --- cuda_core/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuda_core/tests/test_utils.py b/cuda_core/tests/test_utils.py index 91075d85368..e5e47894646 100644 --- a/cuda_core/tests/test_utils.py +++ b/cuda_core/tests/test_utils.py @@ -102,7 +102,7 @@ def _arr_size(arr): def _arr_is_c_contiguous(arr): if torch is not None and isinstance(arr, torch.Tensor): return arr.is_contiguous() - return arr.flags.c_contiguous if hasattr(arr, "flags") else arr.flags["C_CONTIGUOUS"] + return arr.flags.c_contiguous if hasattr(arr.flags, "c_contiguous") else arr.flags["C_CONTIGUOUS"] def _arr_is_writeable(arr): From 6e5fb12e33ca66230b6aebef49a81171bd6404f9 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 1 May 2026 10:55:42 -0400 Subject: [PATCH 057/244] DOCS: Grammar and spelling; Linking to objects (#1997) --- cuda_core/cuda/core/_device.pyx | 18 +++++++++--------- cuda_core/cuda/core/_event.pyx | 15 ++++++++++++++- cuda_core/cuda/core/_linker.pyx | 2 +- cuda_core/cuda/core/_memory/_buffer.pyx | 5 +++-- .../core/_memory/_device_memory_resource.pyx | 2 +- cuda_core/cuda/core/_module.pyx | 2 +- cuda_core/cuda/core/_program.pyx | 8 ++++---- cuda_core/cuda/core/_stream.pyx | 11 +++++++++-- cuda_core/cuda/core/system/_system.pyx | 10 ++++++++++ cuda_core/docs/source/getting-started.rst | 4 ++-- 10 files changed, 54 insertions(+), 23 deletions(-) diff --git a/cuda_core/cuda/core/_device.pyx b/cuda_core/cuda/core/_device.pyx index 1ea2df564c4..c0d7f09ee44 100644 --- a/cuda_core/cuda/core/_device.pyx +++ b/cuda_core/cuda/core/_device.pyx @@ -377,7 +377,7 @@ cdef class DeviceProperties: @property def gpu_overlap(self) -> bool: - """bool: Device can possibly copy memory and execute a kernel concurrently. Deprecated. Use instead async_engine_count.""" + """bool: Device can possibly copy memory and execute a kernel concurrently. Deprecated. Use :attr:`~DeviceProperties.async_engine_count` instead.""" return bool(self._get_cached_attribute(driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_GPU_OVERLAP)) @property @@ -662,7 +662,7 @@ cdef class DeviceProperties: @property def read_only_host_register_supported(self) -> bool: - """bool: True if device supports using the cuMemHostRegister flag CU_MEMHOSTERGISTER_READ_ONLY to register memory that must be mapped as read-only to the GPU, False if not.""" + """bool: True if device supports using the cuMemHostRegister flag CU_MEMHOSTREGISTER_READ_ONLY to register memory that must be mapped as read-only to the GPU, False if not.""" return bool( self._get_cached_attribute(driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_READ_ONLY_HOST_REGISTER_SUPPORTED) ) @@ -841,12 +841,12 @@ cdef class DeviceProperties: @property def mem_decompress_algorithm_mask(self) -> int: - """int: The returned valued shall be interpreted as a bitmask, where the individual bits are described by the CUmemDecompressAlgorithm enum.""" + """int: The returned value shall be interpreted as a bitmask, where the individual bits are described by the CUmemDecompressAlgorithm enum.""" return self._get_cached_attribute(driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MEM_DECOMPRESS_ALGORITHM_MASK) @property def mem_decompress_maximum_length(self) -> int: - """int: The returned valued is the maximum length in bytes of a single decompress operation that is allowed.""" + """int: The returned value is the maximum length in bytes of a single decompress operation that is allowed.""" return self._get_cached_attribute(driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MEM_DECOMPRESS_MAXIMUM_LENGTH) @property @@ -897,7 +897,7 @@ cdef class DeviceProperties: @property def host_memory_pools_supported(self) -> bool: - """bool: Device suports HOST location with the cuMemAllocAsync and cuMemPool family of APIs.""" + """bool: Device supports HOST location with the cuMemAllocAsync and cuMemPool family of APIs.""" return bool( self._get_cached_attribute(driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_HOST_MEMORY_POOLS_SUPPORTED) ) @@ -1033,7 +1033,7 @@ class Device: Parameters ---------- peer : Device | int - The peer device to check accessibility to. Can be a Device object or device ID. + The peer device to check accessibility to. Can be a :obj:`~_device.Device` object or device ID. """ peer = Device(peer) cdef int d1 = self.device_id @@ -1253,7 +1253,7 @@ class Device: Note ---- - The newly context will not be set as current. + The newly created context will not be set as current. Parameters ---------- @@ -1269,7 +1269,7 @@ class Device: raise NotImplementedError("WIP: https://github.com/NVIDIA/cuda-python/issues/189") def create_stream(self, obj: IsStreamT | None = None, options: StreamOptions | None = None) -> Stream: - """Create a Stream object. + """Create a :obj:`~_stream.Stream` object. New stream objects can be created in two different ways: @@ -1300,7 +1300,7 @@ class Device: return Stream._init(obj=obj, options=options, device_id=self._device_id, ctx=self._context) def create_event(self, options: EventOptions | None = None) -> Event: - """Create an Event object without recording it to a Stream. + """Create an :obj:`~_event.Event` object without recording it to a :obj:`~_stream.Stream`. Note ---- diff --git a/cuda_core/cuda/core/_event.pyx b/cuda_core/cuda/core/_event.pyx index 5fde724d21a..3f5fb7ace26 100644 --- a/cuda_core/cuda/core/_event.pyx +++ b/cuda_core/cuda/core/_event.pyx @@ -213,7 +213,20 @@ cdef class Event: @classmethod def from_ipc_descriptor(cls, ipc_descriptor: IPCEventDescriptor) -> Event: - """Import an event that was exported from another process.""" + """Import an event that was exported from another process. + + Parameters + ---------- + ipc_descriptor : :obj:`~_memory._ipc.IPCEventDescriptor` + The IPC descriptor obtained from :attr:`~Event.ipc_descriptor` in + another process. + + Returns + ------- + :obj:`~_event.Event` + A new event backed by the imported IPC handle. + + """ cdef cydriver.CUipcEventHandle data memcpy(data.reserved, (ipc_descriptor._reserved), sizeof(data.reserved)) cdef Event self = Event.__new__(cls) diff --git a/cuda_core/cuda/core/_linker.pyx b/cuda_core/cuda/core/_linker.pyx index c8dcf8e6150..e89e780b34c 100644 --- a/cuda_core/cuda/core/_linker.pyx +++ b/cuda_core/cuda/core/_linker.pyx @@ -188,7 +188,7 @@ class LinkerOptions: Attributes ---------- name : str, optional - Name of the linker. If the linking succeeds, the name is passed down to the generated `ObjectCode`. + Name of the linker. If the linking succeeds, the name is passed down to the generated :class:`ObjectCode`. arch : str, optional Pass the SM architecture value, such as ``sm_`` (for generating CUBIN) or ``compute_`` (for generating PTX). If not provided, the current device's architecture diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index 7de3c475d5d..a56657a3564 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -205,8 +205,9 @@ cdef class Buffer: Parameters ---------- - dst : :obj:`~_memory.Buffer` - Source buffer to copy data from + dst : :obj:`~_memory.Buffer`, optional + Destination buffer to copy data to. If not provided, a new buffer + is allocated using this buffer's memory resource. stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder` Keyword argument specifying the stream for the asynchronous copy diff --git a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx index 0f1a7f52e21..62df3cfb305 100644 --- a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx @@ -220,7 +220,7 @@ cdef class DeviceMemoryResource(_MemPool): Returns a tuple of sorted device IDs that currently have peer access to allocations from this memory pool. - When setting, accepts a sequence of Device objects or device IDs. + When setting, accepts a sequence of :obj:`~_device.Device` objects or device IDs. Setting to an empty sequence revokes all peer access. For non-owned pools (the default or current device pool), the state diff --git a/cuda_core/cuda/core/_module.pyx b/cuda_core/cuda/core/_module.pyx index aa865382345..d6c9481b82f 100644 --- a/cuda_core/cuda/core/_module.pyx +++ b/cuda_core/cuda/core/_module.pyx @@ -307,7 +307,7 @@ cdef class KernelOccupancy: Returns ------- :obj:`~MaxPotentialBlockSizeOccupancyResult` - An object with `min_grid_size` amd `max_block_size` attributes encoding + An object with `min_grid_size` and `max_block_size` attributes encoding the suggested launch configuration. Note diff --git a/cuda_core/cuda/core/_program.pyx b/cuda_core/cuda/core/_program.pyx index 194ef6da53f..cfc66451c86 100644 --- a/cuda_core/cuda/core/_program.pyx +++ b/cuda_core/cuda/core/_program.pyx @@ -173,7 +173,7 @@ class ProgramOptions: Attributes ---------- name : str, optional - Name of the program. If the compilation succeeds, the name is passed down to the generated `ObjectCode`. + Name of the program. If the compilation succeeds, the name is passed down to the generated :class:`ObjectCode`. arch : str, optional Pass the SM architecture value, such as ``sm_`` (for generating CUBIN) or ``compute_`` (for generating PTX). If not provided, the current device's architecture @@ -272,13 +272,13 @@ class ProgramOptions: Disable the display of a diagnostic number for warning messages. Default: False diag_error : Union[int, list[int]], optional - Emit error for a specified diagnostic message number or comma separated list of numbers. + Emit error for a specified diagnostic message number or comma-separated list of numbers. Default: None diag_suppress : Union[int, list[int]], optional - Suppress a specified diagnostic message number or comma separated list of numbers. + Suppress a specified diagnostic message number or comma-separated list of numbers. Default: None diag_warn : Union[int, list[int]], optional - Emit warning for a specified diagnostic message number or comma separated lis of numbers. + Emit warning for a specified diagnostic message number or comma-separated list of numbers. Default: None brief_diagnostics : bool, optional Disable or enable showing source line and column info in a diagnostic. diff --git a/cuda_core/cuda/core/_stream.pyx b/cuda_core/cuda/core/_stream.pyx index ca13811cd3c..fdb617f0325 100644 --- a/cuda_core/cuda/core/_stream.pyx +++ b/cuda_core/cuda/core/_stream.pyx @@ -227,7 +227,7 @@ cdef class Stream: def record(self, event: Event = None, options: EventOptions = None) -> Event: """Record an event onto the stream. - Creates an Event object (or reuses the given one) by + Creates an :obj:`~_event.Event` object (or reuses the given one) by recording on the stream. Parameters @@ -269,6 +269,13 @@ cdef class Stream: work is completed. This is done by recording a new :obj:`~_event.Event` on the stream and then waiting on it. + Parameters + ---------- + event_or_stream : :obj:`~_event.Event` | :obj:`~_stream.Stream` + The event or stream to wait for. Objects supporting the + ``__cuda_stream__`` protocol are also accepted and treated as + streams. + """ cdef Stream stream cdef EventHandle h_event @@ -332,7 +339,7 @@ cdef class Stream: Note ---- Stream lifetime is not managed, foreign object must remain - alive while this steam is active. + alive while this stream is active. Parameters ---------- diff --git a/cuda_core/cuda/core/system/_system.pyx b/cuda_core/cuda/core/system/_system.pyx index f306c036b8c..d1a7e97e1b6 100644 --- a/cuda_core/cuda/core/system/_system.pyx +++ b/cuda_core/cuda/core/system/_system.pyx @@ -88,6 +88,11 @@ def get_driver_version_full(kernel_mode: bool = False) -> tuple[int, int, int]: def get_nvml_version() -> tuple[int, ...]: """ The version of the NVML library. + + Returns + ------- + version: tuple[int, ...] + Tuple of integers representing the NVML version components. """ if not CUDA_BINDINGS_NVML_IS_COMPATIBLE: raise RuntimeError("NVML library is not available") @@ -97,6 +102,11 @@ def get_nvml_version() -> tuple[int, ...]: def get_driver_branch() -> str: """ Retrieves the driver branch of the NVIDIA driver installed on the system. + + Returns + ------- + branch: str + The driver branch string (e.g., ``"560"``, ``"open"``, etc.). """ if not CUDA_BINDINGS_NVML_IS_COMPATIBLE: raise RuntimeError("NVML library is not available") diff --git a/cuda_core/docs/source/getting-started.rst b/cuda_core/docs/source/getting-started.rst index 1761f2cc37c..7ded390b65c 100644 --- a/cuda_core/docs/source/getting-started.rst +++ b/cuda_core/docs/source/getting-started.rst @@ -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 .. currentmodule:: cuda.core @@ -68,7 +68,7 @@ Don't forget to use :meth:`Device.set_current`! s = dev.create_stream() Next, we compile the CUDA C++ kernel from earlier using the :class:`Program` class. -The result of the compilation is saved as a CUBIN. +The result of the compilation is saved as a CUBIN. Note the use of the ``name_expressions`` parameter to the :meth:`Program.compile` method to specify which kernel template instantiations to compile: .. code-block:: python From 371fa42cadd6207274966f9c766daae79767eb23 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 1 May 2026 07:59:41 -0700 Subject: [PATCH 058/244] [FIX]: cuda.core: simplify `_check_runtime_error` logic (#2003) * cuda.core: prefer binding names for runtime errors Use the generated runtime error enum as the name source for known CUDA Runtime errors so error messages remain stable when the runtime name table differs from the installed bindings. Made-with: Cursor * cuda.core: simplify runtime error naming path `_check_error()` only routes `runtime.cudaError_t` instances into `_check_runtime_error()`, so consulting `cudaGetErrorName()` and keeping a fallback for unknown values does not improve the normal `cuda.core` path. The Windows hybrid cudart issue is that the runtime name table can lag the generated enum table, so using `error.name` directly is both simpler and a better match for the values the code already has. With the runtime path now relying on enum members, the runtime-side tests no longer need to account for `UNEXPECTED ERROR CODE` in this loop or keep a separate monkeypatch test for avoiding the runtime name lookup. Made-with: Cursor --- cuda_core/cuda/core/_utils/cuda_utils.pyx | 7 +++---- cuda_core/tests/test_cuda_utils.py | 10 +--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/cuda_core/cuda/core/_utils/cuda_utils.pyx b/cuda_core/cuda/core/_utils/cuda_utils.pyx index 867d066ce2c..1bcfa524884 100644 --- a/cuda_core/cuda/core/_utils/cuda_utils.pyx +++ b/cuda_core/cuda/core/_utils/cuda_utils.pyx @@ -171,10 +171,9 @@ cpdef inline int _check_driver_error(cydriver.CUresult error) except?-1 nogil: cpdef inline int _check_runtime_error(error) except?-1: if error == _RUNTIME_SUCCESS: return 0 - name_err, name = runtime.cudaGetErrorName(error) - if name_err != _RUNTIME_SUCCESS: - raise CUDAError(f"UNEXPECTED ERROR CODE: {error}") - name = name.decode() + # `_check_error()` reaches this path only for `runtime.cudaError_t` values. + # Use the enum name directly because Windows hybrid cudart can lag that table. + name = error.name expl = RUNTIME_CUDA_ERROR_EXPLANATIONS.get(int(error)) if expl is not None: raise CUDAError(f"{name}: {expl}") diff --git a/cuda_core/tests/test_cuda_utils.py b/cuda_core/tests/test_cuda_utils.py index be22e57998b..32ea504248d 100644 --- a/cuda_core/tests/test_cuda_utils.py +++ b/cuda_core/tests/test_cuda_utils.py @@ -48,7 +48,6 @@ def test_check_driver_error(): def test_check_runtime_error(): - num_unexpected = 0 for error in runtime.cudaError_t: if error == runtime.cudaError_t.cudaSuccess: assert cuda_utils._check_runtime_error(error) == 0 @@ -56,14 +55,7 @@ def test_check_runtime_error(): with pytest.raises(cuda_utils.CUDAError) as e: cuda_utils._check_runtime_error(error) msg = str(e) - if "UNEXPECTED ERROR CODE" in msg: - num_unexpected += 1 - else: - # Example repr(error): - enum_name = repr(error).split(".", 1)[1].split(":", 1)[0] - assert enum_name in msg - # Smoke test: We don't want most to be unexpected. - assert num_unexpected < len(driver.CUresult) * 0.5 + assert error.name in msg def test_driver_error_enum_has_non_empty_docstring(): From ad9bc92cdc0084708e520b16033d0b20f26d2792 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Fri, 1 May 2026 13:31:57 -0700 Subject: [PATCH 059/244] Fix torch-incompatible assertions in TestViewCudaArrayInterfaceGPU (#1999) * Fix torch-incompatible assertions in TestViewCudaArrayInterfaceGPU The _check_view method in TestViewCudaArrayInterfaceGPU was missed during the tensor bridge refactor (#1894) and still used raw numpy attributes (in_arr.size, in_arr.strides, in_arr.flags, etc.) that don't work with torch tensors. Use the _arr_* helpers that #1894 added for torch/numpy compatibility. Caught by the nightly optional-dependency CI (#1987). Co-Authored-By: Claude Opus 4.6 (1M context) * Fix strides assertion for torch CAI: allow explicit C-contiguous strides torch's __cuda_array_interface__ always reports strides, even for C-contiguous tensors. Use the same assertion pattern as the other _check_view methods: allow strides to equal the C-contiguous values instead of requiring None. Verified locally: 7/7 torch CAI tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) * Unify strides assertion pattern across all _check_view methods Use the same if/else pattern with `in (None, strides_in_counts)` in all three _check_view methods for consistency. Previously TestViewCPU and TestViewCudaArrayInterfaceGPU used a one-liner that was harder to read and behaved slightly differently. Verified locally: 66/66 tests pass across TestViewCPU, TestViewGPU, and TestViewCudaArrayInterfaceGPU (including all torch variants). Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: flip strides assertion, add _arr_dtype, merge main Per @rwgk's review: - Flip strides check to branch on view.strides (all 3 _check_view) - Add _arr_dtype helper using __cuda_array_interface__["typestr"] for torch tensors, restore dtype assertion in CAI _check_view - Merge main to pick up #1998 (numba flags fix) Verified locally: 76/76 tests pass across all three test classes. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_core/tests/test_utils.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/cuda_core/tests/test_utils.py b/cuda_core/tests/test_utils.py index e5e47894646..5bcdead92c6 100644 --- a/cuda_core/tests/test_utils.py +++ b/cuda_core/tests/test_utils.py @@ -111,6 +111,12 @@ def _arr_is_writeable(arr): return arr.flags.writeable if hasattr(arr.flags, "writeable") else True +def _arr_dtype(arr): + if torch is not None and isinstance(arr, torch.Tensor): + return np.dtype(arr.__cuda_array_interface__["typestr"]) + return arr.dtype + + def _cpu_array_samples(): samples = [ np.empty(3, dtype=np.int32), @@ -171,7 +177,10 @@ def _check_view(self, view, in_arr): assert view.shape == expected_shape assert view.size == _arr_size(in_arr) strides_in_counts = _arr_strides_in_counts(in_arr) - assert (_arr_is_c_contiguous(in_arr) and view.strides is None) or view.strides == strides_in_counts + if view.strides is None: + assert _arr_is_c_contiguous(in_arr) + else: + assert view.strides == strides_in_counts assert view.device_id == -1 assert view.is_device_accessible is False assert view.exporting_obj is in_arr @@ -277,8 +286,8 @@ def _check_view(self, view, in_arr, dev): assert view.shape == expected_shape assert view.size == _arr_size(in_arr) strides_in_counts = _arr_strides_in_counts(in_arr) - if _arr_is_c_contiguous(in_arr): - assert view.strides in (None, strides_in_counts) + if view.strides is None: + assert _arr_is_c_contiguous(in_arr) else: assert view.strides == strides_in_counts assert view.device_id == dev.device_id @@ -343,15 +352,16 @@ def test_cuda_array_interface_gpu(self, in_arr, use_stream): def _check_view(self, view, in_arr, dev): assert isinstance(view, StridedMemoryView) - assert view.ptr == gpu_array_ptr(in_arr) - assert view.shape == in_arr.shape - assert view.size == in_arr.size - strides_in_counts = convert_strides_to_counts(in_arr.strides, in_arr.dtype.itemsize) - if in_arr.flags["C_CONTIGUOUS"]: - assert view.strides is None + assert view.ptr == _arr_ptr(in_arr) + expected_shape = tuple(in_arr.shape) + assert view.shape == expected_shape + assert view.size == _arr_size(in_arr) + strides_in_counts = _arr_strides_in_counts(in_arr) + if view.strides is None: + assert _arr_is_c_contiguous(in_arr) else: assert view.strides == strides_in_counts - assert view.dtype == in_arr.dtype + assert view.dtype == _arr_dtype(in_arr) assert view.device_id == dev.device_id assert view.is_device_accessible is True assert view.exporting_obj is in_arr From 2849053e0605658650c206453a87f0795a6a3100 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 1 May 2026 13:50:06 -0700 Subject: [PATCH 060/244] test: xfail Windows MCDM mempool OOM setup failures (#2000) * test: xfail Windows mempool OOM cases Work around nvbugs5815123 by treating OOM returns from mempool setup in affected tests as expected failures on Windows. Unsupported configurations still skip normally, while other platforms continue to fail on unexpected OOMs. Made-with: Cursor * test: limit mempool OOM xfail to MCDM Use NVML to confirm the CUDA device is running on Windows MCDM before treating mempool OOM setup failures as expected. If the MCDM check cannot be completed, leave the original test failure visible. Made-with: Cursor * test: centralize mempool OOM xfail helper Move the Windows MCDM detection and mempool OOM xfail handling into a shared test helper so cuda.bindings and cuda.core tests use the same workaround logic. Made-with: Cursor * test: keep MCDM detection fallback in xfail helper Let the MCDM detector report only the detected state and keep the broad fallback in the mempool OOM xfail path, where detection failures should leave the original test failure visible. Made-with: Cursor * test: simplify MCDM helper device lookup Use getattr for the shared mempool helper so it accepts device objects and raw ordinals without extra branching. Co-authored-by: Cursor * test: restore managed helper skip naming Keep the established managed-memory test helper name so call sites stay readable, while documenting that Windows MCDM mempool OOM setup failures are xfailed rather than skipped. Co-authored-by: Cursor * test: rename pinned helper for xfail flow Clarify pinned mempool test setup by keeping skip for capability checks and using xfail naming for the Windows MCDM constructor workaround. Co-authored-by: Cursor * test: tolerate missing mempool xfail helper Allow cuda_core tests to run against older cuda.bindings artifacts by falling back when the mempool xfail helper is unavailable, so collection succeeds without the new OOM xfail behavior. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../cuda/bindings/_test_helpers/mempool.py | 54 +++++++++++++++++++ cuda_bindings/tests/test_cuda.py | 8 +++ cuda_bindings/tests/test_cudart.py | 2 + cuda_core/tests/conftest.py | 49 ++++++++++++++++- .../tests/test_managed_memory_warning.py | 10 +++- cuda_core/tests/test_memory.py | 7 +-- 6 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 cuda_bindings/cuda/bindings/_test_helpers/mempool.py diff --git a/cuda_bindings/cuda/bindings/_test_helpers/mempool.py b/cuda_bindings/cuda/bindings/_test_helpers/mempool.py new file mode 100644 index 00000000000..deee79f1aff --- /dev/null +++ b/cuda_bindings/cuda/bindings/_test_helpers/mempool.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import sys + +import pytest + +from cuda.bindings import driver, runtime + + +def is_windows_mcdm_device(device=0): + if sys.platform != "win32": + return False + import cuda.bindings.nvml as nvml + + device_id = int(getattr(device, "device_id", device)) + (err,) = driver.cuInit(0) + if err != driver.CUresult.CUDA_SUCCESS: + return False + err, pci_bus_id = driver.cuDeviceGetPCIBusId(13, device_id) + if err != driver.CUresult.CUDA_SUCCESS: + return False + pci_bus_id = pci_bus_id.split(b"\x00", 1)[0].decode("ascii") + nvml.init_v2() + try: + handle = nvml.device_get_handle_by_pci_bus_id_v2(pci_bus_id) + current, _ = nvml.device_get_driver_model_v2(handle) + return current == nvml.DriverModel.DRIVER_MCDM + finally: + nvml.shutdown() + + +def xfail_if_mempool_oom(err_or_exc, api_name=None, device=0): + if api_name is not None and not isinstance(api_name, str): + device = api_name + api_name = None + + is_oom = err_or_exc in ( + driver.CUresult.CUDA_ERROR_OUT_OF_MEMORY, + runtime.cudaError_t.cudaErrorMemoryAllocation, + ) or "CUDA_ERROR_OUT_OF_MEMORY" in str(err_or_exc) + + if not is_oom: + return + try: + is_windows_mcdm = is_windows_mcdm_device(device) + except Exception: + # If MCDM detection fails, leave the primary test failure visible. + return + if not is_windows_mcdm: + return + + api_context = f"{api_name} " if api_name else "" + pytest.xfail(f"{api_context}could not reserve VA for mempool operations on Windows MCDM") diff --git a/cuda_bindings/tests/test_cuda.py b/cuda_bindings/tests/test_cuda.py index e3eefb1fdd7..e12d53d9665 100644 --- a/cuda_bindings/tests/test_cuda.py +++ b/cuda_bindings/tests/test_cuda.py @@ -12,6 +12,7 @@ import cuda.bindings.driver as cuda import cuda.bindings.runtime as cudart from cuda.bindings import driver +from cuda.bindings._test_helpers.mempool import xfail_if_mempool_oom def driverVersionLessThan(target): @@ -270,6 +271,7 @@ def test_cuda_memPool_attr(): attr_list = [None] * 8 err, pool = cuda.cuMemPoolCreate(poolProps) + xfail_if_mempool_oom(err, "cuMemPoolCreate", poolProps.location.id) assert err == cuda.CUresult.CUDA_SUCCESS for idx, attr in enumerate( @@ -468,6 +470,12 @@ def test_cuda_graphMem_attr(device): params.bytesize = allocSize err, allocNode = cuda.cuGraphAddMemAllocNode(graph, None, 0, params) + if err == cuda.CUresult.CUDA_ERROR_OUT_OF_MEMORY: + (destroy_err,) = cuda.cuGraphDestroy(graph) + assert destroy_err == cuda.CUresult.CUDA_SUCCESS + (destroy_err,) = cuda.cuStreamDestroy(stream) + assert destroy_err == cuda.CUresult.CUDA_SUCCESS + xfail_if_mempool_oom(err, "cuGraphAddMemAllocNode", device) assert err == cuda.CUresult.CUDA_SUCCESS err, freeNode = cuda.cuGraphAddMemFreeNode(graph, [allocNode], 1, params.dptr) assert err == cuda.CUresult.CUDA_SUCCESS diff --git a/cuda_bindings/tests/test_cudart.py b/cuda_bindings/tests/test_cudart.py index 3fa5594a262..144d7e75b12 100644 --- a/cuda_bindings/tests/test_cudart.py +++ b/cuda_bindings/tests/test_cudart.py @@ -11,6 +11,7 @@ import cuda.bindings.runtime as cudart from cuda import pathfinder from cuda.bindings import runtime +from cuda.bindings._test_helpers.mempool import xfail_if_mempool_oom def isSuccess(err): @@ -432,6 +433,7 @@ def test_cudart_MemPool_attr(): attr_list = [None] * 8 err, pool = cudart.cudaMemPoolCreate(poolProps) + xfail_if_mempool_oom(err, "cudaMemPoolCreate", poolProps.location.id) assertSuccess(err) for idx, attr in enumerate( diff --git a/cuda_core/tests/conftest.py b/cuda_core/tests/conftest.py index 85c5e75ff78..9f48686c30c 100644 --- a/cuda_core/tests/conftest.py +++ b/cuda_core/tests/conftest.py @@ -27,7 +27,17 @@ PinnedMemoryResourceOptions, _device, ) -from cuda.core._utils.cuda_utils import handle_return +from cuda.core._utils.cuda_utils import CUDAError, handle_return + +try: + from cuda.bindings._test_helpers.mempool import xfail_if_mempool_oom +except ModuleNotFoundError: + # Older cuda.bindings artifacts (for example 12.9.x backports) do not ship + # this helper yet. In that case, keep the primary failure visible instead of + # xfail-ing the known Windows MCDM mempool setup issue. + def xfail_if_mempool_oom(err_or_exc, api_name=None, device=0): + return + # Import shared test helpers for tests across subprojects. # PLEASE KEEP IN SYNC with copies in other conftest.py in this repo. @@ -61,21 +71,56 @@ def skip_if_managed_memory_unsupported(device): pytest.skip("ManagedMemoryResource requires CUDA 13.0 or later") try: ManagedMemoryResource() + except CUDAError as e: + xfail_if_mempool_oom(e, device) + raise except RuntimeError as e: if "requires CUDA 13.0" in str(e): pytest.skip("ManagedMemoryResource requires CUDA 13.0 or later") raise -def create_managed_memory_resource_or_skip(*args, **kwargs): +def create_managed_memory_resource_or_skip(*args, xfail_device=None, **kwargs): + # Keep the established "skip" helper name for call-site readability, even though + # Windows MCDM mempool OOM setup failures are xfailed instead of skipped. try: return ManagedMemoryResource(*args, **kwargs) + except CUDAError as e: + xfail_if_mempool_oom(e, _device_id_from_resource_options(xfail_device, args, kwargs)) + raise except RuntimeError as e: if "requires CUDA 13.0" in str(e): pytest.skip("ManagedMemoryResource requires CUDA 13.0 or later") raise +def create_pinned_memory_resource_or_xfail(*args, xfail_device=None, **kwargs): + try: + return PinnedMemoryResource(*args, **kwargs) + except CUDAError as e: + xfail_if_mempool_oom(e, xfail_device) + raise + + +def _device_id_from_resource_options(device, args, kwargs): + if device is not None: + return device + options = kwargs.get("options") + if options is None and args: + options = args[0] + if options is None: + return 0 + if isinstance(options, dict): + preferred_location = options.get("preferred_location") + preferred_location_type = options.get("preferred_location_type") + else: + preferred_location = getattr(options, "preferred_location", None) + preferred_location_type = getattr(options, "preferred_location_type", None) + if preferred_location_type in (None, "device") and isinstance(preferred_location, int) and preferred_location >= 0: + return preferred_location + return 0 + + @pytest.fixture(scope="session", autouse=True) def session_setup(): # Always init CUDA. diff --git a/cuda_core/tests/test_managed_memory_warning.py b/cuda_core/tests/test_managed_memory_warning.py index 78015978e72..5e6032ebe9e 100644 --- a/cuda_core/tests/test_managed_memory_warning.py +++ b/cuda_core/tests/test_managed_memory_warning.py @@ -13,8 +13,10 @@ import pytest import cuda.bindings +from conftest import xfail_if_mempool_oom from cuda.core import Device, ManagedMemoryResource, ManagedMemoryResourceOptions from cuda.core._memory._managed_memory_resource import reset_concurrent_access_warning +from cuda.core._utils.cuda_utils import CUDAError _cuda_major = int(cuda.bindings.__version__.split(".")[0]) @@ -47,8 +49,12 @@ def device_without_concurrent_managed_access(init_cuda): @requires_cuda_13 def test_default_pool_error_without_concurrent_access(device_without_concurrent_managed_access): """ManagedMemoryResource() raises RuntimeError when the default pool doesn't support managed.""" - with pytest.raises(RuntimeError, match="does not support managed allocations"): - ManagedMemoryResource() + try: + with pytest.raises(RuntimeError, match="does not support managed allocations"): + ManagedMemoryResource() + except CUDAError as exc: + xfail_if_mempool_oom(exc, device_without_concurrent_managed_access) + raise @requires_cuda_13 diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index 85dd4a7ea2b..fb99895616d 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -22,6 +22,7 @@ from conftest import ( create_managed_memory_resource_or_skip, + create_pinned_memory_resource_or_xfail, skip_if_managed_memory_unsupported, skip_if_pinned_memory_unsupported, ) @@ -639,7 +640,7 @@ def test_non_managed_resources_report_not_managed(mr_kind): mr = DeviceMemoryResource(device) else: skip_if_pinned_memory_unsupported(device) - mr = PinnedMemoryResource() + mr = create_pinned_memory_resource_or_xfail(xfail_device=device) assert mr.is_managed is False buf = mr.allocate(1024) assert buf.is_managed is False @@ -684,7 +685,7 @@ def test_pinned_memory_resource_initialization(init_cuda): device.set_current() - mr = PinnedMemoryResource() + mr = create_pinned_memory_resource_or_xfail(xfail_device=device) assert mr.is_device_accessible assert mr.is_host_accessible @@ -1581,7 +1582,7 @@ def test_memory_resource_alloc_zero_bytes(init_cuda, memory_resource_factory): pytest.skip("Device does not support mempool operations") elif MR is PinnedMemoryResource: skip_if_pinned_memory_unsupported(device) - mr = MR() + mr = create_pinned_memory_resource_or_xfail(xfail_device=device) elif MR is ManagedMemoryResource: skip_if_managed_memory_unsupported(device) mr = create_managed_memory_resource_or_skip(MROps(preferred_location=device.device_id)) From 64e2e6a9d4e335b3a0f06f5a27610e6ced4133b0 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 4 May 2026 09:52:04 -0700 Subject: [PATCH 061/244] [no-ci] Trust any collaborator in restricted-paths guard (#2010) * Trust any collaborator in restricted-paths guard Restricted-paths review is only meant for authors outside the collaborator set, so read and triage access should count as trusted signals too. Co-authored-by: Cursor * Post PR comment when restricted-paths review is required Make it easier to discover why Needs-Restricted-Paths-Review was applied by posting a short PR comment with a link to the workflow run summary whenever the label is newly added. Co-authored-by: Cursor * TEMPORARY: Switch to pull_request trigger for testing This commit is for testing the collaborator permission check and must be reverted before merge: 1. Changes trigger from pull_request_target to pull_request so this branch's workflow definition runs instead of main's. 2. Adds a dummy change to cuda_bindings/pyproject.toml to trigger the restricted-paths detection. REVERT THIS COMMIT BEFORE MERGE. Made-with: Cursor * TEMPORARY: Exclude write permission from trusted collaborators This commit is for testing the label-and-comment path and must be reverted before merge. It temporarily treats write access as untrusted so the current PR will exercise Needs-Restricted-Paths-Review assignment again. Co-authored-by: Cursor * Revert "TEMPORARY: Exclude write permission from trusted collaborators" This reverts commit 31ddac24cd64c5077af4860d7a497a8fbace0d96. * Revert "TEMPORARY: Switch to pull_request trigger for testing" This reverts commit 9b2fcd8ed0f54a8876dfaf00abaf3f70b76685fd. --------- Co-authored-by: Cursor --- .github/workflows/restricted-paths-guard.yml | 23 ++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/restricted-paths-guard.yml b/.github/workflows/restricted-paths-guard.yml index 8d7e6eccd4f..80e28dc050c 100644 --- a/.github/workflows/restricted-paths-guard.yml +++ b/.github/workflows/restricted-paths-guard.yml @@ -29,6 +29,7 @@ jobs: PR_AUTHOR: ${{ github.event.pull_request.user.login }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_URL: ${{ github.event.pull_request.html_url }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} # Workflow policy inputs REVIEW_LABEL: Needs-Restricted-Paths-Review @@ -113,9 +114,25 @@ jobs: echo '```' } + post_review_label_comment() { + local comment_body + printf -v comment_body '%s\n\n%s\n' \ + "\`$REVIEW_LABEL\` was assigned by \`CI: Restricted Paths Guard\`." \ + "For details, open [this workflow run]($RUN_URL) and click **Summary**." + + if gh api "repos/$REPO/issues/$PR_NUMBER/comments" \ + -f body="$comment_body" >/dev/null; then + COMMENT_ACTION="posted" + else + COMMENT_ACTION="failed (non-fatal)" + echo "::warning::Failed to post PR comment about newly added $REVIEW_LABEL." + fi + } + HAS_TRUSTED_SIGNAL=false LABEL_ACTION="not needed (no restricted paths)" TRUSTED_SIGNALS="(none)" + COMMENT_ACTION="not needed" if [ "$TOUCHES_RESTRICTED_PATHS" = "true" ]; then # Distinguish a legitimate 404 "not a collaborator" response from @@ -149,13 +166,13 @@ jobs: fi case "$COLLABORATOR_PERMISSION" in - admin|maintain|write) + admin|maintain|write|triage|read) HAS_TRUSTED_SIGNAL=true LABEL_ACTION="not needed (collaborator permission is a trusted signal)" TRUSTED_SIGNALS="collaborator_permission:$COLLABORATOR_PERMISSION" ;; *) - # triage, read, or none: not a trusted signal + # none: not a trusted signal ;; esac fi @@ -189,6 +206,7 @@ jobs: exit 1 else LABEL_ACTION="added" + post_review_label_comment fi elif [ "$LABEL_ALREADY_PRESENT" = "true" ]; then LABEL_ACTION="left in place (manual removal required)" @@ -203,6 +221,7 @@ jobs: echo "- **Restricted paths**: \`cuda_bindings/\`, \`cuda_python/\`" echo "- **Trusted signals**: $TRUSTED_SIGNALS" echo "- **Label action**: $LABEL_ACTION" + echo "- **Comment action**: $COMMENT_ACTION" if [ "$TOUCHES_RESTRICTED_PATHS" = "true" ]; then echo "" write_matching_restricted_paths From 7bd6397f4e223fa4a0eeefab01d20820d1568539 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Mon, 4 May 2026 17:48:15 -0700 Subject: [PATCH 062/244] fix(test): unblock root pixi run test workflow (#1978) * fix(test): unblock root pixi run test workflow `pixi run test` failed at the cuda_bindings build stage because list-form pixi `cmd` arrays didn't expand `$PIXI_ENVIRONMENT_NAME` reliably, so the inner per-package `pixi run` calls picked the cuda_bindings default environment (no cuda-version pin). The conda solver then resolved cuda-version=12.9 and the build failed with a missing `CUatomicOperation_enum` (a CUDA-13.x-only symbol). Wrap the three test-* tasks in `bash -c '...'` so the shell expands `$PIXI_ENVIRONMENT_NAME` and forward it explicitly via `-e` to each inner pixi run. Once the bindings build was unblocked, cuda_core's cython test build hit a second issue: `cythonize` cannot resolve `cimport cuda.bindings.*` against pixi-build's editable install, which exposes the cuda namespace package via a finder hook that Cython's filesystem .pxd resolver does not consult. Replace the `cythonize` CLI invocation with a small Python wrapper that calls `Cython.Build.cythonize()` with an explicit `include_path` resolved from the imported `cuda.bindings` package. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(test): satisfy ruff isort grouping in build_tests.py Co-Authored-By: Claude Opus 4.7 (1M context) * fix(test): replace cython build wrapper with PYTHONPATH shim Drop cuda_core/tests/cython/build_tests.py in favor of a small PYTHONPATH shim in build_tests.sh. Same outcome (Cython's .pxd resolver finds cuda.bindings via the package's parent directory), three lines instead of a separate setuptools entry point. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(test): use Cython include_path wrapper for editable installs Replace the PYTHONPATH shim in cuda_core/tests/cython/build_tests.sh with a small Python driver (build_tests.py) that calls Cython.Build.cythonize() with an explicit include_path resolved at runtime from cuda.bindings.__file__. Avoids platform-specific PYTHONPATH separator handling and surfaces missing-import failures as Python exceptions instead of silent fallbacks. Apply the same wrapper pattern to cuda_bindings/tests/cython/ for symmetry; both shell scripts gain `set -eo pipefail` and `${VAR:-}` defaults so the previously optional CPLUS_INCLUDE_PATH / CL env vars keep working under stricter error mode. Expand the pixi.toml comment block to document why each test-* task is wrapped in `bash -c '...'` and note Linux-only scope. Verified end-to-end: `pixi run test` passes 4,314 tests (974 pathfinder + 384 bindings + 2,956 core), 208 skipped, 2 xfailed. Co-Authored-By: Claude Opus 4.7 (1M context) * style: ruff isort grouping and format in build_tests.py Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- cuda_bindings/tests/cython/build_tests.py | 53 +++++++++++++++++++++++ cuda_bindings/tests/cython/build_tests.sh | 13 ++++-- cuda_core/tests/cython/build_tests.py | 52 ++++++++++++++++++++++ cuda_core/tests/cython/build_tests.sh | 10 +++-- pixi.toml | 36 +++++++++------ 5 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 cuda_bindings/tests/cython/build_tests.py create mode 100644 cuda_core/tests/cython/build_tests.py diff --git a/cuda_bindings/tests/cython/build_tests.py b/cuda_bindings/tests/cython/build_tests.py new file mode 100644 index 00000000000..53e66070634 --- /dev/null +++ b/cuda_bindings/tests/cython/build_tests.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +"""Build cuda_bindings Cython test extensions in-place. + +pixi-build's editable install exposes the `cuda` namespace package via a +PEP 660 finder hook. Python's import machinery honors the hook, but +Cython's filesystem .pxd resolver only walks real directories on sys.path, +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. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from Cython.Build import cythonize +from setuptools import setup + +import cuda.bindings + + +def _bindings_source_root() -> Path: + # cuda.bindings.__file__ -> ...//cuda/bindings/__init__.py + root = Path(cuda.bindings.__file__).resolve().parents[2] + if not (root / "cuda" / "bindings").is_dir(): + raise RuntimeError( + f"cuda.bindings source tree not found at {root}; pixi-build editable install layout may have changed." + ) + return root + + +def main() -> None: + script_dir = Path(__file__).resolve().parent + pyx_files = sorted(str(p) 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, + nthreads=1, + include_path=[str(_bindings_source_root())], + compiler_directives={"freethreading_compatible": True}, + ) + + sys.argv = [sys.argv[0], "build_ext", "--inplace"] + setup(name="cuda_bindings_cython_tests", ext_modules=ext_modules) + + +if __name__ == "__main__": + main() diff --git a/cuda_bindings/tests/cython/build_tests.sh b/cuda_bindings/tests/cython/build_tests.sh index c2ddc9ea79d..12cf46f7ffa 100755 --- a/cuda_bindings/tests/cython/build_tests.sh +++ b/cuda_bindings/tests/cython/build_tests.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -eo pipefail # SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE @@ -6,13 +7,17 @@ UNAME=$(uname) if [ "$UNAME" == "Linux" ] ; then SCRIPTPATH=$(dirname $(realpath "$0")) - export CPLUS_INCLUDE_PATH=$CUDA_HOME/include:$CPLUS_INCLUDE_PATH + export CPLUS_INCLUDE_PATH=$CUDA_HOME/include:${CPLUS_INCLUDE_PATH:-} elif [[ "$UNAME" == CYGWIN* || "$UNAME" == MINGW* || "$UNAME" == MSYS* ]] ; then SCRIPTPATH="$(dirname $(cygpath -w $(realpath "$0")))" - export CL="/I\"${CUDA_HOME}\\include\" ${CL}" + export CL="/I\"${CUDA_HOME}\\include\" ${CL:-}" else exit 1 fi -# Use -j 1 to side-step any process-pool issues and ensure deterministic single-threaded builds -cythonize -3 -j 1 -i -Xfreethreading_compatible=True ${SCRIPTPATH}/test_*.pyx +# Use a Python driver so the cuda.bindings source root is resolved at +# runtime and passed via Cython's include_path -- avoids platform-specific +# PYTHONPATH separator handling and surfaces import errors as exceptions. +# nthreads=1 inside the driver mirrors the previous `-j 1` to side-step +# any process-pool issues and keep builds deterministic. +python "${SCRIPTPATH}/build_tests.py" diff --git a/cuda_core/tests/cython/build_tests.py b/cuda_core/tests/cython/build_tests.py new file mode 100644 index 00000000000..af08ab25ea4 --- /dev/null +++ b/cuda_core/tests/cython/build_tests.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Build cuda_core Cython test extensions in-place. + +pixi-build's editable install exposes the `cuda` namespace package via a +PEP 660 finder hook. Python's import machinery honors the hook, but +Cython's filesystem .pxd resolver only walks real directories on sys.path, +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. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from Cython.Build import cythonize +from setuptools import setup + +import cuda.bindings + + +def _bindings_source_root() -> Path: + # cuda.bindings.__file__ -> ...//cuda/bindings/__init__.py + root = Path(cuda.bindings.__file__).resolve().parents[2] + if not (root / "cuda" / "bindings").is_dir(): + raise RuntimeError( + f"cuda.bindings source tree not found at {root}; pixi-build editable install layout may have changed." + ) + return root + + +def main() -> None: + script_dir = Path(__file__).resolve().parent + pyx_files = sorted(str(p) 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}, + ) + + sys.argv = [sys.argv[0], "build_ext", "--inplace"] + setup(name="cuda_core_cython_tests", ext_modules=ext_modules) + + +if __name__ == "__main__": + main() diff --git a/cuda_core/tests/cython/build_tests.sh b/cuda_core/tests/cython/build_tests.sh index 3e20136133a..7ee65c50d68 100755 --- a/cuda_core/tests/cython/build_tests.sh +++ b/cuda_core/tests/cython/build_tests.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -eo pipefail # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 @@ -6,13 +7,16 @@ UNAME=$(uname) if [ "$UNAME" == "Linux" ] ; then SCRIPTPATH=$(dirname $(realpath "$0")) - export CPLUS_INCLUDE_PATH=${SCRIPTPATH}/../../cuda/core/_include:$CUDA_HOME/include:$CPLUS_INCLUDE_PATH + export CPLUS_INCLUDE_PATH=${SCRIPTPATH}/../../cuda/core/_include:$CUDA_HOME/include:${CPLUS_INCLUDE_PATH:-} elif [[ "$UNAME" == CYGWIN* || "$UNAME" == MINGW* || "$UNAME" == MSYS* ]] ; then SCRIPTPATH="$(dirname $(cygpath -w $(realpath "$0")))" CUDA_CORE_INCLUDE_PATH=$(echo "${SCRIPTPATH}\..\..\cuda\core\_include" | sed 's/\\/\\\\/g') - export CL="/I\"${CUDA_CORE_INCLUDE_PATH}\" /I\"${CUDA_HOME}\\include\" ${CL}" + export CL="/I\"${CUDA_CORE_INCLUDE_PATH}\" /I\"${CUDA_HOME}\\include\" ${CL:-}" else exit 1 fi -cythonize -3 -i -Xfreethreading_compatible=True ${SCRIPTPATH}/test_*.pyx +# Use a Python driver so the cuda.bindings source root is resolved at +# runtime and passed via Cython's include_path -- avoids platform-specific +# PYTHONPATH separator handling and surfaces import errors as exceptions. +python "${SCRIPTPATH}/build_tests.py" diff --git a/pixi.toml b/pixi.toml index 71d6b5d91bc..f73d299e012 100644 --- a/pixi.toml +++ b/pixi.toml @@ -18,30 +18,40 @@ docs = { features = [], solve-group = "docs" } PIXI_ENVIRONMENT_NAME = "${PIXI_ENVIRONMENT_NAME/default/cu13}" # Test Tasks -# Runs tests across all sub-packages: pathfinder → bindings → core (dependency order) -# Each sub-package has its own pixi.toml; the -e environment propagates via PIXI_ENVIRONMENT_NAME +# Runs tests across all sub-packages: pathfinder → bindings → core (dependency order). +# +# Each sub-package has its own pixi.toml. We wrap each task in `bash -c '...'` +# (single-quoted in TOML, double-quoted inside) because pixi `cmd` arrays do +# not expand shell variables -- without this, the inner `pixi run` would drop +# into the sub-package's `default` environment, which lacks a cuda-version +# pin, causing the conda solver to pick a mismatched CUDA toolkit. Forwarding +# `-e "$PIXI_ENVIRONMENT_NAME"` keeps the active environment consistent end +# to end. +# +# Linux-only for now; Windows/macOS root test-task parity is tracked +# separately. # # Usage: pixi run test | pixi run -e cu12 test | pixi run -e cu13 test [target.linux.tasks.test-pathfinder] cmd = [ - "pixi", - "run", - "--manifest-path", - "$PIXI_PROJECT_ROOT/cuda_pathfinder", - "test", + "bash", + "-c", + 'pixi run --manifest-path "$PIXI_PROJECT_ROOT/cuda_pathfinder" -e "$PIXI_ENVIRONMENT_NAME" test', ] [target.linux.tasks.test-bindings] cmd = [ - "pixi", - "run", - "--manifest-path", - "$PIXI_PROJECT_ROOT/cuda_bindings", - "test", + "bash", + "-c", + 'pixi run --manifest-path "$PIXI_PROJECT_ROOT/cuda_bindings" -e "$PIXI_ENVIRONMENT_NAME" test', ] [target.linux.tasks.test-core] -cmd = ["pixi", "run", "--manifest-path", "$PIXI_PROJECT_ROOT/cuda_core", "test"] +cmd = [ + "bash", + "-c", + 'pixi run --manifest-path "$PIXI_PROJECT_ROOT/cuda_core" -e "$PIXI_ENVIRONMENT_NAME" test', +] [target.linux.tasks.test] depends-on = [ From ecd558af05f8b5ec7243ee638920743518d66823 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Mon, 4 May 2026 21:18:57 -0400 Subject: [PATCH 063/244] Re-wrap most of the enums in cuda.bindings.nvml for cuda.core.system. (#2014) * Re-wrap most of the enums in cuda.bindings.nvml for cuda.core.system. * Fixes for Python 3.10 * Fix Windows test * Line wrapping * Fix typo * Address comments in robo-review * Add assert * Add sync checks * Fix spacing * Clean up test --- cuda_core/cuda/core/system/_clock.pxi | 110 ++++- cuda_core/cuda/core/system/_cooler.pxi | 64 ++- cuda_core/cuda/core/system/_device.pyx | 375 +++++++++++++++--- cuda_core/cuda/core/system/_event.pxi | 87 +++- cuda_core/cuda/core/system/_fan.pxi | 15 +- cuda_core/cuda/core/system/_inforom.pxi | 34 +- cuda_core/cuda/core/system/_nvlink.pxi | 26 +- cuda_core/cuda/core/system/_pci_info.pxi | 25 +- cuda_core/cuda/core/system/_system_events.pyx | 52 ++- cuda_core/cuda/core/system/_temperature.pxi | 174 +++++++- cuda_core/docs/source/api.rst | 28 +- cuda_core/docs/source/api_private.rst | 29 +- cuda_core/pixi.toml | 3 + cuda_core/pyproject.toml | 1 + cuda_core/tests/system/test_system_device.py | 49 +-- cuda_core/tests/system/test_system_events.py | 2 +- cuda_core/tests/test_enum_coverage.py | 275 +++++++++++++ 17 files changed, 1149 insertions(+), 200 deletions(-) create mode 100644 cuda_core/tests/test_enum_coverage.py diff --git a/cuda_core/cuda/core/system/_clock.pxi b/cuda_core/cuda/core/system/_clock.pxi index 5d08d2a5676..d30a4192afe 100644 --- a/cuda_core/cuda/core/system/_clock.pxi +++ b/cuda_core/cuda/core/system/_clock.pxi @@ -3,9 +3,71 @@ # SPDX-License-Identifier: Apache-2.0 -ClockId = nvml.ClockId -ClocksEventReasons = nvml.ClocksEventReasons -ClockType = nvml.ClockType +class ClockId(StrEnum): + """ + Clock Ids. These are used in combination with :class:`ClockType` to specify a single clock value. + """ + CURRENT = "current" + CUSTOMER_BOOST_MAX = "customer_boost_max" + # APP_CLOCK_TARGET and APP_CLOCK_DEFAULT are deprecated so not included here + + +ClockId.CURRENT.__doc__ = "Current actual clock value." +ClockId.CUSTOMER_BOOST_MAX.__doc__ = "OEM-defined maximum clock rate" + + +_CLOCK_ID_MAPPING = { + ClockId.CURRENT: nvml.ClockId.CURRENT, + ClockId.CUSTOMER_BOOST_MAX: nvml.ClockId.CUSTOMER_BOOST_MAX, +} + + +class ClocksEventReasons(StrEnum): + """ + Reasons for a clocks event. These are used in combination with :class:`ClockType` to specify the reason for a clocks event. + """ + NONE = "none" + GPU_IDLE = "gpu_idle" + APPLICATIONS_CLOCKS_SETTING = "applications_clocks_setting" + SW_POWER_CAP = "sw_power_cap" + HW_SLOWDOWN = "hw_slowdown" + SYNC_BOOST = "sync_boost" + SW_THERMAL_SLOWDOWN = "sw_thermal_slowdown" + HW_THERMAL_SLOWDOWN = "hw_thermal_slowdown" + HW_POWER_BRAKE_SLOWDOWN = "hw_power_brake_slowdown" + DISPLAY_CLOCK_SETTING = "display_clock_setting" + + +_CLOCKS_EVENT_REASONS_MAPPING = { + nvml.ClocksEventReasons.EVENT_REASON_NONE: ClocksEventReasons.NONE, + nvml.ClocksEventReasons.EVENT_REASON_GPU_IDLE: ClocksEventReasons.GPU_IDLE, + nvml.ClocksEventReasons.EVENT_REASON_APPLICATIONS_CLOCKS_SETTING: ClocksEventReasons.APPLICATIONS_CLOCKS_SETTING, + nvml.ClocksEventReasons.EVENT_REASON_SW_POWER_CAP: ClocksEventReasons.SW_POWER_CAP, + nvml.ClocksEventReasons.THROTTLE_REASON_HW_SLOWDOWN: ClocksEventReasons.HW_SLOWDOWN, + nvml.ClocksEventReasons.EVENT_REASON_SYNC_BOOST: ClocksEventReasons.SYNC_BOOST, + nvml.ClocksEventReasons.EVENT_REASON_SW_THERMAL_SLOWDOWN: ClocksEventReasons.SW_THERMAL_SLOWDOWN, + nvml.ClocksEventReasons.THROTTLE_REASON_HW_THERMAL_SLOWDOWN: ClocksEventReasons.HW_THERMAL_SLOWDOWN, + nvml.ClocksEventReasons.THROTTLE_REASON_HW_POWER_BRAKE_SLOWDOWN: ClocksEventReasons.HW_POWER_BRAKE_SLOWDOWN, + nvml.ClocksEventReasons.EVENT_REASON_DISPLAY_CLOCK_SETTING: ClocksEventReasons.DISPLAY_CLOCK_SETTING, +} + + +class ClockType(StrEnum): + """ + Clock types. All speeds are in Mhz. + """ + GRAPHICS = "graphics" + SM = "sm" + MEMORY = "memory" + VIDEO = "video" + + +_CLOCK_TYPE_MAPPING = { + ClockType.GRAPHICS: nvml.ClockType.CLOCK_GRAPHICS, + ClockType.SM: nvml.ClockType.CLOCK_SM, + ClockType.MEMORY: nvml.ClockType.CLOCK_MEM, + ClockType.VIDEO: nvml.ClockType.CLOCK_VIDEO, +} cdef class ClockOffsets: @@ -48,11 +110,18 @@ cdef class ClockInfo: cdef intptr_t _handle cdef int _clock_type - def __init__(self, handle, clock_type: ClockType): + def __init__(self, handle, clock_type: ClockType | str): self._handle = handle + try: + clock_type = _CLOCK_TYPE_MAPPING[clock_type] + except KeyError: + raise ValueError( + f"Invalid clock type: {clock_type}. " + f"Must be one of {list(ClockType.__members__.values())}" + ) from None self._clock_type = int(clock_type) - def get_current_mhz(self, clock_id: ClockId = ClockId.CURRENT) -> int: + def get_current_mhz(self, clock_id: ClockId | str = ClockId.CURRENT) -> int: """ Get the current clock speed of a specific clock domain, in MHz. @@ -60,14 +129,21 @@ cdef class ClockInfo: Parameters ---------- - clock_id: :class:`ClockId` - The clock ID to query. + clock_id: :class:`ClockId` | str + The clock ID to query. Defaults to the current clock value. Returns ------- int The clock speed in MHz. """ + try: + clock_id = _CLOCK_ID_MAPPING[clock_id] + except KeyError: + raise ValueError( + f"Invalid clock ID: {clock_id}. " + f"Must be one of {list(ClockId.__members__.values())}" + ) from None return nvml.device_get_clock(self._handle, self._clock_type, clock_id) def get_max_mhz(self) -> int: @@ -99,24 +175,26 @@ cdef class ClockInfo: """ return nvml.device_get_max_customer_boost_clock(self._handle, self._clock_type) - def get_min_max_clock_of_pstate_mhz(self, pstate: Pstates) -> tuple[int, int]: + def get_min_max_clock_of_pstate_mhz(self, pstate: int) -> tuple[int, int]: """ Get the minimum and maximum clock speeds for this clock domain at a given performance state (Pstate), in MHz. Parameters ---------- - pstate: :class:`Pstates` - The performance state to query. + pstate: int + The performance state to query. Must be an int between 0 and 15, + where 0 is the highest performance state (P0) and 15 is the lowest + (P15). Returns ------- tuple[int, int] A tuple containing the minimum and maximum clock speeds in MHz. """ - return nvml.device_get_min_max_clock_of_p_state(self._handle, self._clock_type, pstate) + return nvml.device_get_min_max_clock_of_p_state(self._handle, self._clock_type, _pstate_to_enum(pstate)) - def get_offsets(self, pstate: Pstates) -> ClockOffsets: + def get_offsets(self, pstate: int) -> ClockOffsets: """ Retrieve min, max and current clock offset of some clock domain for a given Pstate. @@ -124,12 +202,14 @@ cdef class ClockInfo: Parameters ---------- - pstate: :class:`Pstates` - The performance state to query. + pstate: int + The performance state to query. Must be an int between 0 and 15, + where 0 is the highest performance state (P0) and 15 is the lowest + (P15). Returns ------- :obj:`~_device.ClockOffsets` An object with the min, max and current clock offset. """ - return ClockOffsets(nvml.device_get_clock_offsets(self._handle, self._clock_type, pstate)) + return ClockOffsets(nvml.device_get_clock_offsets(self._handle, self._clock_type, _pstate_to_enum(pstate))) diff --git a/cuda_core/cuda/core/system/_cooler.pxi b/cuda_core/cuda/core/system/_cooler.pxi index b3ab76939e8..cfce09bd284 100644 --- a/cuda_core/cuda/core/system/_cooler.pxi +++ b/cuda_core/cuda/core/system/_cooler.pxi @@ -3,8 +3,52 @@ # SPDX-License-Identifier: Apache-2.0 -CoolerControl = nvml.CoolerControl -CoolerTarget = nvml.CoolerTarget +class CoolerControl(StrEnum): + """ + Cooler control type. + """ + TOGGLE = "toggle" + VARIABLE = "variable" + + +CoolerControl.TOGGLE.__doc__ = """ +This cooler can only be toggled either ON or OFF (e.g. a switch). +""" +CoolerControl.VARIABLE.__doc__ = """ +This cooler's level can be adjusted from some minimum to some maximum (e.g. a knob). +""" + + +_COOLER_CONTROL_MAPPING = { + nvml.CoolerControl.THERMAL_COOLER_SIGNAL_TOGGLE: CoolerControl.TOGGLE, + nvml.CoolerControl.THERMAL_COOLER_SIGNAL_VARIABLE: CoolerControl.VARIABLE, +} + + +class CoolerTarget(StrEnum): + """ + Cooler target. + """ + NONE = "none" + GPU = "gpu" + MEMORY = "memory" + POWER_SUPPLY = "power_supply" + # THERMAL_GPU_RELATED is a composite target, so it is omitted here and will + # get returned as 3 separate targets: GPU, MEMORY, and POWER_SUPPLY. + + +CoolerTarget.NONE.__doc__ = "This cooler controls nothing." +CoolerTarget.GPU.__doc__ = "This cooler can cool the GPU." +CoolerTarget.MEMORY.__doc__ = "This cooler can cool the memory." +CoolerTarget.POWER_SUPPLY.__doc__ = "This cooler can cool the power supply." + + +_COOLER_TARGET_MAPPING = { + nvml.CoolerTarget.THERMAL_NONE: CoolerTarget.NONE, + nvml.CoolerTarget.THERMAL_GPU: CoolerTarget.GPU, + nvml.CoolerTarget.THERMAL_MEMORY: CoolerTarget.MEMORY, + nvml.CoolerTarget.THERMAL_POWER_SUPPLY: CoolerTarget.POWER_SUPPLY, +} cdef class CoolerInfo: @@ -14,14 +58,13 @@ cdef class CoolerInfo: self._cooler_info = cooler_info @property - def signal_type(self) -> CoolerControl: + def signal_type(self) -> CoolerControl | None: """ The cooler's control signal characteristics. - The possible types are restricted, variable and toggle. See - :class:`CoolerControl` for details. + The possible types are variable and toggle. """ - return CoolerControl(self._cooler_info.signal_type) + return _COOLER_CONTROL_MAPPING.get(self._cooler_info.signal_type, None) @property def target(self) -> list[CoolerTarget]: @@ -32,4 +75,11 @@ cdef class CoolerInfo: :class:`CoolerTarget` for details. """ cdef uint64_t[1] targets = [self._cooler_info.target] - return [CoolerTarget(1 << ev) for ev in _unpack_bitmask(targets)] + output_targets = [] + for target in _unpack_bitmask(targets): + try: + output_target = _COOLER_TARGET_MAPPING[1 << target] + except KeyError: + raise ValueError(f"Unknown cooler target bit: {1 << target}") + output_targets.append(output_target) + return output_targets diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index 8615be0c531..f3cc62fe546 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -5,22 +5,37 @@ from libc.stdint cimport intptr_t, uint64_t from libc.math cimport ceil +import sys +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from backports.strenum import StrEnum from multiprocessing import cpu_count from typing import Iterable +import warnings from cuda.bindings import nvml +try: + from cuda.bindings._internal._fast_enum import FastEnum +except ImportError: + from enum import IntEnum as FastEnum from ._nvml_context cimport initialize -AddressingMode = nvml.DeviceAddressingModeType -AffinityScope = nvml.AffinityScope -BrandType = nvml.BrandType -DeviceArch = nvml.DeviceArch -GpuP2PCapsIndex = nvml.GpuP2PCapsIndex -GpuP2PStatus = nvml.GpuP2PStatus -GpuTopologyLevel = nvml.GpuTopologyLevel -Pstates = nvml.Pstates +cdef object _pstate_to_int(object pstate): + if pstate == nvml.Pstates.PSTATE_UNKNOWN: + return None + assert ( + int(pstate) >= 0 and int(pstate) <= 15 + ), f"Invalid P-state: {pstate}. Must be between 0 and 15 inclusive, or PSTATE_UNKNOWN." + return int(pstate) - int(nvml.Pstates.PSTATE_0) + + +cdef int _pstate_to_enum(int pstate): + if pstate < 0 or pstate > 15: + raise ValueError(f"Invalid P-state: {pstate}. Must be between 0 and 15 inclusive.") + return int(pstate) + int(nvml.Pstates.PSTATE_0) include "_clock.pxi" @@ -42,6 +57,174 @@ include "_temperature.pxi" include "_utilization.pxi" +class AddressingMode(StrEnum): + """ + Addressing mode of a device. + + For Kepler™ or newer fully supported devices. + """ + HMM = "hmm" + ATS = "ats" + + +AddressingMode.HMM.__doc__ = """ + System allocated memory (``malloc``, ``mmap``) is addressable from the device + (GPU), via software-based mirroring of the CPU's page tables, on the GPU. +""" + + +AddressingMode.ATS.__doc__ = """ + System allocated memory (``malloc``, ``mmap``) is addressable from the device + (GPU), via Address Translation Services. This means that there is (effectively) + a single set of page tables, and the CPU and GPU both use them. +""" + + +_ADDRESSING_MODE_MAPPING = { + nvml.DeviceAddressingModeType.DEVICE_ADDRESSING_MODE_HMM: AddressingMode.HMM, + nvml.DeviceAddressingModeType.DEVICE_ADDRESSING_MODE_ATS: AddressingMode.ATS, +} + + +class AffinityScope(StrEnum): + """ + Scope for affinity queries. + """ + NODE = "node" + SOCKET = "socket" + + +AffinityScope.NODE.__doc__ = """ +The NUMA node is the scope of the affinity query. This is the default scope. +""" + + +AffinityScope.SOCKET.__doc__ = """ +The CPU socket is the scope of the affinity query. +""" + + +_AFFINITY_SCOPE_MAPPING = { + AffinityScope.NODE: nvml.AffinityScope.NODE, + AffinityScope.SOCKET: nvml.AffinityScope.SOCKET, +} + + +_BRAND_TYPE_MAPPING = { + nvml.BrandType.BRAND_UNKNOWN: "Unknown", + nvml.BrandType.BRAND_QUADRO: "Quadro", + nvml.BrandType.BRAND_TESLA: "Tesla", + nvml.BrandType.BRAND_NVS: "NVS", + nvml.BrandType.BRAND_GRID: "GRID", + nvml.BrandType.BRAND_GEFORCE: "GeForce", + nvml.BrandType.BRAND_TITAN: "Titan", + nvml.BrandType.BRAND_NVIDIA_VAPPS: "NVIDIA vApps", + nvml.BrandType.BRAND_NVIDIA_VPC: "NVIDIA VPC", + nvml.BrandType.BRAND_NVIDIA_VCS: "NVIDIA VCS", + nvml.BrandType.BRAND_NVIDIA_VWS: "NVIDIA VWS", + nvml.BrandType.BRAND_NVIDIA_CLOUD_GAMING: "NVIDIA Cloud Gaming", + nvml.BrandType.BRAND_NVIDIA_VGAMING: "NVIDIA vGaming", + nvml.BrandType.BRAND_QUADRO_RTX: "Quadro RTX", + nvml.BrandType.BRAND_NVIDIA_RTX: "NVIDIA RTX", + nvml.BrandType.BRAND_NVIDIA: "NVIDIA", + nvml.BrandType.BRAND_GEFORCE_RTX: "GeForce RTX", + nvml.BrandType.BRAND_TITAN_RTX: "Titan RTX", +} + + +# This uses FastEnum instead of StrEnum because the ordering of the values is +# meaningful, e.g. Kepler "or later" +class DeviceArch(FastEnum): + """ + Device architecture. + """ + KEPLER = int(nvml.DeviceArch.KEPLER) + MAXWELL = int(nvml.DeviceArch.MAXWELL) + PASCAL = int(nvml.DeviceArch.PASCAL) + VOLTA = int(nvml.DeviceArch.VOLTA) + TURING = int(nvml.DeviceArch.TURING) + AMPERE = int(nvml.DeviceArch.AMPERE) + ADA = int(nvml.DeviceArch.ADA) + HOPPER = int(nvml.DeviceArch.HOPPER) + BLACKWELL = int(nvml.DeviceArch.BLACKWELL) + UNKNOWN = int(nvml.DeviceArch.UNKNOWN) + + +class GpuP2PCapsIndex(StrEnum): + """ + GPU peer-to-peer capabilities index. + """ + READ = "read" + WRITE = "write" + NVLINK = "nvlink" + ATOMICS = "atomics" + PCI = "pci" + PROP = "prop" + UNKNOWN = "unknown" + + +_GPU_P2P_CAPS_INDEX_MAPPING = { + GpuP2PCapsIndex.READ: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_READ, + GpuP2PCapsIndex.WRITE: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_WRITE, + GpuP2PCapsIndex.NVLINK: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_NVLINK, + GpuP2PCapsIndex.ATOMICS: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_ATOMICS, + GpuP2PCapsIndex.PCI: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_PCI, + GpuP2PCapsIndex.PROP: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_PROP, +} + + +class GpuP2PStatus(StrEnum): + """ + GPU peer-to-peer status. + """ + OK = "ok" + CHIPSET_NOT_SUPPORTED = "chipset not supported" + GPU_NOT_SUPPORTED = "GPU not supported" + IOH_TOPOLOGY_NOT_SUPPORTED = "IOH topology not supported" + DISABLED_BY_REGKEY = "disabled by regkey" + NOT_SUPPORTED = "not supported" + UNKNOWN = "unknown" + + +_GPU_P2P_STATUS_MAPPING = { + nvml.GpuP2PStatus.P2P_STATUS_OK: GpuP2PStatus.OK, + # Typo in upstream library + nvml.GpuP2PStatus.P2P_STATUS_CHIPSET_NOT_SUPPORED: GpuP2PStatus.CHIPSET_NOT_SUPPORTED, + nvml.GpuP2PStatus.P2P_STATUS_CHIPSET_NOT_SUPPORTED: GpuP2PStatus.CHIPSET_NOT_SUPPORTED, + nvml.GpuP2PStatus.P2P_STATUS_GPU_NOT_SUPPORTED: GpuP2PStatus.GPU_NOT_SUPPORTED, + nvml.GpuP2PStatus.P2P_STATUS_IOH_TOPOLOGY_NOT_SUPPORTED: GpuP2PStatus.IOH_TOPOLOGY_NOT_SUPPORTED, + nvml.GpuP2PStatus.P2P_STATUS_DISABLED_BY_REGKEY: GpuP2PStatus.DISABLED_BY_REGKEY, + nvml.GpuP2PStatus.P2P_STATUS_NOT_SUPPORTED: GpuP2PStatus.NOT_SUPPORTED, + nvml.GpuP2PStatus.P2P_STATUS_UNKNOWN: GpuP2PStatus.UNKNOWN, +} + + +class GpuTopologyLevel(StrEnum): + """ + Represents level relationships within a system between two GPUs. + """ + INTERNAL = "internal" + SINGLE = "single" + MULTIPLE = "multiple" + HOSTBRIDGE = "hostbridge" + NODE = "node" + SYSTEM = "system" + + +_GPU_TOPOLOGY_LEVEL_MAPPING = { + GpuTopologyLevel.INTERNAL: nvml.GpuTopologyLevel.TOPOLOGY_INTERNAL, + GpuTopologyLevel.SINGLE: nvml.GpuTopologyLevel.TOPOLOGY_SINGLE, + GpuTopologyLevel.MULTIPLE: nvml.GpuTopologyLevel.TOPOLOGY_MULTIPLE, + GpuTopologyLevel.HOSTBRIDGE: nvml.GpuTopologyLevel.TOPOLOGY_HOSTBRIDGE, + GpuTopologyLevel.NODE: nvml.GpuTopologyLevel.TOPOLOGY_NODE, + GpuTopologyLevel.SYSTEM: nvml.GpuTopologyLevel.TOPOLOGY_SYSTEM, +} + + +_GPU_TOPOLOGY_LEVEL_INV_MAPPING = {v: k for k, v in _GPU_TOPOLOGY_LEVEL_MAPPING.items()} + + + cdef class Device: """ Representation of a device. @@ -184,7 +367,7 @@ cdef class Device: try: return DeviceArch(arch) except ValueError: - return nvml.DeviceArch.UNKNOWN + return DeviceArch.UNKNOWN @property def name(self) -> str: @@ -194,11 +377,13 @@ cdef class Device: return nvml.device_get_name(self._handle) @property - def brand(self) -> BrandType: + def brand(self) -> str: """ - :obj:`~BrandType` brand of the device + The brand of the device. + + Returns "Unknown" if the brand is unknown. """ - return BrandType(nvml.device_get_brand(self._handle)) + return _BRAND_TYPE_MAPPING.get(nvml.device_get_brand(self._handle), "Unknown") @property def serial(self) -> str: @@ -322,23 +507,11 @@ cdef class Device: # ADDRESSING MODE @property - def addressing_mode(self) -> AddressingMode: + def addressing_mode(self) -> AddressingMode | None: """ Get the :obj:`~AddressingMode` of the device. - - Addressing modes can be one of: - - - :attr:`AddressingMode.DEVICE_ADDRESSING_MODE_HMM`: System allocated - memory (``malloc``, ``mmap``) is addressable from the device (GPU), via - software-based mirroring of the CPU's page tables, on the GPU. - - :attr:`AddressingMode.DEVICE_ADDRESSING_MODE_ATS`: System allocated - memory (``malloc``, ``mmap``) is addressable from the device (GPU), via - Address Translation Services. This means that there is (effectively) a - single set of page tables, and the CPU and GPU both use them. - - :attr:`AddressingMode.DEVICE_ADDRESSING_MODE_NONE`: Neither HMM nor ATS - is active. """ - return AddressingMode(nvml.device_get_addressing_mode(self._handle).value) + return _ADDRESSING_MODE_MAPPING.get(nvml.device_get_addressing_mode(self._handle).value, None) ######################################################################### # MIG (MULTI-INSTANCE GPU) DEVICES @@ -378,7 +551,7 @@ cdef class Device: device._handle = handle yield device - def get_memory_affinity(self, scope: AffinityScope=AffinityScope.NODE) -> list[int]: + def get_memory_affinity(self, scope: AffinityScope | str=AffinityScope.NODE) -> list[int]: """ Retrieves a list of indices of NUMA nodes or CPU sockets with the ideal memory affinity for the device. @@ -390,16 +563,35 @@ cdef class Device: If requested scope is not applicable to the target topology, the API will fall back to reporting the memory affinity for the immediate non-I/O ancestor of the device. + + Parameters + ---------- + scope: AffinityScope | str, optional + The scope of the affinity query. Must be one of the values of + :class:`AffinityScope`. Default is :attr:`AffinityScope.NODE`. + + Returns + ------- + list[int] + A list of indices of NUMA nodes or CPU sockets with the ideal memory + affinity for the device. """ + try: + scope = _AFFINITY_SCOPE_MAPPING[scope] + except KeyError: + raise ValueError( + f"Invalid affinity scope: {scope}. " + f"Must be one of {list(AffinityScope.__members__.values())}" + ) from None return _unpack_bitmask( nvml.device_get_memory_affinity( self._handle, ceil(cpu_count() / 64), - scope + scope, ) ) - def get_cpu_affinity(self, scope: AffinityScope=AffinityScope.NODE) -> list[int]: + def get_cpu_affinity(self, scope: AffinityScope | str=AffinityScope.NODE) -> list[int]: """ Retrieves a list of indices of NUMA nodes or CPU sockets with the ideal CPU affinity for the device. @@ -411,7 +603,26 @@ cdef class Device: If requested scope is not applicable to the target topology, the API will fall back to reporting the memory affinity for the immediate non-I/O ancestor of the device. + + Parameters + ---------- + scope: AffinityScope | str, optional + The scope of the affinity query. Must be one of the values of + :class:`AffinityScope`. Default is :attr:`AffinityScope.NODE`. + + Returns + ------- + list[int] + A list of indices of NUMA nodes or CPU sockets with the ideal memory + affinity for the device. """ + try: + scope = _AFFINITY_SCOPE_MAPPING[scope] + except KeyError: + raise ValueError( + f"Invalid affinity scope: {scope}. " + f"Must be one of {list(AffinityScope.__members__.values())}" + ) from None return _unpack_bitmask( nvml.device_get_cpu_affinity_within_scope( self._handle, @@ -444,7 +655,7 @@ cdef class Device: # CLOCK # See external class definitions in _clock.pxi - def get_clock(self, clock_type: ClockType) -> ClockInfo: + def get_clock(self, clock_type: ClockType | str) -> ClockInfo: """ :obj:`~_device.ClockInfo` object to get information about and manage a specific clock on a device. """ @@ -485,7 +696,14 @@ cdef class Device: """ cdef uint64_t[1] reasons reasons[0] = nvml.device_get_current_clocks_event_reasons(self._handle) - return [ClocksEventReasons(1 << reason) for reason in _unpack_bitmask(reasons)] + output_reasons = [] + for reason in _unpack_bitmask(reasons): + try: + output_reason = _CLOCKS_EVENT_REASONS_MAPPING[1 << reason] + except KeyError: + raise ValueError(f"Unknown clock event reason bit: {1 << reason}") + output_reasons.append(output_reason) + return output_reasons @property def supported_clock_event_reasons(self) -> list[ClocksEventReasons]: @@ -499,7 +717,14 @@ cdef class Device: """ cdef uint64_t[1] reasons reasons[0] = nvml.device_get_supported_clocks_event_reasons(self._handle) - return [ClocksEventReasons(1 << reason) for reason in _unpack_bitmask(reasons)] + output_reasons = [] + for reason in _unpack_bitmask(reasons): + try: + output_reason = _CLOCKS_EVENT_REASONS_MAPPING[1 << reason] + except KeyError: + raise ValueError(f"Unknown clock event reason bit: {1 << reason}") + output_reasons.append(output_reason) + return output_reasons ########################################################################## # COOLER @@ -556,7 +781,7 @@ cdef class Device: # EVENTS # See external class definitions in _event.pxi - def register_events(self, events: EventType | int | list[EventType | int]) -> DeviceEvents: + def register_events(self, events: EventType | str | list[EventType | str]) -> DeviceEvents: """ Starts recording events on this device. @@ -575,14 +800,14 @@ cdef class Device: -------- >>> device = Device(index=0) >>> events = device.register_events([ - ... EventType.EVENT_TYPE_XID_CRITICAL_ERROR, + ... EventType.XID_CRITICAL_ERROR, ... ]) >>> while event := events.wait(timeout_ms=10000): ... print(f"Event {event.event_type} occurred on device {event.device.uuid}") Parameters ---------- - events: EventType, int, or list of EventType or int + events: EventType, str, or list of EventType or str The event type or list of event types to register for this device. Returns @@ -612,7 +837,14 @@ cdef class Device: """ cdef uint64_t[1] bitmask bitmask[0] = nvml.device_get_supported_event_types(self._handle) - return [EventType(1 << ev) for ev in _unpack_bitmask(bitmask)] + events = [] + for ev in _unpack_bitmask(bitmask): + try: + ev_enum = _EVENT_TYPE_MAPPING[1 << ev] + except KeyError: + raise ValueError(f"Unknown event type bit: {1 << ev}") + events.append(ev_enum) + return events ########################################################################## # FAN @@ -752,15 +984,20 @@ cdef class Device: # See external class definitions in _performance.pxi @property - def performance_state(self) -> Pstates: + def performance_state(self) -> int | None: """ The current performance state of the device. For Fermi™ or newer fully supported devices. - See :class:`Pstates` for possible performance states. + Returns + ------- + int | None + The current performance state of the device, as an integer between 0 and 15, + where 0 is maximum performance and higher numbers are lower performance. + Returns `None` if the performance state is unknown. """ - return Pstates(nvml.device_get_performance_state(self._handle)) + return _pstate_to_int(nvml.device_get_performance_state(self._handle)) @property def dynamic_pstates_info(self) -> GpuDynamicPstatesInfo: @@ -770,7 +1007,7 @@ cdef class Device: return GpuDynamicPstatesInfo(nvml.device_get_dynamic_pstates_info(self._handle)) @property - def supported_pstates(self) -> list[Pstates]: + def supported_pstates(self) -> list[int]: """ Get all supported Performance States (P-States) for the device. @@ -779,10 +1016,23 @@ cdef class Device: Return ------ - list[Pstates] - A list of supported P-States for the device. - """ - return [Pstates(x) for x in nvml.device_get_supported_performance_states(self._handle)] + list[int] + A list of supported performance state of the device, as an integer + between 0 and 15, where 0 is maximum performance and higher numbers + are lower performance. + """ + # From nvml.h: + # The returned array would contain a contiguous list of valid P-States + # supported by the device. If the number of supported P-States is fewer + # than the size of the array supplied missing elements would contain \a + # NVML_PSTATE_UNKNOWN. + + pstates = [] + for pstate in nvml.device_get_supported_performance_states(self._handle): + pstate_value = _pstate_to_int(pstate) + if pstate_value is not None: + pstates.append(pstate_value) + return pstates ########################################################################## # PROCESS @@ -838,7 +1088,7 @@ cdef class Device: ####################################################################### # TOPOLOGY - def get_topology_nearest_gpus(self, level: GpuTopologyLevel) -> Iterable[Device]: + def get_topology_nearest_gpus(self, level: GpuTopologyLevel | str) -> Iterable[Device]: """ Retrieve the GPUs that are nearest to this device at a specific interconnectivity level. @@ -855,6 +1105,13 @@ cdef class Device: The nearest devices at the given topology level. """ cdef Device device + try: + level = _GPU_TOPOLOGY_LEVEL_MAPPING[level] + except KeyError: + raise ValueError( + f"Invalid topology level: {level}. " + f"Must be one of {list(GpuTopologyLevel.__members__.values())}" + ) from None for handle in nvml.device_get_topology_nearest_gpus(self._handle, level): device = Device.__new__(Device) device._handle = handle @@ -904,15 +1161,15 @@ def get_topology_common_ancestor(device1: Device, device2: Device) -> GpuTopolog :class:`GpuTopologyLevel` The common ancestor level of the two devices. """ - return GpuTopologyLevel( + return _GPU_TOPOLOGY_LEVEL_INV_MAPPING[ nvml.device_get_topology_common_ancestor( device1._handle, device2._handle, ) - ) + ] -def get_p2p_status(device1: Device, device2: Device, index: GpuP2PCapsIndex) -> GpuP2PStatus: +def get_p2p_status(device1: Device, device2: Device, index: GpuP2PCapsIndex | str) -> GpuP2PStatus: """ Retrieve the P2P status between two devices. @@ -922,7 +1179,7 @@ def get_p2p_status(device1: Device, device2: Device, index: GpuP2PCapsIndex) -> The first device. device2: :class:`Device` The second device. - index: :class:`GpuP2PCapsIndex` + index: :class:`GpuP2PCapsIndex` | str The P2P capability index being looked for between ``device1`` and ``device2``. Returns @@ -930,19 +1187,26 @@ def get_p2p_status(device1: Device, device2: Device, index: GpuP2PCapsIndex) -> :class:`GpuP2PStatus` The P2P status between the two devices. """ - return GpuP2PStatus( + try: + index_enum = _GPU_P2P_CAPS_INDEX_MAPPING[index] + except KeyError: + raise ValueError( + f"Invalid P2P caps index: {index}. " + f"Must be one of {list(GpuP2PCapsIndex.__members__.values())}" + ) from None + return _GPU_P2P_STATUS_MAPPING.get( nvml.device_get_p2p_status( device1._handle, device2._handle, - index, - ) + index_enum, + ), + GpuP2PStatus.UNKNOWN ) __all__ = [ "AddressingMode", "AffinityScope", - "BrandType", "ClockId", "ClocksEventReasons", "ClockType", @@ -959,10 +1223,7 @@ __all__ = [ "GpuP2PStatus", "GpuTopologyLevel", "InforomObject", - "NvlinkVersion", - "PcieUtilCounter", - "Pstates", - "TemperatureSensors", + "NvlinkInfo", "TemperatureThresholds", "ThermalController", "ThermalTarget", diff --git a/cuda_core/cuda/core/system/_event.pxi b/cuda_core/cuda/core/system/_event.pxi index 983b737b233..30aa11efa30 100644 --- a/cuda_core/cuda/core/system/_event.pxi +++ b/cuda_core/cuda/core/system/_event.pxi @@ -3,7 +3,56 @@ # SPDX-License-Identifier: Apache-2.0 -EventType = nvml.EventType +class EventType(StrEnum): + """ + Event types that can be waited on with :class:`DeviceEvents`. + """ + NONE = "none" + SINGLE_BIT_ECC_ERROR = "single_bit_ecc_error" + DOUBLE_BIT_ECC_ERROR = "double_bit_ecc_error" + PSTATE = "pstate" + XID_CRITICAL_ERROR = "xid_critical_error" + CLOCK = "clock" + POWER_SOURCE_CHANGE = "power_source_change" + MIG_CONFIG_CHANGE = "mig_config_change" + SINGLE_BIT_ECC_ERROR_STORM = "single_bit_ecc_error_storm" + DRAM_RETIREMENT_EVENT = "dram_retirement_event" + DRAM_RETIREMENT_FAILURE = "dram_retirement_failure" + NON_FATAL_POISON_ERROR = "non_fatal_poison_error" + FATAL_POISON_ERROR = "fatal_poison_error" + GPU_UNAVAILABLE_ERROR = "gpu_unavailable_error" + GPU_RECOVERY_ACTION = "gpu_recovery_action" + + +EventType.PSTATE.__doc__ = """ +Event about PState changes + +On Fermi™ architecture, PState changes are also an indicator that GPU is throttling down due to +no work being executed on the GPU, power capping or thermal capping. In a typical situation, +Fermi-based GPU should stay in P0 for the duration of the execution of the compute process. +""" + + +_EVENT_TYPE_MAPPING = { + nvml.EventType.NONE: EventType.NONE, + nvml.EventType.SINGLE_BIT_ECC_ERROR: EventType.SINGLE_BIT_ECC_ERROR, + nvml.EventType.DOUBLE_BIT_ECC_ERROR: EventType.DOUBLE_BIT_ECC_ERROR, + nvml.EventType.PSTATE: EventType.PSTATE, + nvml.EventType.XID_CRITICAL_ERROR: EventType.XID_CRITICAL_ERROR, + nvml.EventType.CLOCK: EventType.CLOCK, + nvml.EventType.POWER_SOURCE_CHANGE: EventType.POWER_SOURCE_CHANGE, + nvml.EventType.MIG_CONFIG_CHANGE: EventType.MIG_CONFIG_CHANGE, + nvml.EventType.SINGLE_BIT_ECC_ERROR_STORM: EventType.SINGLE_BIT_ECC_ERROR_STORM, + nvml.EventType.DRAM_RETIREMENT_EVENT: EventType.DRAM_RETIREMENT_EVENT, + nvml.EventType.DRAM_RETIREMENT_FAILURE: EventType.DRAM_RETIREMENT_FAILURE, + nvml.EventType.NON_FATAL_POISON_ERROR: EventType.NON_FATAL_POISON_ERROR, + nvml.EventType.FATAL_POISON_ERROR: EventType.FATAL_POISON_ERROR, + nvml.EventType.GPU_UNAVAILABLE_ERROR: EventType.GPU_UNAVAILABLE_ERROR, + nvml.EventType.GPU_RECOVERY_ACTION: EventType.GPU_RECOVERY_ACTION, +} + + +_EVENT_TYPE_INV_MAPPING = {v: k for k, v in _EVENT_TYPE_MAPPING.items()} cdef class EventData: @@ -18,7 +67,7 @@ cdef class EventData: """ The device on which the event occurred. """ - device = Device.__new__() + device = Device.__new__(Device) device._handle = self._event_data.device return device @@ -27,17 +76,17 @@ cdef class EventData: """ The type of event that was triggered. """ - return EventType(self._event_data.event_type) + return _EVENT_TYPE_MAPPING[self._event_data.event_type] @property def event_data(self) -> int: """ Returns Xid error for the device in the event of - :attr:`~cuda.core.system.EventType.EVENT_TYPE_XID_CRITICAL_ERROR`. + :attr:`~cuda.core.system.EventType.XID_CRITICAL_ERROR`. Raises :class:`ValueError` for other event types. """ - if self.event_type != EventType.EVENT_TYPE_XID_CRITICAL_ERROR: + if self._event_data.event_type != nvml.EventType.XID_CRITICAL_ERROR: raise ValueError("event_data is only available for Xid critical error events.") return self._event_data.event_data @@ -46,11 +95,11 @@ cdef class EventData: """ The GPU instance ID for MIG devices. - Only valid for events of type :attr:`EventType.EVENT_TYPE_XID_CRITICAL_ERROR`. + Only valid for events of type :attr:`EventType.XID_CRITICAL_ERROR`. Raises :class:`ValueError` for other event types. """ - if self.event_type != EventType.EVENT_TYPE_XID_CRITICAL_ERROR: + if self._event_data.event_type != nvml.EventType.XID_CRITICAL_ERROR: raise ValueError("gpu_instance_id is only available for Xid critical error events.") return self._event_data.gpu_instance_id @@ -59,11 +108,11 @@ cdef class EventData: """ The Compute instance ID for MIG devices. - Only valid for events of type :attr:`EventType.EVENT_TYPE_XID_CRITICAL_ERROR`. + Only valid for events of type :attr:`EventType.XID_CRITICAL_ERROR`. Raises :class:`ValueError` for other event types. """ - if self.event_type != EventType.EVENT_TYPE_XID_CRITICAL_ERROR: + if self._event_data.event_type != nvml.EventType.XID_CRITICAL_ERROR: raise ValueError("compute_instance_id is only available for Xid critical error events.") return self._event_data.compute_instance_id @@ -75,16 +124,24 @@ cdef class DeviceEvents: cdef intptr_t _event_set cdef intptr_t _device_handle - def __init__(self, device_handle: intptr_t, events: EventType | int | list[EventType | int]): + def __init__(self, device_handle: intptr_t, events: EventType | str | list[EventType | str]): cdef unsigned long long event_bitmask - if isinstance(events, (int, EventType)): - event_bitmask = int(events) - elif isinstance(events, list): + if isinstance(events, (str, EventType)): + events = [events] + + if isinstance(events, list): event_bitmask = 0 for ev in events: - event_bitmask |= int(ev) + try: + ev_enum = _EVENT_TYPE_INV_MAPPING[ev] + except KeyError: + raise ValueError( + f"Invalid event type: {ev}. " + f"Must be one of {list(EventType.__members__.values())}" + ) from None + event_bitmask |= int(ev_enum) else: - raise TypeError("events must be an EventType, int, or list of EventType or int") + raise TypeError("events must be an EventType, str, or list of EventType or str") self._device_handle = device_handle self._event_set = nvml.event_set_create() diff --git a/cuda_core/cuda/core/system/_fan.pxi b/cuda_core/cuda/core/system/_fan.pxi index bfe417c267e..651ab50997b 100644 --- a/cuda_core/cuda/core/system/_fan.pxi +++ b/cuda_core/cuda/core/system/_fan.pxi @@ -3,7 +3,18 @@ # SPDX-License-Identifier: Apache-2.0 -FanControlPolicy = nvml.FanControlPolicy +class FanControlPolicy(StrEnum): + """ + Fan control policies. + """ + TEMPERATURE_CONTROLLED = "temperature_controlled" + MANUAL = "manual" + + +_FAN_CONTROL_POLICY_MAPPING = { + nvml.FanControlPolicy.TEMPERATURE_CONTINUOUS_SW: FanControlPolicy.TEMPERATURE_CONTROLLED, + nvml.FanControlPolicy.MANUAL: FanControlPolicy.MANUAL, +} cdef class FanInfo: @@ -94,7 +105,7 @@ cdef class FanInfo: For all CUDA-capable discrete products with fans. """ - return FanControlPolicy(nvml.device_get_fan_control_policy_v2(self._handle, self._fan)) + return _FAN_CONTROL_POLICY_MAPPING[nvml.device_get_fan_control_policy_v2(self._handle, self._fan)] def set_default_speed(self): """ diff --git a/cuda_core/cuda/core/system/_inforom.pxi b/cuda_core/cuda/core/system/_inforom.pxi index 43fb076f7ac..a76bb0a2bbd 100644 --- a/cuda_core/cuda/core/system/_inforom.pxi +++ b/cuda_core/cuda/core/system/_inforom.pxi @@ -3,7 +3,28 @@ # SPDX-License-Identifier: Apache-2.0 -InforomObject = nvml.InforomObject +class InforomObject(StrEnum): + """ + InfoROM objects types. + """ + OEM = "oem" + ECC = "ecc" + POWER = "power" + DEN = "den" + + +InforomObject.OEM.__doc__ = "An object defined by OEM." +InforomObject.ECC.__doc__ = "The ECC object determining the level of ECC support." +InforomObject.POWER.__doc__ = "The power management object." +InforomObject.DEN.__doc__ = "DRAM Encryption object." + + +_INFOROM_OBJECT_MAPPING = { + InforomObject.OEM: nvml.InforomObject.INFOROM_OEM, + InforomObject.ECC: nvml.InforomObject.INFOROM_ECC, + InforomObject.POWER: nvml.InforomObject.INFOROM_POWER, + InforomObject.DEN: nvml.InforomObject.INFOROM_DEN, +} cdef class InforomInfo: @@ -12,7 +33,7 @@ cdef class InforomInfo: def __init__(self, device: Device): self._device = device - def get_version(self, inforom: InforomObject) -> str: + def get_version(self, inforom: InforomObject | str) -> str: """ Retrieves the InfoROM version for a given InfoROM object. @@ -31,7 +52,14 @@ cdef class InforomInfo: str The InfoROM version. """ - return nvml.device_get_inforom_version(self._device._handle, inforom) + try: + inforom_enum = _INFOROM_OBJECT_MAPPING[inforom] + except KeyError: + raise ValueError( + f"Invalid InfoROM object: {inforom}. " + f"Must be one of {list(InforomObject.__members__.values())}" + ) from None + return nvml.device_get_inforom_version(self._device._handle, inforom_enum) @property def image_version(self) -> str: diff --git a/cuda_core/cuda/core/system/_nvlink.pxi b/cuda_core/cuda/core/system/_nvlink.pxi index aeee3af1535..6d4a7ac9b2f 100644 --- a/cuda_core/cuda/core/system/_nvlink.pxi +++ b/cuda_core/cuda/core/system/_nvlink.pxi @@ -3,7 +3,15 @@ # SPDX-License-Identifier: Apache-2.0 -NvlinkVersion = nvml.NvlinkVersion +_NVLINK_VERSION_MAPPING = { + nvml.NvlinkVersion.VERSION_1_0: (1, 0), + nvml.NvlinkVersion.VERSION_2_0: (2, 0), + nvml.NvlinkVersion.VERSION_2_2: (2, 2), + nvml.NvlinkVersion.VERSION_3_0: (3, 0), + nvml.NvlinkVersion.VERSION_3_1: (3, 1), + nvml.NvlinkVersion.VERSION_4_0: (4, 0), + nvml.NvlinkVersion.VERSION_5_0: (5, 0), +} cdef class NvlinkInfo: @@ -18,18 +26,24 @@ cdef class NvlinkInfo: self._link = link @property - def version(self) -> NvlinkVersion: + def version(self) -> tuple[int, int]: """ - Retrieves the :obj:`~NvlinkVersion` for the device and link. + Retrieves the NvLink version for the device and link. For all products with NvLink support. Returns ------- - NvlinkVersion - The Nvlink version. + tuple[int, int] + The Nvlink version as a tuple of (major, minor). """ - return NvlinkVersion(nvml.device_get_nvlink_version(self._device._handle, self._link)) + version = nvml.device_get_nvlink_version(self._device._handle, self._link) + if version == nvml.NvlinkVersion.VERSION_INVALID: + raise RuntimeError(f"Invalid NvLink version returned for device") + try: + return _NVLINK_VERSION_MAPPING[version] + except KeyError: + raise RuntimeError(f"Unknown NvLink version {version} returned for device") from None @property def state(self) -> bool: diff --git a/cuda_core/cuda/core/system/_pci_info.pxi b/cuda_core/cuda/core/system/_pci_info.pxi index 1402853fa2f..55922b767ad 100644 --- a/cuda_core/cuda/core/system/_pci_info.pxi +++ b/cuda_core/cuda/core/system/_pci_info.pxi @@ -3,9 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 -PcieUtilCounter = nvml.PcieUtilCounter - - cdef class PciInfo: """ PCI information about a GPU device. @@ -134,9 +131,25 @@ cdef class PciInfo: """ return nvml.device_get_curr_pcie_link_width(self._handle) - def get_throughput(self, counter: PcieUtilCounter) -> int: + @property + def rx_throughput(self) -> int: + """ + Retrieve PCIe reception throughput, in KB/s. + + This function is querying a byte counter over a 20ms interval, and thus + is the PCIe throughput over that interval. + + For Maxwell™ or newer fully supported devices. + + This method is not supported in virtual machines running virtual GPU + (vGPU). + """ + return nvml.device_get_pcie_throughput(self._handle, nvml.PcieUtilCounter.PCIE_UTIL_RX_BYTES) + + @property + def tx_throughput(self) -> int: """ - Retrieve PCIe utilization information, in KB/s. + Retrieve PCIe transmission throughput, in KB/s. This function is querying a byte counter over a 20ms interval, and thus is the PCIe throughput over that interval. @@ -146,7 +159,7 @@ cdef class PciInfo: This method is not supported in virtual machines running virtual GPU (vGPU). """ - return nvml.device_get_pcie_throughput(self._handle, counter) + return nvml.device_get_pcie_throughput(self._handle, nvml.PcieUtilCounter.PCIE_UTIL_TX_BYTES) @property def replay_counter(self) -> int: diff --git a/cuda_core/cuda/core/system/_system_events.pyx b/cuda_core/cuda/core/system/_system_events.pyx index eea17523294..a00a7bdf97b 100644 --- a/cuda_core/cuda/core/system/_system_events.pyx +++ b/cuda_core/cuda/core/system/_system_events.pyx @@ -5,6 +5,12 @@ from libc.stdint cimport intptr_t +import sys +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from backports.strenum import StrEnum + from cuda.bindings import nvml from ._nvml_context cimport initialize @@ -12,7 +18,21 @@ from ._nvml_context cimport initialize from . import _device -SystemEventType = nvml.SystemEventType +class SystemEventType(StrEnum): + """ + System event types. + """ + UNBIND = "unbind" + BIND = "bind" + + +_SYSTEM_EVENT_TYPE_MAPPING = { + nvml.SystemEventType.GPU_DRIVER_UNBIND: SystemEventType.UNBIND, + nvml.SystemEventType.GPU_DRIVER_BIND: SystemEventType.BIND, +} + + +_SYSTEM_EVENT_TYPE_INV_MAPPING = {v: k for k, v in _SYSTEM_EVENT_TYPE_MAPPING.items()} cdef class SystemEvent: @@ -28,7 +48,7 @@ cdef class SystemEvent: """ The :obj:`~SystemEventType` that was triggered. """ - return SystemEventType(self._event_data.event_type) + return _SYSTEM_EVENT_TYPE_MAPPING[self._event_data.event_type] @property def gpu_id(self) -> int: @@ -68,16 +88,24 @@ cdef class RegisteredSystemEvents: """ cdef intptr_t _event_set - def __init__(self, events: SystemEventType | int | list[SystemEventType | int]): + def __init__(self, events: SystemEventType | str | list[SystemEventType | str]): cdef unsigned long long event_bitmask - if isinstance(events, (int, SystemEventType)): - event_bitmask = int(events) - elif isinstance(events, list): + if isinstance(events, (str, SystemEventType)): + events = [events] + + if isinstance(events, list): event_bitmask = 0 for ev in events: - event_bitmask |= int(ev) + try: + ev_enum = _SYSTEM_EVENT_TYPE_INV_MAPPING[ev] + except KeyError: + raise ValueError( + f"Invalid event type: {ev}. " + f"Must be one of {list(SystemEventType.__members__.values())}" + ) from None + event_bitmask |= int(ev_enum) else: - raise TypeError("events must be an SystemEventType, int, or list of SystemEventType or int") + raise TypeError("events must be an SystemEventType, str, or list of SystemEventType or str") initialize() @@ -128,7 +156,7 @@ cdef class RegisteredSystemEvents: return SystemEvents(nvml.system_event_set_wait(self._event_set, timeout_ms, buffer_size)) -def register_events(events: SystemEventType | int | list[SystemEventType | int]) -> RegisteredSystemEvents: +def register_events(events: SystemEventType | str | list[SystemEventType | str]) -> RegisteredSystemEvents: """ Starts recording of events on test system. @@ -140,15 +168,13 @@ def register_events(events: SystemEventType | int | list[SystemEventType | int]) Examples -------- >>> from cuda.core import system - >>> events = system.register_events([ - ... SystemEventType.SYSTEM_EVENT_TYPE_GPU_DRIVER_UNBIND, - ... ]) + >>> events = system.register_events([SystemEventType.UNBIND]) >>> while event := events.wait(timeout_ms=10000): ... print(f"Event {event.event_type} occurred.") Parameters ---------- - events: SystemEventType, int, or list of SystemEventType or int + events: SystemEventType, str, or list of SystemEventType or str The event type or list of event types to register for this device. Returns diff --git a/cuda_core/cuda/core/system/_temperature.pxi b/cuda_core/cuda/core/system/_temperature.pxi index 187f52be81e..b322df4a591 100644 --- a/cuda_core/cuda/core/system/_temperature.pxi +++ b/cuda_core/cuda/core/system/_temperature.pxi @@ -3,10 +3,115 @@ # SPDX-License-Identifier: Apache-2.0 -TemperatureSensors = nvml.TemperatureSensors -TemperatureThresholds = nvml.TemperatureThresholds -ThermalController = nvml.ThermalController -ThermalTarget = nvml.ThermalTarget +class TemperatureThresholds(StrEnum): + """ + Temperature threshold types. + """ + SHUTDOWN = "shutdown" + SLOWDOWN = "slowdown" + MEM_MAX = "mem_max" + GPU_MAX = "gpu_max" + ACOUSTIC_MIN = "acoustic_min" + ACOUSTIC_CURR = "acoustic_curr" + ACOUSTIC_MAX = "acoustic_max" + GPS_CURR = "gps_curr" + + +_TEMPERATURE_THRESHOLD_MAPPING = { + TemperatureThresholds.SHUTDOWN: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_SHUTDOWN, + TemperatureThresholds.SLOWDOWN: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_SLOWDOWN, + TemperatureThresholds.MEM_MAX: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_MEM_MAX, + TemperatureThresholds.GPU_MAX: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_GPU_MAX, + TemperatureThresholds.ACOUSTIC_MIN: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_ACOUSTIC_MIN, + TemperatureThresholds.ACOUSTIC_CURR: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_ACOUSTIC_CURR, + TemperatureThresholds.ACOUSTIC_MAX: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_ACOUSTIC_MAX, + TemperatureThresholds.GPS_CURR: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_GPS_CURR, +} + + +class ThermalController(StrEnum): + """ + Thermal controller types. + """ + GPU_INTERNAL = "gpu_internal" + ADM1032 = "adm1032" + ADT7461 = "adt7461" + MAX6649 = "max6649" + MAX1617 = "max1617" + LM99 = "lm99" + LM89 = "lm89" + LM64 = "lm64" + G781 = "g781" + ADT7473 = "adt7473" + SBMAX6649 = "sbmax6649" + VBIOSEVT = "vbiosevt" + OS = "os" + NVSYSCON_CANOAS = "nvsyscon_canoas" + NVSYSCON_E551 = "nvsyscon_e551" + MAX6649R = "max6649r" + ADT7473S = "adt7473s" + UNKNOWN = "unknown" + + +_THERMAL_CONTROLLER_MAPPING = { + nvml.ThermalController.GPU_INTERNAL: ThermalController.GPU_INTERNAL, + nvml.ThermalController.ADM1032: ThermalController.ADM1032, + nvml.ThermalController.ADT7461: ThermalController.ADT7461, + nvml.ThermalController.MAX6649: ThermalController.MAX6649, + nvml.ThermalController.MAX1617: ThermalController.MAX1617, + nvml.ThermalController.LM99: ThermalController.LM99, + nvml.ThermalController.LM89: ThermalController.LM89, + nvml.ThermalController.LM64: ThermalController.LM64, + nvml.ThermalController.G781: ThermalController.G781, + nvml.ThermalController.ADT7473: ThermalController.ADT7473, + nvml.ThermalController.SBMAX6649: ThermalController.SBMAX6649, + nvml.ThermalController.VBIOSEVT: ThermalController.VBIOSEVT, + nvml.ThermalController.OS: ThermalController.OS, + nvml.ThermalController.NVSYSCON_CANOAS: ThermalController.NVSYSCON_CANOAS, + nvml.ThermalController.NVSYSCON_E551: ThermalController.NVSYSCON_E551, + nvml.ThermalController.MAX6649R: ThermalController.MAX6649R, + nvml.ThermalController.ADT7473S: ThermalController.ADT7473S, +} + + +class ThermalTarget(StrEnum): + """ + Thermal sensor targets. + """ + NONE = "none" + GPU = "gpu" + MEMORY = "memory" + POWER_SUPPLY = "power_supply" + BOARD = "board" + VCD_BOARD = "vcd_board" + VCD_INLET = "vcd_inlet" + VCD_OUTLET = "vcd_outlet" + ALL = "all" + + +ThermalTarget.GPU.__doc__ = "GPU core temperature requires physical GPU handle." +ThermalTarget.MEMORY.__doc__ = "GPU memory temperature requires physical GPU handle." +ThermalTarget.POWER_SUPPLY.__doc__ = "GPU power supply temperature requires physical GPU handle." +ThermalTarget.BOARD.__doc__ = "GPU board ambient temperature requires physical GPU handle." +ThermalTarget.VCD_BOARD.__doc__ = "Visual Computing Device Board temperature requires visual computing device handle." +ThermalTarget.VCD_INLET.__doc__ = "Visual Computing Device Inlet temperature requires visual computing device handle." +ThermalTarget.VCD_OUTLET.__doc__ = "Visual Computing Device Outlet temperature requires visual computing device handle." + + +_THERMAL_TARGET_MAPPING = { + nvml.ThermalTarget.NONE: ThermalTarget.NONE, + nvml.ThermalTarget.GPU: ThermalTarget.GPU, + nvml.ThermalTarget.MEMORY: ThermalTarget.MEMORY, + nvml.ThermalTarget.POWER_SUPPLY: ThermalTarget.POWER_SUPPLY, + nvml.ThermalTarget.BOARD: ThermalTarget.BOARD, + nvml.ThermalTarget.VCD_BOARD: ThermalTarget.VCD_BOARD, + nvml.ThermalTarget.VCD_INLET: ThermalTarget.VCD_INLET, + nvml.ThermalTarget.VCD_OUTLET: ThermalTarget.VCD_OUTLET, + nvml.ThermalTarget.ALL: ThermalTarget.ALL, +} + + +_THERMAL_TARGET_INV_MAPPING = {v: k for k, v in _THERMAL_TARGET_MAPPING.items()} # In cuda.bindings.nvml, this is an anonymous struct inside nvmlThermalSettings_t. @@ -33,7 +138,7 @@ cdef class ThermalSensor: @property def controller(self) -> ThermalController: - return ThermalController(self._ptr[0].controller) + return _THERMAL_CONTROLLER_MAPPING.get(self._ptr[0].controller, ThermalController.UNKNOWN) @property def default_min_temp(self) -> int: @@ -49,7 +154,7 @@ cdef class ThermalSensor: @property def target(self) -> ThermalTarget: - return ThermalTarget(self._ptr[0].target) + return _THERMAL_TARGET_MAPPING.get(self._ptr[0].target, ThermalTarget.NONE) cdef class ThermalSettings: @@ -77,27 +182,25 @@ cdef class Temperature: def __init__(self, handle: int): self._handle = handle - def get_sensor( - self, - sensor: TemperatureSensors = TemperatureSensors.TEMPERATURE_GPU - ) -> int: + def get_sensor(self) -> int: """ Get the temperature reading from a specific sensor on the device, in degrees Celsius. - Parameters - ---------- - sensor: :class:`TemperatureSensors`, optional - The temperature sensor to query. + The only sensor currently supported is the GPU temperature sensor. Returns ------- int The temperature in degrees Celsius. """ - return nvml.device_get_temperature_v(self._handle, sensor) + # NOTE: nvml.device_get_temperature_v takes a sensor type from the + # TemperatorSensors enum, but there is only one value in that enum. For + # future compatibility if there are other values for that enum, this is + # a method, not a property + return nvml.device_get_temperature_v(self._handle, nvml.TemperatureSensors.TEMPERATURE_GPU) - def get_threshold(self, threshold_type: TemperatureThresholds) -> int: + def get_threshold(self, threshold_type: TemperatureThresholds | str) -> int: """ Retrieves the temperature threshold for this GPU with the specified threshold type, in degrees Celsius. @@ -118,7 +221,29 @@ cdef class Temperature: use :meth:`get_field_values` with ``NVML_FI_DEV_TEMPERATURE_*`` fields to retrieve temperature thresholds on these architectures. """ - return nvml.device_get_temperature_threshold(self._handle, threshold_type) + try: + threshold_type_enum = _TEMPERATURE_THRESHOLD_MAPPING[threshold_type] + except KeyError: + raise ValueError( + f"Invalid temperature threshold type: {threshold_type}. " + f"Must be one of {list(TemperatureThresholds.__members__.values())}" + ) from None + if threshold_type_enum in ( + nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_SHUTDOWN, + nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_SLOWDOWN, + nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_MEM_MAX, + nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_GPU_MAX + ): + device_arch = nvml.DeviceArch(nvml.device_get_architecture(self._handle)) + if device_arch >= nvml.DeviceArch.ADA: + warnings.warn( + f"{threshold_type} is no longer recommended for Ada and later architectures. " + "Use get_field_values with NVML_FI_DEV_TEMPERATURE_* fields to retrieve this " + "threshold on these architectures.", + DeprecationWarning, + stacklevel=2 + ) + return nvml.device_get_temperature_threshold(self._handle, threshold_type_enum) @property def margin(self) -> int: @@ -127,12 +252,10 @@ cdef class Temperature: """ return nvml.device_get_margin_temperature(self._handle) - def get_thermal_settings(self, sensor_index: ThermalTarget) -> ThermalSettings: + def get_thermal_settings(self, sensor_index: ThermalTarget | str) -> ThermalSettings: """ Used to execute a list of thermal system instructions. - TODO: The above docstring is from the NVML header, but it doesn't seem to make sense. - Parameters ---------- sensor_index: ThermalTarget @@ -143,4 +266,13 @@ cdef class Temperature: :obj:`~_device.ThermalSettings` The thermal settings for the specified sensor. """ - return ThermalSettings(nvml.device_get_thermal_settings(self._handle, sensor_index)) + # TODO: The above docstring is from the NVML header, but it doesn't seem to make sense. + try: + sensor_index_enum = _THERMAL_TARGET_INV_MAPPING[sensor_index] + except KeyError: + raise ValueError( + f"Invalid thermal sensor index: {sensor_index}. " + f"Must be one of {list(ThermalTarget.__members__.values())}" + ) from None + + return ThermalSettings(nvml.device_get_thermal_settings(self._handle, sensor_index_enum)) diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 054831a03c1..77f04e6cd8c 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -201,33 +201,6 @@ Events system.register_events system.SystemEventType -Enums -````` - -.. autosummary:: - :toctree: generated/ - - system.AddressingMode - system.AffinityScope - system.BrandType - system.ClockId - system.ClocksEventReasons - system.ClockType - system.CoolerControl - system.CoolerTarget - system.DeviceArch - system.EventType - system.FanControlPolicy - system.FieldId - system.InforomObject - system.NvlinkVersion - system.PcieUtilCounter - system.Pstates - system.TemperatureSensors - system.TemperatureThresholds - system.ThermalController - system.ThermalTarget - Types ````` @@ -237,6 +210,7 @@ Types :template: autosummary/cyclass.rst system.Device + system.NvlinkInfo .. module:: cuda.core.utils diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index 141773967e8..a3bb4f1395c 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -71,13 +71,9 @@ NVML system._device.FieldValues system._device.GpuDynamicPstatesInfo system._device.GpuDynamicPstatesUtilization - system._device.GpuP2PCapsIndex - system._device.GpuP2PStatus - system._device.GpuTopologyLevel system._device.InforomInfo system._device.MemoryInfo system._device.MigInfo - system._device.NvlinkInfo system._device.PciInfo system._device.ProcessInfo system._device.RepairStatus @@ -87,3 +83,28 @@ NVML system._system_events.RegisteredSystemEvents system._system_events.SystemEvent system._system_events.SystemEvents + +.. These are not technically private, but are included here to avoid cluttering the main API reference. + +.. autosummary:: + :toctree: generated/ + + system.AddressingMode + system.AffinityScope + system.ClockId + system.ClocksEventReasons + system.ClockType + system.CoolerControl + system.CoolerTarget + system.DeviceArch + system.EventType + system.FanControlPolicy + system.FieldId + system.GpuP2PCapsIndex + system.GpuP2PStatus + system.GpuTopologyLevel + system.InforomObject + system.TemperatureThresholds + system.ThermalController + system.ThermalTarget + system.SystemEventType diff --git a/cuda_core/pixi.toml b/cuda_core/pixi.toml index 1008fe9711f..df5111497fa 100644 --- a/cuda_core/pixi.toml +++ b/cuda_core/pixi.toml @@ -186,6 +186,9 @@ numpy = "*" cuda-bindings = "*" cuda-pathfinder = "*" +[package.target.'python_version < "3.11"'.run-dependencies] +"backports.strenum" = "*" + [target.linux.tasks.build-cython-tests] cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.sh"] diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index aa403409894..9c2d36ea144 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -50,6 +50,7 @@ classifiers = [ dependencies = [ "cuda-pathfinder >=1.4.2", "numpy", + "backports.strenum; python_version < '3.11'", ] [project.optional-dependencies] diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index 932022c3422..9625d16b2db 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -20,7 +20,8 @@ if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: from cuda.bindings import nvml - from cuda.core.system import DeviceArch, _device + from cuda.bindings.nvml import DeviceArch + from cuda.core.system import _device @pytest.fixture(autouse=True, scope="module") @@ -107,7 +108,7 @@ def test_device_cpu_affinity(): @pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="Device attributes not supported on WSL or Windows") def test_affinity(): for device in system.Device.get_all_devices(): - for scope in (system.AffinityScope.NODE, system.AffinityScope.SOCKET): + for scope in system.AffinityScope.__members__.values(): with unsupported_before(device, DeviceArch.KEPLER): affinity = device.get_cpu_affinity(scope) assert isinstance(affinity, list) @@ -214,7 +215,8 @@ def test_device_pci_info(): assert 0 <= pci_info.current_link_width <= 0xFF with unsupported_before(device, None): - assert isinstance(pci_info.get_throughput(system.PcieUtilCounter.PCIE_UTIL_TX_BYTES), int) + assert isinstance(pci_info.tx_throughput, int) + assert isinstance(pci_info.rx_throughput, int) assert isinstance(pci_info.replay_counter, int) @@ -275,30 +277,29 @@ def test_register_events(): assert all(isinstance(ev, system.EventType) for ev in supported_events) for device in system.Device.get_all_devices(): - events = device.register_events([]) + events = device.register_events(["xid_critical_error"]) with pytest.raises(system.TimeoutError): events.wait(timeout_ms=500) for device in system.Device.get_all_devices(): - events = device.register_events(0) + events = device.register_events([system.EventType.XID_CRITICAL_ERROR]) with pytest.raises(system.TimeoutError): events.wait(timeout_ms=500) + for device in system.Device.get_all_devices(): + events = device.register_events([]) + with pytest.raises(system.TimeoutError): + events.wait(timeout_ms=500) -def test_event_type_parsing(): - events = [system.EventType(1 << ev) for ev in _device._unpack_bitmask(array.array("Q", [3]))] - assert events == [ - system.EventType.SINGLE_BIT_ECC_ERROR, - system.EventType.DOUBLE_BIT_ECC_ERROR, - ] + for device in system.Device.get_all_devices(): + with pytest.raises(TypeError): + events = device.register_events(0) def test_device_brand(): for device in system.Device.get_all_devices(): brand = device.brand - assert isinstance(brand, system.BrandType) - assert isinstance(brand.name, str) - assert isinstance(brand.value, int) + assert isinstance(brand, str) def test_device_pci_bus_id(): @@ -437,7 +438,7 @@ def test_addressing_mode(): # is also unsupported on other hardware. with unsupported_before(device, None): addressing_mode = device.addressing_mode - assert isinstance(addressing_mode, system.AddressingMode) + assert addressing_mode is None or addressing_mode in system.AddressingMode.__members__.values() def test_display_mode(): @@ -487,7 +488,7 @@ def test_get_p2p_status(): devices = list(system.Device.get_all_devices()) - status = system.get_p2p_status(devices[0], devices[1], system.GpuP2PCapsIndex.P2P_CAPS_INDEX_READ) + status = system.get_p2p_status(devices[0], devices[1], system.GpuP2PCapsIndex.READ) assert isinstance(status, system.GpuP2PStatus) @@ -497,7 +498,7 @@ def test_get_nearest_gpus(): # in practice on our CI. for device in system.Device.get_all_devices(): - for near_device in device.get_topology_nearest_gpus(system.GpuTopologyLevel.TOPOLOGY_SINGLE): + for near_device in device.get_topology_nearest_gpus(system.GpuTopologyLevel.SINGLE): assert isinstance(near_device, system.Device) @@ -519,7 +520,7 @@ def test_get_inforom_version(): assert isinstance(inforom_image_version, str) assert len(inforom_image_version) > 0 - inforom_version = inforom.get_version(system.InforomObject.INFOROM_OEM) + inforom_version = inforom.get_version(system.InforomObject.OEM) assert isinstance(inforom_version, str) assert len(inforom_version) > 0 @@ -668,7 +669,7 @@ def test_cooler(): assert isinstance(cooler_info, _device.CoolerInfo) signal_type = cooler_info.signal_type - assert isinstance(signal_type, system.CoolerControl) + assert isinstance(signal_type, (system.CoolerControl, type(None))) target = cooler_info.target assert all(isinstance(t, system.CoolerTarget) for t in target) @@ -686,7 +687,7 @@ def test_temperature(): # By docs, should be supported on KEPLER or newer, but experimentally, # is also unsupported on other hardware. with unsupported_before(device, None): - for threshold in list(system.TemperatureThresholds)[:-1]: + for threshold in list(system.TemperatureThresholds): t = temperature.get_threshold(threshold) assert isinstance(t, int) assert t >= 0 @@ -716,10 +717,10 @@ def test_pstates(): for device in system.Device.get_all_devices(): with unsupported_before(device, None): pstate = device.performance_state - assert isinstance(pstate, system.Pstates) + assert isinstance(pstate, int) pstates = device.supported_pstates - assert all(isinstance(p, system.Pstates) for p in pstates) + assert all(isinstance(p, int) for p in pstates) dynamic_pstates_info = device.dynamic_pstates_info assert isinstance(dynamic_pstates_info, _device.GpuDynamicPstatesInfo) @@ -765,7 +766,9 @@ def test_nvlink(): with unsupported_before(device, None): version = nvlink_info.version - assert isinstance(version, system.NvlinkVersion) + assert isinstance(version, tuple) + assert len(version) == 2 + assert all(isinstance(i, int) for i in version) with unsupported_before(device, None): state = nvlink_info.state diff --git a/cuda_core/tests/system/test_system_events.py b/cuda_core/tests/system/test_system_events.py index 493f1b79934..7cfe7459109 100644 --- a/cuda_core/tests/system/test_system_events.py +++ b/cuda_core/tests/system/test_system_events.py @@ -23,7 +23,7 @@ def test_register_events(): # Also, some hardware doesn't support any event types. try: - events = system.register_events([system.SystemEventType.GPU_DRIVER_UNBIND]) + events = system.register_events([system.SystemEventType.UNBIND]) except system.UnknownError: pytest.skip("system events may only be registered once per process") diff --git a/cuda_core/tests/test_enum_coverage.py b/cuda_core/tests/test_enum_coverage.py new file mode 100644 index 00000000000..9c70c9f6042 --- /dev/null +++ b/cuda_core/tests/test_enum_coverage.py @@ -0,0 +1,275 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Verify that every cuda_binding enum member has a corresponding entry in the +# cuda_core wrapper mappings. No GPU required; the test only inspects +# mapping dicts at import time, so it runs on any CI host that has a +# compatible cuda.bindings version. + +import importlib +import inspect +import pkgutil +import sys + +import pytest + +import cuda.core +from cuda.core import system + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from backports.strenum import StrEnum + +# Each entry is: +# (cuda_binding_enum, str_enum, mapping_dict, binding_unmapped, str_enum_unmapped) +# +# cuda_binding_enum: the cuda.bindings enum class +# str_enum: the cuda_core StrEnum wrapper class, or None if the mapping does +# not use a StrEnum (e.g. maps to plain str or tuple) +# mapping_dict: the dict that maps between the two enum types +# binding_unmapped: cuda_binding_enum member names intentionally absent from the mapping +# (sentinels, deprecated aliases, etc.) +# str_enum_unmapped: StrEnum member names intentionally absent from the mapping +# (fallback sentinels returned by the wrapper via .get(value, default)) +# +# The first test checks that every member of cuda_binding_enum whose name is NOT +# in binding_unmapped appears as either a key or a value of the mapping dict, +# and conversely that every str_enum member not in str_enum_unmapped also +# appears. +_CASES = [] + +if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: + # Populated below only when NVML bindings are compatible, so that importing + # this module on an incompatible host does not raise ImportError. + from cuda.bindings import nvml + from cuda.core.system import _device, _system_events + + _CASES.extend( + [ + ( + nvml.DeviceAddressingModeType, + _device.AddressingMode, + _device._ADDRESSING_MODE_MAPPING, + # NONE means "no special addressing mode is active"; not a valid target + {"DEVICE_ADDRESSING_MODE_NONE"}, + set(), + ), + ( + nvml.BrandType, + None, # maps to plain str, not a StrEnum + _device._BRAND_TYPE_MAPPING, + # COUNT is a sentinel, not a real brand + {"BRAND_COUNT"}, + set(), + ), + ( + nvml.GpuP2PStatus, + _device.GpuP2PStatus, + _device._GPU_P2P_STATUS_MAPPING, + # Both the typo'd (SUPPORED) and corrected (SUPPORTED) spellings + # share the same integer value; the mapping covers both via aliases + set(), + set(), + ), + ( + nvml.ClocksEventReasons, + _device.ClocksEventReasons, + _device._CLOCKS_EVENT_REASONS_MAPPING, + set(), + set(), + ), + ( + nvml.EventType, + _device.EventType, + _device._EVENT_TYPE_MAPPING, + set(), + set(), + ), + ( + nvml.FanControlPolicy, + _device.FanControlPolicy, + _device._FAN_CONTROL_POLICY_MAPPING, + set(), + set(), + ), + ( + nvml.CoolerControl, + _device.CoolerControl, + _device._COOLER_CONTROL_MAPPING, + # NONE means no signal; COUNT is a sentinel + {"THERMAL_COOLER_SIGNAL_NONE", "THERMAL_COOLER_SIGNAL_COUNT"}, + set(), + ), + ( + nvml.CoolerTarget, + _device.CoolerTarget, + _device._COOLER_TARGET_MAPPING, + # GPU_RELATED is a composite bitmask (GPU | MEMORY | POWER_SUPPLY); + # the wrapper expands it into individual targets instead of mapping + # it as a single entry + {"THERMAL_GPU_RELATED"}, + set(), + ), + ( + nvml.ThermalController, + _device.ThermalController, + _device._THERMAL_CONTROLLER_MAPPING, + # NONE and UNKNOWN are both handled by the .get() fallback that + # returns ThermalController.UNKNOWN when the value is not in the mapping + {"NONE", "UNKNOWN"}, + # UNKNOWN is the default returned by .get() for unrecognised controllers + {"UNKNOWN"}, + ), + ( + nvml.ThermalTarget, + _device.ThermalTarget, + _device._THERMAL_TARGET_MAPPING, + # UNKNOWN is a fallback sentinel; handled by .get() + {"UNKNOWN"}, + set(), + ), + ( + nvml.NvlinkVersion, + None, # maps to tuple, not a StrEnum + _device._NVLINK_VERSION_MAPPING, + # VERSION_INVALID is a sentinel for "no NvLink present" + {"VERSION_INVALID"}, + set(), + ), + ( + nvml.SystemEventType, + _system_events.SystemEventType, + _system_events._SYSTEM_EVENT_TYPE_MAPPING, + set(), + set(), + ), + ( + nvml.AffinityScope, + _device.AffinityScope, + _device._AFFINITY_SCOPE_MAPPING, + set(), + set(), + ), + ( + nvml.GpuP2PCapsIndex, + _device.GpuP2PCapsIndex, + _device._GPU_P2P_CAPS_INDEX_MAPPING, + # UNKNOWN is returned by the driver when an index is unrecognised; + # it is not a capability the caller selects + {"P2P_CAPS_INDEX_UNKNOWN"}, + # UNKNOWN is a driver-side fallback, not a caller-selectable index + {"UNKNOWN"}, + ), + ( + nvml.GpuTopologyLevel, + _device.GpuTopologyLevel, + _device._GPU_TOPOLOGY_LEVEL_MAPPING, + set(), + set(), + ), + ( + nvml.ClockId, + _device.ClockId, + _device._CLOCK_ID_MAPPING, + # APP_CLOCK_TARGET and APP_CLOCK_DEFAULT are deprecated; COUNT is a sentinel + {"APP_CLOCK_TARGET", "APP_CLOCK_DEFAULT", "COUNT"}, + set(), + ), + ( + nvml.ClockType, + _device.ClockType, + _device._CLOCK_TYPE_MAPPING, + # COUNT is a sentinel + {"CLOCK_COUNT"}, + set(), + ), + ( + nvml.InforomObject, + _device.InforomObject, + _device._INFOROM_OBJECT_MAPPING, + # COUNT is a sentinel + {"INFOROM_COUNT"}, + set(), + ), + ( + nvml.TemperatureThresholds, + _device.TemperatureThresholds, + _device._TEMPERATURE_THRESHOLD_MAPPING, + # COUNT is a sentinel + {"TEMPERATURE_THRESHOLD_COUNT"}, + set(), + ), + ] + ) + + +# StrEnum subclasses that intentionally have no associated cuda_binding. +# Add classes here (with a comment explaining why) when a new StrEnum is +# introduced that wraps something other than a cuda_binding enum. +_UNBOUND_STR_ENUMS: frozenset[type] = frozenset() + + +@pytest.mark.parametrize( + "binding, str_enum, mapping, binding_unmapped, str_enum_unmapped", + _CASES, + ids=[x[0].__name__ for x in _CASES], +) +def test_wrapper_covers_all_binding_members(binding, str_enum, mapping, binding_unmapped, str_enum_unmapped): + """Every cuda_binding enum member must appear in the wrapper mapping (or be allow-listed). + + Also checks the reverse: every StrEnum wrapper member must appear in the + mapping (or be listed in the per-entry str_enum_unmapped set). + """ + required = set(binding.__members__) - binding_unmapped + # Compare by integer value so that enum aliases (two names, one integer) + # are treated as covered when the canonical member appears in the mapping. + covered_values = frozenset(int(m) for m in (*mapping.keys(), *mapping.values()) if isinstance(m, binding)) + missing = {name for name in required if int(binding.__members__[name]) not in covered_values} + assert not missing, f"{binding.__name__} has members not covered by the wrapper mapping: {missing}" + + # Reverse check: every StrEnum member must also appear in the mapping. + if str_enum is not None: + required_str = set(str_enum.__members__) - str_enum_unmapped + covered_str = {m.name for m in (*mapping.keys(), *mapping.values()) if isinstance(m, str_enum)} + missing_str = required_str - covered_str + assert not missing_str, f"{str_enum.__name__} has members not covered by the wrapper mapping: {missing_str}" + + +def test_all_str_enums_in_cases(): + """Every StrEnum subclass in cuda.core must appear in _CASES or _UNBOUND_STR_ENUMS. + + This ensures that when a new StrEnum wrapper is added to cuda.core, the + author is prompted to add a binding-coverage entry to _CASES (or explicitly + declare it as unbound in _UNBOUND_STR_ENUMS). + """ + + def discover_str_enums() -> set[type]: + """Walk all submodules of cuda.core and return every StrEnum subclass found.""" + found: set[type] = set() + for _, modname, _ in pkgutil.walk_packages( + path=cuda.core.__path__, + prefix=cuda.core.__name__ + ".", + onerror=lambda _: None, + ): + try: + mod = importlib.import_module(modname) + except Exception: # noqa + continue + try: + members = inspect.getmembers(mod, inspect.isclass) + except Exception: # noqa + continue + for _, obj in members: + if obj is not StrEnum and issubclass(obj, StrEnum): + found.add(obj) + return found + + covered = {x[1] for x in _CASES if x[1] is not None} + uncovered = discover_str_enums() - covered - _UNBOUND_STR_ENUMS + assert not uncovered, ( + f"StrEnum subclasses in cuda.core not covered by _CASES: " + f"{sorted(c.__qualname__ for c in uncovered)}\n" + "Add a _CASES entry for each, or add to _UNBOUND_STR_ENUMS if it does not wrap a cuda_binding enum." + ) From 7633824b3a6fd54637e538ed7a7ba4516cbad979 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Tue, 5 May 2026 10:17:21 -0400 Subject: [PATCH 064/244] Address a couple of comments in #2014 (#2021) --- cuda_core/cuda/core/system/_device.pyx | 2 -- cuda_core/cuda/core/system/_nvlink.pxi | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index f3cc62fe546..189f778ff4f 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -188,8 +188,6 @@ class GpuP2PStatus(StrEnum): _GPU_P2P_STATUS_MAPPING = { nvml.GpuP2PStatus.P2P_STATUS_OK: GpuP2PStatus.OK, - # Typo in upstream library - nvml.GpuP2PStatus.P2P_STATUS_CHIPSET_NOT_SUPPORED: GpuP2PStatus.CHIPSET_NOT_SUPPORTED, nvml.GpuP2PStatus.P2P_STATUS_CHIPSET_NOT_SUPPORTED: GpuP2PStatus.CHIPSET_NOT_SUPPORTED, nvml.GpuP2PStatus.P2P_STATUS_GPU_NOT_SUPPORTED: GpuP2PStatus.GPU_NOT_SUPPORTED, nvml.GpuP2PStatus.P2P_STATUS_IOH_TOPOLOGY_NOT_SUPPORTED: GpuP2PStatus.IOH_TOPOLOGY_NOT_SUPPORTED, diff --git a/cuda_core/cuda/core/system/_nvlink.pxi b/cuda_core/cuda/core/system/_nvlink.pxi index 6d4a7ac9b2f..ad246b8364f 100644 --- a/cuda_core/cuda/core/system/_nvlink.pxi +++ b/cuda_core/cuda/core/system/_nvlink.pxi @@ -39,7 +39,7 @@ cdef class NvlinkInfo: """ version = nvml.device_get_nvlink_version(self._device._handle, self._link) if version == nvml.NvlinkVersion.VERSION_INVALID: - raise RuntimeError(f"Invalid NvLink version returned for device") + raise RuntimeError("Invalid NvLink version returned for device") try: return _NVLINK_VERSION_MAPPING[version] except KeyError: From dca52926f872022c106e0b71bfb3938047660f2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:51:10 +0000 Subject: [PATCH 065/244] Bump the actions-monthly group with 12 updates (#2006) * Bump the actions-monthly group with 12 updates Bumps the actions-monthly group with 12 updates: | Package | From | To | | --- | --- | --- | | [korthout/backport-action](https://github.com/korthout/backport-action) | `4.3.0` | `4.5.0` | | [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `8.0.0` | `8.1.0` | | [astral-sh/ruff-action](https://github.com/astral-sh/ruff-action) | `3.6.1` | `4.0.0` | | [github/codeql-action](https://github.com/github/codeql-action) | `4.35.1` | `4.35.3` | | [conda-incubator/setup-miniconda](https://github.com/conda-incubator/setup-miniconda) | `3.3.0` | `4.0.1` | | [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) | `4.0.0` | `5.0.0` | | [actions/github-script](https://github.com/actions/github-script) | `8` | `9` | | [actions/setup-python](https://github.com/actions/setup-python) | `5.6.0` | `6.2.0` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` | | [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) | `3.4.0` | `3.4.1` | | [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.13.0` | `1.14.0` | | [mozilla-actions/sccache-action](https://github.com/mozilla-actions/sccache-action) | `0.0.9` | `0.0.10` | Updates `korthout/backport-action` from 4.3.0 to 4.5.0 - [Release notes](https://github.com/korthout/backport-action/releases) - [Commits](https://github.com/korthout/backport-action/compare/3c06f323a58619da1e8522229ebc8d5de2633e46...7c3f6cd5843cac11bc59a04a1b7699af93261670) Updates `astral-sh/setup-uv` from 8.0.0 to 8.1.0 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/cec208311dfd045dd5311c1add060b2062131d57...08807647e7069bb48b6ef5acd8ec9567f424441b) Updates `astral-sh/ruff-action` from 3.6.1 to 4.0.0 - [Release notes](https://github.com/astral-sh/ruff-action/releases) - [Commits](https://github.com/astral-sh/ruff-action/compare/4919ec5cf1f49eff0871dbcea0da843445b837e6...0ce1b0bf8b818ef400413f810f8a11cdbda0034b) Updates `github/codeql-action` from 4.35.1 to 4.35.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Commits](https://github.com/github/codeql-action/compare/v4.35.1...v4.35.3) Updates `conda-incubator/setup-miniconda` from 3.3.0 to 4.0.1 - [Release notes](https://github.com/conda-incubator/setup-miniconda/releases) - [Changelog](https://github.com/conda-incubator/setup-miniconda/blob/main/CHANGELOG.md) - [Commits](https://github.com/conda-incubator/setup-miniconda/compare/fc2d68f6413eb2d87b895e92f8584b5b94a10167...8ee1f361103df19b6f8c8655fd3967a8ecb162d5) Updates `actions/upload-pages-artifact` from 4.0.0 to 5.0.0 - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/7b1f4a764d45c48632c6b24a0339c27f5614fb0b...fc324d3547104276b827a68afc52ff2a11cc49c9) Updates `actions/github-script` from 8 to 9 - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v8...v9) Updates `actions/setup-python` from 5.6.0 to 6.2.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5.6.0...a309ff8b426b58ec0e2a45f0f869d46889d02405) Updates `actions/upload-artifact` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) Updates `pypa/cibuildwheel` from 3.4.0 to 3.4.1 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/ee02a1537ce3071a004a6b08c41e72f0fdc42d9a...8d2b08b68458a16aeb24b64e68a09ab1c8e82084) Updates `pypa/gh-action-pypi-publish` from 1.13.0 to 1.14.0 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e...cef221092ed1bacb1cc03d23a2d87d1d172e277b) Updates `mozilla-actions/sccache-action` from 0.0.9 to 0.0.10 - [Release notes](https://github.com/mozilla-actions/sccache-action/releases) - [Commits](https://github.com/mozilla-actions/sccache-action/compare/7d986dd989559c6ecdb630a3fd2557667be217ad...9e7fa8a12102821edf02ca5dbea1acd0f89a2696) --- updated-dependencies: - dependency-name: korthout/backport-action dependency-version: 4.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-monthly - dependency-name: astral-sh/setup-uv dependency-version: 8.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-monthly - dependency-name: astral-sh/ruff-action dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-monthly - dependency-name: github/codeql-action dependency-version: 4.35.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-monthly - dependency-name: conda-incubator/setup-miniconda dependency-version: 4.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-monthly - dependency-name: actions/upload-pages-artifact dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-monthly - dependency-name: actions/github-script dependency-version: '9' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-monthly - dependency-name: actions/setup-python dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-monthly - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-monthly - dependency-name: pypa/cibuildwheel dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-monthly - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-monthly - dependency-name: mozilla-actions/sccache-action dependency-version: 0.0.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-monthly ... Signed-off-by: dependabot[bot] * fix codeql pinnings --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Leo Fang --- .github/workflows/backport.yml | 4 ++-- .github/workflows/bandit.yml | 6 +++--- .github/workflows/build-docs.yml | 4 ++-- .github/workflows/build-wheel.yml | 20 +++++++++---------- .github/workflows/codeql.yml | 4 ++-- .github/workflows/coverage.yml | 10 +++++----- .github/workflows/release-cuda-pathfinder.yml | 4 ++-- .github/workflows/release.yml | 6 +++--- .github/workflows/test-sdist-linux.yml | 4 ++-- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 35646370b7c..3c48f1a48b6 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -43,7 +43,7 @@ jobs: echo "OLD_BRANCH=${OLD_BRANCH}" >> $GITHUB_ENV - name: Create backport pull requests - uses: korthout/backport-action@3c06f323a58619da1e8522229ebc8d5de2633e46 # v4.3.0 + uses: korthout/backport-action@7c3f6cd5843cac11bc59a04a1b7699af93261670 # v4.5.0 with: copy_assignees: true copy_labels_pattern: true @@ -67,7 +67,7 @@ jobs: run: echo "BACKPORT_BRANCH=${{ inputs.backport-branch }}" >> $GITHUB_ENV - name: Create backport pull requests - uses: korthout/backport-action@3c06f323a58619da1e8522229ebc8d5de2633e46 # v4.3.0 + uses: korthout/backport-action@7c3f6cd5843cac11bc59a04a1b7699af93261670 # v4.5.0 with: copy_assignees: true copy_labels_pattern: true diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml index 2567ab6eb71..4abcf260336 100644 --- a/.github/workflows/bandit.yml +++ b/.github/workflows/bandit.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: false @@ -38,10 +38,10 @@ jobs: echo "codes=$(uvx toml2json ./ruff.toml | jq -r '.lint.ignore | map(select(test("^S\\d+"))) | join(",")')" >> "$GITHUB_OUTPUT" - name: Perform Bandit Analysis using Ruff - uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1 + uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0 with: args: "check --select S --ignore ${{ steps.ignore-codes.outputs.codes }} --output-format sarif --output-file results.sarif" - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v4.35.1 + uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: sarif_file: results.sarif diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 123f2300f63..1bd0c0194c1 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -62,7 +62,7 @@ jobs: # TODO: This workflow runs on GH-hosted runner and cannot use the proxy cache - name: Set up miniforge - uses: conda-incubator/setup-miniconda@fc2d68f6413eb2d87b895e92f8584b5b94a10167 # v3.3.0 + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1 with: activate-environment: cuda-python-docs environment-file: ./cuda_python/docs/environment-docs.yml @@ -244,7 +244,7 @@ jobs: # TODO: Consider removing this step? - name: Upload doc artifacts - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: artifacts/ retention-days: 3 diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index fbb8d092b06..cbfb793d84f 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -54,7 +54,7 @@ jobs: # xref: https://github.com/orgs/community/discussions/42856#discussioncomment-7678867 - name: Adding addtional GHA cache-related env vars - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | core.exportVariable('ACTIONS_CACHE_SERVICE_V2', 'on'); @@ -148,7 +148,7 @@ jobs: - name: Upload cuda.pathfinder build artifacts if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cuda-pathfinder-wheel path: cuda_pathfinder/*.whl @@ -162,7 +162,7 @@ jobs: cuda-version: ${{ inputs.cuda-version }} - name: Build cuda.bindings wheel - uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 with: package-dir: ./cuda_bindings/ output-dir: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} @@ -219,14 +219,14 @@ jobs: twine check --strict ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl - name: Upload cuda.bindings build artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl if-no-files-found: error - name: Build cuda.core wheel - uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 with: package-dir: ./cuda_core/ output-dir: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} @@ -316,7 +316,7 @@ jobs: - name: Upload cuda-python build artifacts if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cuda-python-wheel path: cuda_python/*.whl @@ -354,7 +354,7 @@ jobs: popd - name: Upload cuda.bindings Cython tests - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests path: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} @@ -368,7 +368,7 @@ jobs: popd - name: Upload cuda.core Cython tests - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} @@ -415,7 +415,7 @@ jobs: rmdir $OLD_BASENAME - name: Build cuda.core wheel - uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 with: package-dir: ./cuda_core/ output-dir: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} @@ -497,7 +497,7 @@ jobs: twine check --strict ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl - name: Upload cuda.core build artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0bde4858fd4..1f36bd0d694 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,13 +31,13 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Initialize CodeQL - uses: github/codeql-action/init@34950e1b113b30df4edee1a6d3a605242df0c40b # v3.31.8 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} queries: security-extended - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@34950e1b113b30df4edee1a6d3a605242df0c40b # v3.31.8 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 95deb813f04..53b2aa6c858 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -164,7 +164,7 @@ jobs: ls -lh $REPO_ROOT/.coverage.linux - name: Upload Linux coverage data - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-data-linux path: .coverage.linux @@ -173,7 +173,7 @@ jobs: if-no-files-found: error - name: Upload cuda source code for coverage mapping - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cuda-source-linux path: ${{ env.INSTALL_ROOT }}/cuda/ @@ -242,7 +242,7 @@ jobs: ls -lahR ./wheels/ - name: Upload Windows wheel artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-windows-wheels path: ./wheels/*.whl @@ -373,7 +373,7 @@ jobs: ls -lh "$GITHUB_WORKSPACE/.coverage.windows" - name: Upload Windows coverage data - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-data-windows path: .coverage.windows @@ -475,7 +475,7 @@ jobs: echo "[SUCCESS] Coverage reports generated successfully" - name: Archive combined coverage results - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-combined path: docs/coverage/ diff --git a/.github/workflows/release-cuda-pathfinder.yml b/.github/workflows/release-cuda-pathfinder.yml index c2ba575eccb..ab1fdbaaeca 100644 --- a/.github/workflows/release-cuda-pathfinder.yml +++ b/.github/workflows/release-cuda-pathfinder.yml @@ -231,7 +231,7 @@ jobs: ls -la dist - name: Publish to TestPyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: repository-url: https://test.pypi.org/legacy/ @@ -308,7 +308,7 @@ jobs: ls -la dist - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 # -------------------------------------------------------------------------- # Verify the PyPI package installs and imports correctly. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7cbc847e2c8..e555e015003 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,7 +97,7 @@ jobs: ref: ${{ inputs.git-tag }} - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" @@ -176,7 +176,7 @@ jobs: ./ci/tools/validate-release-wheels "${{ inputs.git-tag }}" "${{ inputs.component }}" "dist" - name: Publish package distributions to TestPyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: repository-url: https://test.pypi.org/legacy/ @@ -206,6 +206,6 @@ jobs: ./ci/tools/validate-release-wheels "${{ inputs.git-tag }}" "${{ inputs.component }}" "dist" - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 # TODO: add another job to make the release leave the draft state? diff --git a/.github/workflows/test-sdist-linux.yml b/.github/workflows/test-sdist-linux.yml index 49a68877cc0..d4ece790cf4 100644 --- a/.github/workflows/test-sdist-linux.yml +++ b/.github/workflows/test-sdist-linux.yml @@ -52,13 +52,13 @@ jobs: # The env vars ACTIONS_CACHE_SERVICE_V2, ACTIONS_RESULTS_URL, and ACTIONS_RUNTIME_TOKEN # are exposed by this action. - name: Enable sccache - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # 0.0.9 + uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # 0.0.10 with: disable_annotations: 'true' # xref: https://github.com/orgs/community/discussions/42856#discussioncomment-7678867 - name: Adding additional GHA cache-related env vars - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL']) From 93f513eee49ade0f7ba5efc39af3c43202b648f8 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Tue, 5 May 2026 11:08:47 -0400 Subject: [PATCH 066/244] Simplify cuda_core nvml version check (#2017) --- cuda_core/cuda/core/system/_system.pyx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cuda_core/cuda/core/system/_system.pyx b/cuda_core/cuda/core/system/_system.pyx index d1a7e97e1b6..815b366c87b 100644 --- a/cuda_core/cuda/core/system/_system.pyx +++ b/cuda_core/cuda/core/system/_system.pyx @@ -15,7 +15,7 @@ try: except ImportError: CUDA_BINDINGS_NVML_IS_COMPATIBLE = False else: - CUDA_BINDINGS_NVML_IS_COMPATIBLE = _BINDINGS_VERSION >= (13, 1, 2) or (_BINDINGS_VERSION[0] == 12 and _BINDINGS_VERSION[1:3] >= (9, 6)) + CUDA_BINDINGS_NVML_IS_COMPATIBLE = _BINDINGS_VERSION >= (13, 2, 0) or (_BINDINGS_VERSION[0] == 12 and _BINDINGS_VERSION[1:3] >= (9, 6)) if CUDA_BINDINGS_NVML_IS_COMPATIBLE: @@ -23,15 +23,7 @@ if CUDA_BINDINGS_NVML_IS_COMPATIBLE: from cuda.bindings import nvml except ImportError: CUDA_BINDINGS_NVML_IS_COMPATIBLE = False - else: - # TODO: We need to be even more specific than version numbers for development. - # This can be removed once we have a release including everything we need. - for member in ["FieldId", "ClocksEventReasons"]: - if not hasattr(nvml, member): - CUDA_BINDINGS_NVML_IS_COMPATIBLE = False - break -if CUDA_BINDINGS_NVML_IS_COMPATIBLE: from ._nvml_context import initialize else: from cuda.core._utils.cuda_utils import driver, handle_return, runtime From 98df790d491f0fb78c9894e441404f85efba280e Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Tue, 5 May 2026 14:29:49 -0400 Subject: [PATCH 067/244] Add CUDA process checkpointing helpers (#1983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add CUDA process checkpointing helpers * Address checkpoint review feedback * Rewrite checkpoint tests: replace mocks with real GPU tests Replace the entire mock-based test suite with real GPU tests that exercise the CUDA driver checkpoint API directly: - Input validation: pid type/range, public symbol checks - Lifecycle (single GPU): state transitions at every stage (running→locked→checkpointed→locked→running), restore_thread_id, lock/unlock, lock with timeout, full checkpoint-restore cycle - GPU migration: rotation mapping and same-chip swap following the r580-migration-api.c pattern; gracefully skip when the driver does not support migration (CUDA_ERROR_INVALID_VALUE — NVBug 5437334) The self_process fixture wraps os.getpid() and safety-unlocks on teardown if the test fails mid-lifecycle. Co-Authored-By: Claude Opus 4.6 (1M context) * Accept Device.uuid strings in gpu_mapping; use cuda.core APIs in tests - checkpoint._make_restore_args now accepts UUID strings (as returned by Device.uuid) in addition to CUuuid objects, via a new _as_cuuuid helper that converts "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" strings to CUuuid using ctypes. - Tests no longer import cuda.bindings.driver; all device queries use cuda.core.Device (Device().uuid for current device, Device.uuid for mapping keys/values, Device.get_all_devices() for enumeration). Co-Authored-By: Claude Opus 4.6 (1M context) * Apply pre-commit formatting fixes Ruff import sorting, ruff format, and noqa annotation for best-effort teardown in the self_process fixture. Co-Authored-By: Claude Opus 4.6 (1M context) * Restore original device in self_process fixture teardown The swap migration test calls set_current() on a different device. Record the initial device from init_cuda and restore it on teardown so tests are side-effect free. Co-Authored-By: Claude Opus 4.6 (1M context) * Address checkpoint review follow-ups * Skip checkpoint lifecycle/migration tests in CI cuCheckpointProcessCheckpoint hangs on CI runners (ephemeral VM + container), causing all CUDA 13.x test jobs to time out. Skip the tests that call into the checkpoint driver when the CI environment variable is set. Input validation tests still run everywhere. Co-Authored-By: Claude Opus 4.6 (1M context) * Isolate checkpoint lifecycle tests * Address checkpoint review follow-ups * Fix checkpoint subprocess imports in CI * Handle checkpoint migration no-op in CI * Simplify checkpoint subprocess tests --------- Co-authored-by: Leo Fang Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_core/cuda/core/__init__.py | 2 +- cuda_core/cuda/core/checkpoint.py | 254 ++++++++++ cuda_core/cuda/core/typing.py | 5 + cuda_core/docs/source/api.rst | 62 +++ cuda_core/docs/source/api_private.rst | 1 + cuda_core/docs/source/release/1.0.0-notes.rst | 5 +- cuda_core/tests/test_checkpoint.py | 454 ++++++++++++++++++ cuda_core/tests/test_typing_imports.py | 4 + 8 files changed, 785 insertions(+), 2 deletions(-) create mode 100644 cuda_core/cuda/core/checkpoint.py create mode 100644 cuda_core/tests/test_checkpoint.py diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index 52ebb9e3ddb..73f9a166675 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -28,7 +28,7 @@ def _import_versioned_module(): del _import_versioned_module -from cuda.core import system, utils +from cuda.core import checkpoint, system, utils from cuda.core._device import Device from cuda.core._event import Event, EventOptions from cuda.core._graphics import GraphicsResource diff --git a/cuda_core/cuda/core/checkpoint.py b/cuda_core/cuda/core/checkpoint.py new file mode 100644 index 00000000000..b5831f030ed --- /dev/null +++ b/cuda_core/cuda/core/checkpoint.py @@ -0,0 +1,254 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import ctypes as _ctypes +from collections.abc import Mapping as _Mapping +from typing import Any as _Any + +from cuda.core._utils.cuda_utils import handle_return as _handle_cuda_return +from cuda.core._utils.version import binding_version as _binding_version +from cuda.core._utils.version import driver_version as _driver_version +from cuda.core.typing import ProcessStateT as _ProcessStateT + +try: + from cuda.bindings import driver as _driver +except ImportError: + from cuda import cuda as _driver + + +_PROCESS_STATE_NAME_ATTRS: tuple[tuple[str, _ProcessStateT], ...] = ( + ("CU_PROCESS_STATE_RUNNING", "running"), + ("CU_PROCESS_STATE_LOCKED", "locked"), + ("CU_PROCESS_STATE_CHECKPOINTED", "checkpointed"), + ("CU_PROCESS_STATE_FAILED", "failed"), +) + +_REQUIRED_BINDING_ATTRS = ( + "cuCheckpointProcessCheckpoint", + "cuCheckpointProcessGetRestoreThreadId", + "cuCheckpointProcessGetState", + "cuCheckpointProcessLock", + "cuCheckpointProcessRestore", + "cuCheckpointProcessUnlock", + "CUcheckpointGpuPair", + "CUcheckpointLockArgs", + "CUprocessState", + "CUcheckpointRestoreArgs", +) +_REQUIRED_DRIVER_VERSION = (12, 8, 0) +_driver_capability_checked = False + + +class Process: + """ + CUDA process that can be locked, checkpointed, restored, and unlocked. + + Parameters + ---------- + pid : int + Process ID of the CUDA process. + """ + + __slots__ = ("_pid",) + + def __init__(self, pid: int): + self._pid = _check_pid(pid) + + @property + def pid(self) -> int: + """ + Process ID of the CUDA process. + """ + return self._pid + + @property + def state(self) -> _ProcessStateT: + """ + CUDA checkpoint state for this process. + """ + driver = _get_driver() + state = _call_driver(driver, driver.cuCheckpointProcessGetState, self._pid) + state_names = _get_process_state_names(driver) + try: + return state_names[state] + except KeyError as e: + state_value = int(state) + raise RuntimeError(f"Unknown CUDA checkpoint process state: {state_value}") from e + + @property + def restore_thread_id(self) -> int: + """ + CUDA restore thread ID for this process. + """ + driver = _get_driver() + return _call_driver(driver, driver.cuCheckpointProcessGetRestoreThreadId, self._pid) + + def lock(self, timeout_ms: int = 0) -> None: + """ + Lock this process, blocking further CUDA API calls. + + Parameters + ---------- + timeout_ms : int, optional + Timeout in milliseconds. A value of 0 indicates no timeout. + """ + driver = _get_driver() + args = driver.CUcheckpointLockArgs() + args.timeoutMs = _check_timeout_ms(timeout_ms) + _call_driver(driver, driver.cuCheckpointProcessLock, self._pid, args) + + def checkpoint(self) -> None: + """ + Checkpoint the GPU memory contents of this locked process. + """ + driver = _get_driver() + _call_driver(driver, driver.cuCheckpointProcessCheckpoint, self._pid, None) + + def restore(self, gpu_mapping: _Mapping[_Any, _Any] | None = None) -> None: + """ + Restore this checkpointed process. + + Parameters + ---------- + gpu_mapping : mapping, optional + GPU UUID remapping from each checkpointed GPU UUID to the GPU UUID + to restore onto. For migration workflows, provide mappings for + every GPU visible to the kernel-mode driver. User-space masking + such as ``CUDA_VISIBLE_DEVICES`` does not reduce this mapping + requirement. + """ + driver = _get_driver() + args = _make_restore_args(driver, gpu_mapping) + _call_driver(driver, driver.cuCheckpointProcessRestore, self._pid, args) + + def unlock(self) -> None: + """ + Unlock this locked process so it can resume CUDA API calls. + """ + driver = _get_driver() + _call_driver(driver, driver.cuCheckpointProcessUnlock, self._pid, None) + + +def _get_driver(): + global _driver_capability_checked + if _driver_capability_checked: + return _driver + + binding_ver = _binding_version() + if not _binding_version_supports_checkpoint(binding_ver): + raise RuntimeError( + "CUDA checkpointing requires cuda.bindings with CUDA checkpoint API support. " + f"Found cuda.bindings {'.'.join(str(part) for part in binding_ver[:3])}." + ) + + missing = [name for name in _REQUIRED_BINDING_ATTRS if not hasattr(_driver, name)] + if missing: + raise RuntimeError( + f"CUDA checkpointing requires cuda.bindings with CUDA checkpoint API support. Missing: {', '.join(missing)}" + ) + + driver_ver = _driver_version() + if driver_ver < _REQUIRED_DRIVER_VERSION: + raise RuntimeError( + "CUDA checkpointing is not supported by the installed NVIDIA driver. " + "Upgrade to a driver version with CUDA checkpoint API support." + ) + + _driver_capability_checked = True + return _driver + + +def _binding_version_supports_checkpoint(version) -> bool: + major, minor, patch = version[:3] + return (major == 12 and (minor, patch) >= (8, 0)) or (major == 13 and (minor, patch) >= (0, 2)) or major > 13 + + +def _get_process_state_names(driver) -> dict[_Any, _ProcessStateT]: + return {getattr(driver.CUprocessState, attr): state_name for attr, state_name in _PROCESS_STATE_NAME_ATTRS} + + +def _call_driver(driver, func, *args): + try: + result = func(*args) + except RuntimeError as e: + if "cuCheckpointProcess" in str(e) and "not found" in str(e): + raise RuntimeError( + "CUDA checkpointing is not supported by the installed NVIDIA driver. " + "Upgrade to a driver version with CUDA checkpoint API support." + ) from e + raise + + err = result[0] + not_supported_errors = ( + getattr(driver.CUresult, "CUDA_ERROR_NOT_FOUND", None), + getattr(driver.CUresult, "CUDA_ERROR_NOT_SUPPORTED", None), + ) + if err in not_supported_errors: + raise RuntimeError( + "CUDA checkpointing is not supported by the installed NVIDIA driver. " + "Upgrade to a driver version with CUDA checkpoint API support." + ) + + return _handle_cuda_return(result) + + +def _check_pid(pid: int) -> int: + if isinstance(pid, bool) or not isinstance(pid, int): + raise TypeError("pid must be an int") + if pid <= 0: + raise ValueError("pid must be a positive int") + return pid + + +def _check_timeout_ms(timeout_ms: int) -> int: + if isinstance(timeout_ms, bool) or not isinstance(timeout_ms, int): + raise TypeError("timeout_ms must be an int") + if timeout_ms < 0: + raise ValueError("timeout_ms must be >= 0") + return timeout_ms + + +def _make_restore_args(driver, gpu_mapping: _Mapping[_Any, _Any] | None): + if gpu_mapping is None: + return None + if not isinstance(gpu_mapping, _Mapping): + raise TypeError("gpu_mapping must be a mapping from checkpointed GPU UUID to restore GPU UUID") + + pairs = [] + for old_uuid, new_uuid in gpu_mapping.items(): + pair = driver.CUcheckpointGpuPair() + buffers = [] + pair.oldUuid = _as_cuuuid(driver, old_uuid, buffers) + pair.newUuid = _as_cuuuid(driver, new_uuid, buffers) + pairs.append(pair) + + if not pairs: + return None + + args = driver.CUcheckpointRestoreArgs() + args.gpuPairs = pairs + args.gpuPairsCount = len(pairs) + return args + + +def _as_cuuuid(driver, value, buffers): + """Convert *value* to a ``CUuuid``. + + Accepts a ``CUuuid`` instance (returned as-is) or a UUID string in + the ``"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"`` format returned by + :attr:`Device.uuid`. + """ + if isinstance(value, str): + raw = bytes.fromhex(value.replace("-", "")) + if len(raw) != 16: + raise ValueError(f"GPU UUID string must be 32 hex characters (with optional hyphens), got {value!r}") + buf = _ctypes.create_string_buffer(raw, 16) + buffers.append(buf) + return driver.CUuuid(_ctypes.addressof(buf)) + return value + + +__all__ = [ + "Process", +] diff --git a/cuda_core/cuda/core/typing.py b/cuda_core/cuda/core/typing.py index a66ab1881fb..e95331d463f 100644 --- a/cuda_core/cuda/core/typing.py +++ b/cuda_core/cuda/core/typing.py @@ -4,10 +4,15 @@ """Public type aliases and protocols used in cuda.core API signatures.""" +from typing import Literal as _Literal + from cuda.core._memory._buffer import DevicePointerT from cuda.core._stream import IsStreamT +ProcessStateT = _Literal["running", "locked", "checkpointed", "failed"] + __all__ = [ "DevicePointerT", "IsStreamT", + "ProcessStateT", ] diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 77f04e6cd8c..7e8632b1893 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -174,6 +174,68 @@ CUDA compilation toolchain LinkerOptions +CUDA process checkpointing +-------------------------- + +The :mod:`cuda.core.checkpoint` module wraps the CUDA driver process +checkpoint APIs. These APIs are intended for Linux process checkpoint and +restore workflows, and require a CUDA driver with checkpoint API support and +a ``cuda-bindings`` version that exposes those driver entry points. + +Checkpointing is typically driven by a coordinator process acting on a target +CUDA process, similar to attaching a debugger or sending a signal. The target +process is identified by process ID. Linux and the CUDA driver enforce process +permissions; checkpointing another user's process may require elevated +permissions such as ``CAP_SYS_PTRACE`` or administrator privileges. + +The CUDA checkpoint APIs prepare CUDA-managed GPU state for process-level +checkpoint and restore. They do not capture the CPU process image by +themselves; full process checkpoint workflows still need a CPU-side process +checkpointing tool such as CRIU. A minimal coordinator-side sequence looks like +this: + +.. code-block:: python + + import os + + from cuda.core import checkpoint + + target_pid = os.getpid() # or the PID of another CUDA process + process = checkpoint.Process(target_pid) + process.lock(timeout_ms=5000) + process.checkpoint() + + # Capture or restore the CPU process image outside cuda.core. + + process.restore() + process.unlock() + +``Process.state`` returns one of ``"running"``, ``"locked"``, +``"checkpointed"``, or ``"failed"``. Restore may optionally remap GPUs by +passing ``gpu_mapping`` from each checkpointed GPU UUID to the GPU UUID that +should be used during restore. For migration workflows, provide mappings for +every GPU visible to the NVIDIA kernel-mode driver at checkpoint time. +User-space masking such as ``CUDA_VISIBLE_DEVICES`` does not reduce this +mapping requirement, so applications that rely on user-space GPU masking may +not be valid migration targets. The mapping may use ``CUuuid`` objects or the +UUID strings returned by :attr:`Device.uuid`. A successful restore returns the +process to the locked state; call ``Process.unlock`` after restore to allow +CUDA API calls to resume. + +The CUDA driver requires restore to run from the process restore thread. +Use ``Process.restore_thread_id`` to discover that thread before calling +``Process.restore`` from a checkpoint coordinator. Restore also requires +persistence mode to be enabled or ``cuInit`` to have been called before +execution. + +.. autosummary:: + :toctree: generated/ + + :template: class.rst + + checkpoint.Process + + CUDA system information and NVIDIA Management Library (NVML) ------------------------------------------------------------ diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index a3bb4f1395c..b10898841cc 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -17,6 +17,7 @@ CUDA runtime :toctree: generated/ typing.DevicePointerT + typing.ProcessStateT _memory._virtual_memory_resource.VirtualMemoryAllocationTypeT _memory._virtual_memory_resource.VirtualMemoryLocationTypeT _memory._virtual_memory_resource.VirtualMemoryGranularityT diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst index 3f61a30ec1e..35a9eb7304e 100644 --- a/cuda_core/docs/source/release/1.0.0-notes.rst +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -16,7 +16,10 @@ Highlights New features ------------ -- TBD +- Added the :mod:`cuda.core.checkpoint` module for CUDA process checkpointing, + including string process state queries, lock/checkpoint/restore/unlock + operations, and GPU UUID remapping support for restore. + (`#1343 `__) Breaking changes diff --git a/cuda_core/tests/test_checkpoint.py b/cuda_core/tests/test_checkpoint.py new file mode 100644 index 00000000000..8bf62e82901 --- /dev/null +++ b/cuda_core/tests/test_checkpoint.py @@ -0,0 +1,454 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Real GPU tests for cuda.core.checkpoint — no mocks. +# +# Driver-backed lifecycle tests run through an isolated coordinator/target +# process pair so hangs can be timed out without wedging the pytest process. +# +# Migration tests attempt GPU UUID remapping following the pattern from +# NVIDIA/cuda-checkpoint r580-migration-api.c. They require ≥2 GPUs of +# the same chip type, an unmasked CUDA device view, and a driver that supports +# migration; the tests skip gracefully when the hardware or driver cannot +# satisfy this. + +import os +import signal +import subprocess +import sys +from contextlib import suppress +from pathlib import Path + +import pytest + +from cuda.core import Device, checkpoint +from cuda.core._utils.cuda_utils import CUDAError + +# -- Skip condition ------------------------------------------------------- + + +def _checkpoint_available(): + """Return True if the checkpoint API is usable on this system.""" + try: + checkpoint._get_driver() + return True + except RuntimeError: + return False + + +needs_checkpoint = pytest.mark.skipif( + sys.platform != "linux" or not _checkpoint_available(), + reason="CUDA checkpoint API requires Linux and a supported driver/bindings", +) + + +# -- Helpers --------------------------------------------------------------- + + +_SCENARIO_SKIP_EXIT_CODE = 77 +_CHECKPOINT_TEST_SCRIPT = Path(__file__).resolve() + + +def _checkpoint_child_command(*args: str) -> list[str]: + return [sys.executable, "-I", str(_CHECKPOINT_TEST_SCRIPT), *args] + + +def _scenario_skip(reason): + print(f"SKIP: {reason}", flush=True) + raise SystemExit(_SCENARIO_SKIP_EXIT_CODE) + + +def _run_or_skip_unsupported(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except RuntimeError as exc: + if "CUDA checkpointing is not supported" in str(exc): + _scenario_skip(str(exc)) + raise + + +def _build_rotation_mapping(devices): + n = len(devices) + return {devices[i].uuid: devices[(i + 1) % n].uuid for i in range(n)} + + +def _find_same_chip_pair(devices): + seen = {} + for i, dev in enumerate(devices): + name = dev.name + if name in seen: + return (seen[name], i) + seen[name] = i + return None + + +def _read_prefixed(target, prefix): + line = target.stdout.readline() + if not line: + stderr = target.stderr.read() + raise RuntimeError(f"checkpoint target exited before {prefix!r}; stderr:\n{stderr}") + line = line.strip() + if not line.startswith(prefix): + raise RuntimeError(f"expected target output prefix {prefix!r}, got {line!r}") + return line[len(prefix) :] + + +def _start_target(device_index=0): + target = subprocess.Popen( # noqa: S603 - controlled test subprocess using this Python executable. + _checkpoint_child_command("target", str(device_index)), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + try: + ready_uuid = _read_prefixed(target, "READY:") + except Exception: + _stop_target(target) + raise + return target, ready_uuid + + +def _stop_target(target): + if target.poll() is None: + with suppress(Exception): + target.stdin.write("exit\n") + target.stdin.flush() + try: + target.wait(timeout=5) + except subprocess.TimeoutExpired: + target.kill() + target.wait() + + +def _target_uuid(target): + target.stdin.write("uuid\n") + target.stdin.flush() + return _read_prefixed(target, "UUID:") + + +def _checkpoint_restore(proc, gpu_mapping=None): + _run_or_skip_unsupported(proc.lock, timeout_ms=5000) + _run_or_skip_unsupported(proc.checkpoint) + try: + _run_or_skip_unsupported(proc.restore, gpu_mapping=gpu_mapping) + except (CUDAError, RuntimeError) as exc: + with suppress(Exception): + proc.restore() + with suppress(Exception): + proc.unlock() + if "INVALID_VALUE" in str(exc): + _scenario_skip( + "Driver does not support GPU migration on this hardware (CUDA_ERROR_INVALID_VALUE; see NVBug 5437334)" + ) + raise + proc.unlock() + + +def _run_checkpoint_scenario_or_skip(scenario: str, *, timeout: int = 90) -> None: + """Run mutating checkpoint/restore scenarios out-of-process. + + The CUDA checkpoint APIs can block inside the driver when a runner exposes + symbols but the platform path cannot complete checkpoint/restore. Running + the scenario in its own process group lets the parent test skip that runner + cleanly instead of hanging the entire CI job. + """ + proc = subprocess.Popen( # noqa: S603 - controlled test subprocess using this Python executable. + _checkpoint_child_command("scenario", scenario), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + ) + try: + stdout, stderr = proc.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + with suppress(ProcessLookupError): + os.killpg(proc.pid, signal.SIGKILL) + stdout, stderr = proc.communicate() + pytest.skip( + f"CUDA checkpoint scenario timed out after {timeout}s; driver/hardware did not complete " + f"checkpoint/restore.\nstdout:\n{stdout}\nstderr:\n{stderr}" + ) + + if proc.returncode == _SCENARIO_SKIP_EXIT_CODE: + reason = stdout.strip() or stderr.strip() or "CUDA checkpoint scenario skipped" + pytest.skip(reason) + if proc.returncode != 0: + pytest.fail( + f"CUDA checkpoint scenario failed with exit code {proc.returncode}.\nstdout:\n{stdout}\nstderr:\n{stderr}" + ) + + +def _target_main(device_index: int) -> None: + Device(device_index).set_current() + print(f"READY:{Device().uuid}", flush=True) + + for line in sys.stdin: + command = line.strip() + if command == "uuid": + print(f"UUID:{Device().uuid}", flush=True) + elif command == "exit": + break + + +def _scenario_initial_state_is_running() -> None: + target, _ = _start_target() + proc = checkpoint.Process(target.pid) + try: + assert proc.state == "running" + finally: + _stop_target(target) + + +def _scenario_restore_thread_id_is_positive() -> None: + target, _ = _start_target() + proc = checkpoint.Process(target.pid) + try: + tid = proc.restore_thread_id + assert isinstance(tid, int) + assert tid > 0 + finally: + _stop_target(target) + + +def _scenario_lock_unlock() -> None: + target, _ = _start_target() + proc = checkpoint.Process(target.pid) + try: + _run_or_skip_unsupported(proc.lock) + assert proc.state == "locked" + proc.unlock() + assert proc.state == "running" + finally: + _stop_target(target) + + +def _scenario_lock_default_timeout() -> None: + target, _ = _start_target() + proc = checkpoint.Process(target.pid) + try: + _run_or_skip_unsupported(proc.lock) + assert proc.state == "locked" + proc.unlock() + finally: + _stop_target(target) + + +def _scenario_lock_with_timeout() -> None: + target, _ = _start_target() + proc = checkpoint.Process(target.pid) + try: + _run_or_skip_unsupported(proc.lock, timeout_ms=5000) + assert proc.state == "locked" + proc.unlock() + finally: + _stop_target(target) + + +def _scenario_full_cycle_no_migration() -> None: + target, _ = _start_target() + proc = checkpoint.Process(target.pid) + try: + _run_or_skip_unsupported(proc.lock, timeout_ms=5000) + assert proc.state == "locked" + + _run_or_skip_unsupported(proc.checkpoint) + assert proc.state == "checkpointed" + + _run_or_skip_unsupported(proc.restore) + assert proc.state == "locked" # restore leaves process locked + + proc.unlock() + assert proc.state == "running" + finally: + _stop_target(target) + + +def _scenario_rotation_migrates_context() -> None: + devices = Device.get_all_devices() + if "CUDA_VISIBLE_DEVICES" in os.environ: + _scenario_skip( + "GPU migration tests require an unmasked CUDA device view because " + "the checkpoint mapping must cover every GPU visible to the kernel-mode driver" + ) + if len(devices) < 2: + _scenario_skip("GPU migration tests require at least 2 GPUs") + if _find_same_chip_pair(devices) is None: + _scenario_skip("GPU migration requires at least 2 GPUs of the same chip type") + + gpu_mapping = _build_rotation_mapping(devices) + target, uuid_origin = _start_target(0) + current_uuid = uuid_origin + proc = checkpoint.Process(target.pid) + try: + for step in range(len(devices)): + expected_uuid = devices[(step + 1) % len(devices)].uuid + _checkpoint_restore(proc, gpu_mapping=gpu_mapping) + observed_uuid = _target_uuid(target) + if observed_uuid == current_uuid: + _scenario_skip("Driver accepted GPU rotation but migration is a no-op on this hardware/driver version") + assert observed_uuid == expected_uuid, f"Step {step}: expected UUID {expected_uuid}, got {observed_uuid}" + current_uuid = observed_uuid + + assert _target_uuid(target) == uuid_origin + finally: + _stop_target(target) + + +def _scenario_swap_identical_gpus() -> None: + devices = Device.get_all_devices() + if "CUDA_VISIBLE_DEVICES" in os.environ: + _scenario_skip( + "GPU migration tests require an unmasked CUDA device view because " + "the checkpoint mapping must cover every GPU visible to the kernel-mode driver" + ) + pair = _find_same_chip_pair(devices) + if pair is None: + _scenario_skip("No two GPUs of the same chip type found") + + i, j = pair + gpu_mapping = {d.uuid: d.uuid for d in devices} + gpu_mapping[devices[i].uuid] = devices[j].uuid + gpu_mapping[devices[j].uuid] = devices[i].uuid + + target, uuid_before = _start_target(i) + proc = checkpoint.Process(target.pid) + try: + assert uuid_before == devices[i].uuid + + _checkpoint_restore(proc, gpu_mapping=gpu_mapping) + uuid_after = _target_uuid(target) + + if uuid_after == devices[i].uuid: + _scenario_skip("Driver accepted GPU swap but migration is a no-op on this hardware/driver version") + assert uuid_after == devices[j].uuid + finally: + _stop_target(target) + + +_SCENARIOS = { + "initial_state_is_running": _scenario_initial_state_is_running, + "restore_thread_id_is_positive": _scenario_restore_thread_id_is_positive, + "lock_unlock": _scenario_lock_unlock, + "lock_default_timeout": _scenario_lock_default_timeout, + "lock_with_timeout": _scenario_lock_with_timeout, + "full_cycle_no_migration": _scenario_full_cycle_no_migration, + "rotation_migrates_context": _scenario_rotation_migrates_context, + "swap_identical_gpus": _scenario_swap_identical_gpus, +} + + +def _checkpoint_child_main(argv: list[str]) -> None: + if len(argv) < 2: + raise SystemExit("checkpoint child mode required") + + if argv[1] == "target": + if len(argv) != 3: + raise SystemExit("target mode requires a device index") + _target_main(int(argv[2])) + return + + if argv[1] == "scenario": + if len(argv) != 3: + raise SystemExit("scenario mode requires a scenario name") + try: + scenario = _SCENARIOS[argv[2]] + except KeyError as exc: + raise SystemExit(f"unknown checkpoint scenario: {argv[2]}") from exc + scenario() + return + + raise SystemExit(f"unknown checkpoint child mode: {argv[1]}") + + +# -- Input validation (no GPU / driver needed) ----------------------------- + + +class TestInputValidation: + @pytest.mark.parametrize( + ("args", "error_type", "match"), + [ + (("abc",), TypeError, "pid must be an int"), + ((True,), TypeError, "pid must be an int"), + ((0,), ValueError, "pid must be a positive int"), + ((-1,), ValueError, "pid must be a positive int"), + ], + ) + def test_process_rejects_invalid_pid(self, args, error_type, match): + with pytest.raises(error_type, match=match): + checkpoint.Process(*args) + + def test_public_symbols(self): + assert checkpoint.__all__ == ["Process"] + assert not hasattr(checkpoint, "ProcessStateT") + + def test_pid_is_read_only(self): + proc = checkpoint.Process(1) + assert proc.pid == 1 + with pytest.raises(AttributeError): + proc.pid = 2 + + +# -- Lifecycle (single GPU, real driver) ----------------------------------- + + +@needs_checkpoint +class TestCheckpointLifecycle: + def test_initial_state_is_running(self): + _run_checkpoint_scenario_or_skip("initial_state_is_running") + + def test_restore_thread_id_is_positive(self): + _run_checkpoint_scenario_or_skip("restore_thread_id_is_positive") + + def test_lock_unlock(self): + _run_checkpoint_scenario_or_skip("lock_unlock") + + def test_lock_default_timeout(self): + """lock() with the default timeout_ms=0 (no timeout).""" + _run_checkpoint_scenario_or_skip("lock_default_timeout") + + def test_lock_with_timeout(self): + _run_checkpoint_scenario_or_skip("lock_with_timeout") + + def test_full_cycle_no_migration(self): + """lock -> checkpoint -> restore -> unlock, verify state at each step.""" + _run_checkpoint_scenario_or_skip("full_cycle_no_migration") + + +# -- GPU migration (>= 2 same-chip GPUs, real driver) --------------------- + + +@needs_checkpoint +class TestCheckpointGpuMigration: + """GPU UUID remapping tests following the r580-migration-api.c pattern. + + These tests require at least two GPUs of the same chip type and a + driver that supports checkpoint migration. They skip when the + hardware cannot satisfy this (e.g. heterogeneous GPUs, or a driver + build where migration returns CUDA_ERROR_INVALID_VALUE — see + NVBug 5437334). + """ + + def test_rotation_migrates_context(self): + """Rotate context through all GPUs and back to the origin. + + Builds a rotation mapping (device i -> device (i+1) % N) for + every visible device and performs N rotations. After each step + the context device UUID is checked. After N steps the context + should be back on the original device. + """ + _run_checkpoint_scenario_or_skip("rotation_migrates_context", timeout=180) + + def test_swap_identical_gpus(self): + """Swap context between two GPUs of the same chip type. + + Sets the context on one of the pair members so that a successful + migration is observable (the context UUID changes). + """ + _run_checkpoint_scenario_or_skip("swap_identical_gpus", timeout=120) + + +if __name__ == "__main__": + _checkpoint_child_main(sys.argv) diff --git a/cuda_core/tests/test_typing_imports.py b/cuda_core/tests/test_typing_imports.py index c05e3ae3b37..2e207d55d8b 100644 --- a/cuda_core/tests/test_typing_imports.py +++ b/cuda_core/tests/test_typing_imports.py @@ -10,10 +10,12 @@ def test_typing_module_imports(): from cuda.core.typing import ( DevicePointerT, IsStreamT, + ProcessStateT, ) assert DevicePointerT is not None assert IsStreamT is not None + assert set(ProcessStateT.__args__) == {"running", "locked", "checkpointed", "failed"} def test_typing_matches_private_definitions(): @@ -23,7 +25,9 @@ def test_typing_matches_private_definitions(): from cuda.core.typing import ( DevicePointerT, IsStreamT, + ProcessStateT, ) assert DevicePointerT is _DevicePointerT assert IsStreamT is _IsStreamT + assert set(ProcessStateT.__args__) == {"running", "locked", "checkpointed", "failed"} From b1d011c4b669bc894664dba4b6a9bc83025bbb6c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 5 May 2026 11:35:58 -0700 Subject: [PATCH 068/244] Handle both Windows conda static-lib layouts. (#2015) Look for cudadevrt under both Library/lib/x64 and Library/lib so CUDA 12 conda environments resolve the real static library instead of falling through to a misleading CUDA_PATH error. Made-with: Cursor --- .../_static_libs/find_static_lib.py | 13 +++++---- cuda_pathfinder/tests/test_find_static_lib.py | 28 ++++++++++++++++++- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py index 22cea7daad8..804b1c04be7 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py @@ -28,7 +28,7 @@ class LocatedStaticLib: class _StaticLibInfo(TypedDict): filename: str ctk_rel_paths: tuple[str, ...] - conda_rel_path: str + conda_rel_paths: tuple[str, ...] site_packages_dirs: tuple[str, ...] @@ -36,7 +36,7 @@ class _StaticLibInfo(TypedDict): "cudadevrt": { "filename": "cudadevrt.lib" if IS_WINDOWS else "libcudadevrt.a", "ctk_rel_paths": (os.path.join("lib", "x64"),) if IS_WINDOWS else ("lib64", "lib"), - "conda_rel_path": os.path.join("lib", "x64") if IS_WINDOWS else "lib", + "conda_rel_paths": ((os.path.join("lib", "x64"), "lib") if IS_WINDOWS else ("lib",)), "site_packages_dirs": ( ("nvidia/cu13/lib/x64", "nvidia/cuda_runtime/lib/x64") if IS_WINDOWS @@ -66,7 +66,7 @@ def __init__(self, name: str) -> None: self.config: _StaticLibInfo = _SUPPORTED_STATIC_LIBS_INFO[name] self.filename: str = self.config["filename"] self.ctk_rel_paths: tuple[str, ...] = self.config["ctk_rel_paths"] - self.conda_rel_path: str = self.config["conda_rel_path"] + self.conda_rel_paths: tuple[str, ...] = self.config["conda_rel_paths"] self.site_packages_dirs: tuple[str, ...] = self.config["site_packages_dirs"] self.error_messages: list[str] = [] self.attachments: list[str] = [] @@ -86,9 +86,10 @@ def try_with_conda_prefix(self) -> str | None: return None anchor = os.path.join(conda_prefix, "Library") if IS_WINDOWS else conda_prefix - file_path = os.path.join(anchor, self.conda_rel_path, self.filename) - if os.path.isfile(file_path): - return file_path + for rel_path in self.conda_rel_paths: + file_path = os.path.join(anchor, rel_path, self.filename) + if os.path.isfile(file_path): + return file_path return None def try_with_cuda_home(self) -> str | None: diff --git a/cuda_pathfinder/tests/test_find_static_lib.py b/cuda_pathfinder/tests/test_find_static_lib.py index 2b30aa12011..e5560dcabbf 100644 --- a/cuda_pathfinder/tests/test_find_static_lib.py +++ b/cuda_pathfinder/tests/test_find_static_lib.py @@ -78,7 +78,7 @@ def test_locate_static_lib(info_summary_append, libname): @pytest.mark.usefixtures("clear_find_static_lib_cache") def test_locate_static_lib_search_order(monkeypatch, tmp_path): filename = CUDADEVRT_INFO["filename"] - conda_rel_path = CUDADEVRT_INFO["conda_rel_path"] + conda_rel_path = CUDADEVRT_INFO["conda_rel_paths"][0] site_pkg_rel = CUDADEVRT_INFO["site_packages_dirs"][0] site_packages_lib_dir = tmp_path / "site-packages" / Path(site_pkg_rel.replace("/", os.sep)) @@ -117,6 +117,32 @@ def test_locate_static_lib_search_order(monkeypatch, tmp_path): assert located_lib.found_via == "CUDA_PATH" +@pytest.mark.usefixtures("clear_find_static_lib_cache") +def test_locate_static_lib_conda_rel_path_fallback(monkeypatch, tmp_path): + filename = CUDADEVRT_INFO["filename"] + conda_rel_paths = CUDADEVRT_INFO["conda_rel_paths"] + if len(conda_rel_paths) == 1: + monkeypatch.setitem(CUDADEVRT_INFO, "conda_rel_paths", ("missing-first", conda_rel_paths[0])) + conda_rel_paths = CUDADEVRT_INFO["conda_rel_paths"] + + conda_prefix = tmp_path / "conda-prefix" + conda_lib_dir = _conda_anchor(conda_prefix) / Path(conda_rel_paths[1]) + conda_path = _make_static_lib_file(conda_lib_dir, filename) + + monkeypatch.setattr( + find_static_lib_module, + "find_sub_dirs_all_sitepackages", + lambda _sub_dir: [], + ) + monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) + monkeypatch.delenv("CUDA_HOME", raising=False) + monkeypatch.delenv("CUDA_PATH", raising=False) + + located_lib = locate_static_lib("cudadevrt") + assert located_lib.abs_path == conda_path + assert located_lib.found_via == "conda" + + @pytest.mark.usefixtures("clear_find_static_lib_cache") def test_find_static_lib_not_found_error_includes_cuda_home_directory_listing(monkeypatch, tmp_path): filename = CUDADEVRT_INFO["filename"] From b280b9daec691951fe59fa76378da976442f944a Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Tue, 5 May 2026 12:26:30 -0700 Subject: [PATCH 069/244] cuda.core: rename HostCallbackNode.callback_fn to callback (#2024) Drops the redundant `_fn` suffix on the lone property of `HostCallbackNode`, completing the agreed-upon item from the v1.0.0 naming audit (#1945). Surface is tiny: one Cython property, its docstring entry, and one test assertion key. Co-authored-by: Cursor --- cuda_core/cuda/core/graph/_subclasses.pyx | 4 ++-- cuda_core/docs/source/release/1.0.0-notes.rst | 4 ++++ cuda_core/tests/graph/test_graph_definition.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cuda_core/cuda/core/graph/_subclasses.pyx b/cuda_core/cuda/core/graph/_subclasses.pyx index 25b648bacef..69505d83353 100644 --- a/cuda_core/cuda/core/graph/_subclasses.pyx +++ b/cuda_core/cuda/core/graph/_subclasses.pyx @@ -573,7 +573,7 @@ cdef class HostCallbackNode(GraphNode): Properties ---------- - callback_fn : callable or None + callback : callable or None The Python callable (None for ctypes function pointer callbacks). """ @@ -613,7 +613,7 @@ cdef class HostCallbackNode(GraphNode): f" cfunc=0x{self._fn:x}>") @property - def callback_fn(self): + def callback(self): """The Python callable, or None for ctypes function pointer callbacks.""" return self._callable diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst index 35a9eb7304e..d3820876b14 100644 --- a/cuda_core/docs/source/release/1.0.0-notes.rst +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -90,6 +90,10 @@ Breaking changes - New: ``kernel.attributes.num_regs`` and ``kernel.attributes[some_dev].num_regs`` +- Renamed :attr:`graph.HostCallbackNode.callback_fn` to + :attr:`graph.HostCallbackNode.callback` to drop the redundant ``_fn`` suffix + (`#1945 `__). + - Unified the conditional graph API on :class:`~graph.GraphCondition` and consistent verbs (`#1945 `__): diff --git a/cuda_core/tests/graph/test_graph_definition.py b/cuda_core/tests/graph/test_graph_definition.py index e85645e0305..5c7696d26bb 100644 --- a/cuda_core/tests/graph/test_graph_definition.py +++ b/cuda_core/tests/graph/test_graph_definition.py @@ -386,7 +386,7 @@ def my_callback(): node = g.callback(my_callback) return node, { - "callback_fn": lambda v: v is my_callback, + "callback": lambda v: v is my_callback, } From 39c085c01f00760d70eaed978333f8fb8a4789b8 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Tue, 5 May 2026 12:30:37 -0700 Subject: [PATCH 070/244] Add green context support (#1976) * Implement green context v1 API * Refine green context split compatibility * Encode green context handle dependencies * Simplify green context view handles * Simplify green context descriptor handling * Expand green context test coverage with proper pytest patterns Restructure tests into fixtures + classes with full resource cleanup: - Fixtures: sm_resource, wq_resource, green_ctx (with CUDAError skip), green_ctx_active (with try/finally restore), fill_kernel - _use_green_ctx context manager for safe push/pop in all tests - TestSMResourceQuery: properties, arch constraints per CC - TestSMResourceSplit: single/two-group splits, discovery, alignment, dry-run vs real parity - TestGreenContextKernelLaunch: compile + launch + verify in green ctx, two independent green contexts, SM + workqueue combined All set_current calls are paired with restore in finally blocks to prevent context stack leaks on test failure. Co-Authored-By: Claude Opus 4.6 (1M context) * Lower green context handling to Cython and simplify Context - Convert ContextOptions and SMResourceOptions/WorkqueueResourceOptions to cdef dataclasses for check_or_create_options compatibility. - Cache SM metadata in typed cdef fields; fall back to arch-based granularity on CUDA 12.x where CUdevSmResource lacks minSmPartitionSize/smCoscheduledAlignment. - Simplify Context to hold only ContextHandle (remove _h_green_ctx and _is_green fields). Green ctx association lives in ContextBox; is_green queries get_context_green_ctx() on demand. - ContextOptions.resources accepts Sequence only (no bare resource). Co-Authored-By: Claude Opus 4.6 (1M context) * Add explicit green context model: ctx.create_stream and ctx.resources Switch from the push model (dev.set_current + dev.create_stream) to the explicit model (ctx.create_stream + ctx.resources) as the primary way to use green contexts. Context.create_stream(options): - Only supported on green contexts (raises on primary contexts). - Delegates to Stream._init, which calls create_stream_handle in C++. - C++ create_stream_handle auto-dispatches: checks get_context_green_ctx and calls cuGreenCtxStreamCreate for green contexts, or cuStreamCreateWithPriority for primary. Single function, no duplication. Context.resources: - Returns a DeviceResources namespace querying this context's resources (cuCtxGetDevResource / cuGreenCtxGetDevResource), not the full device. dev.set_current(green_ctx) still works but is not the recommended path. Tests rewritten to use the explicit model throughout. Push-model set_current kept as regression tests with _use_green_ctx helper. Co-Authored-By: Claude Opus 4.6 (1M context) * Harden green context stream creation and resource queries - Let the driver validate the nonblocking flag for green context streams: cuGreenCtxStreamCreate rejects CU_STREAM_DEFAULT. On failure, check if the context is green + nonblocking is False and raise a clear ValueError. - cuCtxGetStreamPriorityRange failure (CUDA_ERROR_INVALID_CONTEXT) now raises: "No current CUDA context. Call dev.set_current() before creating streams." - C++ create_stream_handle returns CUDA_ERROR_NOT_SUPPORTED if the context is green but cuGreenCtxStreamCreate is unavailable (CUDA < 12.5), instead of falling through to cuStreamCreateWithPriority. - ctx.resources.workqueue now dispatches to cuGreenCtxGetDevResource for green contexts, matching the SM query path. Co-Authored-By: Claude Opus 4.6 (1M context) * Add Stream.resources property Stream.resources delegates to DeviceResources._init_from_ctx via the stream's tracked context handle, returning the same resource view as ctx.resources for the stream's parent context. Co-Authored-By: Claude Opus 4.6 (1M context) * Polish green context API: docs, error handling, simplification - dev.create_context raises ValueError (not NotImplementedError) when options or resources are missing. - Cache version checks (_check_green_ctx_support, _check_workqueue_support) at module level; raise ValueError instead of NotImplementedError. - Simplify _device_resources.pyx: merge _as_uint and _count_to_sm_count into _to_sm_count; inline unsigned int casts for coscheduled params. - Add green context classes to api.rst (Context, ContextOptions, DeviceResources, SMResource, SMResourceOptions, WorkqueueResource, WorkqueueResourceOptions). - Update all docstrings to NumPy style with Attributes/Parameters/Returns sections matching the existing codebase convention. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review comments: consolidate context handles, GIL ordering, std::vector Review comment 1: Consolidate create_context_handle_from_green_ctx with create_context_handle_ref by adding a private overload that takes an optional GreenCtxHandle. The green ctx path now delegates to it after calling cuCtxFromGreenCtx, ensuring registry lookup and deduplication. Review comments 2-4: Move GILReleaseGuard to the first line in create_green_ctx_handle and create_context_handle_from_green_ctx for consistency with the rest of the file. Review comment 6: Keep is_green check inline in _context.pyx using get_context_green_ctx (cannot add a C++ is_green function across separate .so boundaries without linker issues). Review comment 8: Replace malloc/free with std::vector in Device.create_context for automatic cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) * Fix stream registry corruption for owner-backed handles Owner-backed stream handles (from create_stream_handle_with_owner) are no longer registered in the stream_registry. Multiple Python owners can wrap the same CUstream independently, each stacking its own Py_INCREF/Py_DECREF without competing for a single registry slot. The registry lookup at the top is preserved to reuse existing cuda-core-owned handles that carry context metadata. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_core/cuda/core/__init__.py | 8 + cuda_core/cuda/core/_context.pxd | 7 +- cuda_core/cuda/core/_context.pyx | 98 ++- cuda_core/cuda/core/_cpp/REGISTRY_DESIGN.md | 3 +- cuda_core/cuda/core/_cpp/resource_handles.cpp | 178 ++++- cuda_core/cuda/core/_cpp/resource_handles.hpp | 37 + cuda_core/cuda/core/_device.pyx | 80 ++- cuda_core/cuda/core/_device_resources.pxd | 51 ++ cuda_core/cuda/core/_device_resources.pyx | 663 ++++++++++++++++++ cuda_core/cuda/core/_resource_handles.pxd | 11 + cuda_core/cuda/core/_resource_handles.pyx | 30 + cuda_core/cuda/core/_stream.pyx | 55 +- cuda_core/cuda/core/typing.py | 2 + cuda_core/docs/source/api.rst | 6 + cuda_core/docs/source/api_private.rst | 2 + cuda_core/tests/test_green_context.py | 465 ++++++++++++ 16 files changed, 1670 insertions(+), 26 deletions(-) create mode 100644 cuda_core/cuda/core/_device_resources.pxd create mode 100644 cuda_core/cuda/core/_device_resources.pyx create mode 100644 cuda_core/tests/test_green_context.py diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index 73f9a166675..d4042aefcfb 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -29,7 +29,15 @@ def _import_versioned_module(): from cuda.core import checkpoint, system, utils +from cuda.core._context import Context, ContextOptions from cuda.core._device import Device +from cuda.core._device_resources import ( + DeviceResources, + SMResource, + SMResourceOptions, + WorkqueueResource, + WorkqueueResourceOptions, +) from cuda.core._event import Event, EventOptions from cuda.core._graphics import GraphicsResource from cuda.core._launch_config import LaunchConfig diff --git a/cuda_core/cuda/core/_context.pxd b/cuda_core/cuda/core/_context.pxd index 9e1a460f50f..92fa5700a06 100644 --- a/cuda_core/cuda/core/_context.pxd +++ b/cuda_core/cuda/core/_context.pxd @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -from cuda.core._resource_handles cimport ContextHandle +from cuda.core._resource_handles cimport ContextHandle, GreenCtxHandle cdef class Context: """Cython declaration for Context class. @@ -18,3 +18,8 @@ cdef class Context: @staticmethod cdef Context _from_handle(type cls, ContextHandle h_context, int device_id) + + @staticmethod + cdef Context _from_green_ctx(type cls, GreenCtxHandle h_green_ctx, int device_id) + + cpdef close(self) diff --git a/cuda_core/cuda/core/_context.pyx b/cuda_core/cuda/core/_context.pyx index b2b21465c81..225500c7093 100644 --- a/cuda_core/cuda/core/_context.pyx +++ b/cuda_core/cuda/core/_context.pyx @@ -2,18 +2,34 @@ # # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +from collections.abc import Sequence from dataclasses import dataclass +from cuda.bindings cimport cydriver +from cuda.core._device_resources cimport DeviceResources, SMResource, WorkqueueResource +from cuda.core._device_resources import SMResource, WorkqueueResource from cuda.core._resource_handles cimport ( ContextHandle, + GreenCtxHandle, + as_cu, + create_context_handle_from_green_ctx, + get_context_green_ctx, + get_last_error, as_intptr, as_py, ) +from cuda.core._stream import Stream, StreamOptions +from cuda.core._utils.cuda_utils cimport HANDLE_RETURN __all__ = ['Context', 'ContextOptions'] +DeviceResourcesT = Sequence[SMResource | WorkqueueResource] + + cdef class Context: """CUDA context wrapper. @@ -32,10 +48,21 @@ cdef class Context: ctx._device_id = device_id return ctx + @staticmethod + cdef Context _from_green_ctx(type cls, GreenCtxHandle h_green_ctx, int device_id): + """Create Context from an owning green context handle.""" + cdef ContextHandle h_context = create_context_handle_from_green_ctx(h_green_ctx) + if not h_context: + HANDLE_RETURN(get_last_error()) + raise RuntimeError("Failed to create CUDA context view from green context") + return Context._from_handle(cls, h_context, device_id) + @property def handle(self): """Return the underlying CUcontext handle.""" - if self._h_context.get() == NULL: + if not self._h_context: + return None + if as_cu(self._h_context) == NULL: return None return as_py(self._h_context) @@ -43,6 +70,66 @@ cdef class Context: def _handle(self): return self.handle + @property + def is_green(self) -> bool: + """True if this context was created from device resources.""" + if not self._h_context: + return False + return get_context_green_ctx(self._h_context).get() != NULL + + @property + def resources(self) -> DeviceResources: + """Query the hardware resources provisioned for this context. + + For green contexts, returns the resources this context was created + with (SM partition, workqueue config). For primary contexts, returns + the full device resources. + + Raises :class:`RuntimeError` if the context has been closed. + """ + if not self._h_context: + raise RuntimeError("Cannot query resources on a closed context") + return DeviceResources._init_from_ctx(self._h_context, self._device_id) + + def create_stream(self, options: StreamOptions | None = None): + """Create a new stream bound to this green context. + + This method is only available on green contexts. For primary + contexts, use :meth:`Device.create_stream` instead. + + Parameters + ---------- + options : :obj:`~_stream.StreamOptions`, optional + Customizable dataclass for stream creation options. + + Returns + ------- + :obj:`~_stream.Stream` + Newly created stream object. + """ + if not self._h_context: + raise RuntimeError("Cannot create a stream on a closed context") + if not self.is_green: + raise RuntimeError( + "Context.create_stream() is only supported on green contexts. " + "Use Device.create_stream() for primary contexts." + ) + + return Stream._init(options=options, device_id=self._device_id, ctx=self) + + cpdef close(self): + """Release this context wrapper's underlying CUDA handles.""" + cdef cydriver.CUcontext current_ctx + if self._h_context and as_cu(self._h_context) != NULL: + with nogil: + HANDLE_RETURN(cydriver.cuCtxGetCurrent(¤t_ctx)) + if current_ctx == as_cu(self._h_context): + raise RuntimeError( + "Cannot close a CUDA context while it is current. " + "Restore a previous context before closing this context." + ) + self._h_context.reset() + def __eq__(self, other): if not isinstance(other, Context): return NotImplemented @@ -57,9 +144,12 @@ cdef class Context: @dataclass -class ContextOptions: +cdef class ContextOptions: """Options for context creation. - Currently unused, reserved for future use. + Attributes + ---------- + resources : :obj:`~cuda.core.typing.DeviceResourcesT` + Device resources used to create a green context. """ - pass # TODO + resources: DeviceResourcesT diff --git a/cuda_core/cuda/core/_cpp/REGISTRY_DESIGN.md b/cuda_core/cuda/core/_cpp/REGISTRY_DESIGN.md index cbfc609686b..089f98acd93 100644 --- a/cuda_core/cuda/core/_cpp/REGISTRY_DESIGN.md +++ b/cuda_core/cuda/core/_cpp/REGISTRY_DESIGN.md @@ -29,7 +29,8 @@ carries timing/IPC flags, `KernelBox` carries the library dependency). Without this level, a round-tripped handle would produce a new Box with default metadata, losing information that was set at creation. -Instances: `event_registry`, `kernel_registry`, `graph_node_registry`. +Instances: `context_registry`, `stream_registry`, `event_registry`, +`kernel_registry`, `graph_node_registry`. ## Level 2: Resource Handle -> Python Object (Cython) diff --git a/cuda_core/cuda/core/_cpp/resource_handles.cpp b/cuda_core/cuda/core/_cpp/resource_handles.cpp index 5eb4716b981..ff0e0db5066 100644 --- a/cuda_core/cuda/core/_cpp/resource_handles.cpp +++ b/cuda_core/cuda/core/_cpp/resource_handles.cpp @@ -29,6 +29,12 @@ namespace cuda_core { decltype(&cuDevicePrimaryCtxRetain) p_cuDevicePrimaryCtxRetain = nullptr; decltype(&cuDevicePrimaryCtxRelease) p_cuDevicePrimaryCtxRelease = nullptr; decltype(&cuCtxGetCurrent) p_cuCtxGetCurrent = nullptr; +decltype(&cuGreenCtxCreate) p_cuGreenCtxCreate = nullptr; +decltype(&cuGreenCtxDestroy) p_cuGreenCtxDestroy = nullptr; +decltype(&cuCtxFromGreenCtx) p_cuCtxFromGreenCtx = nullptr; +decltype(&cuDevResourceGenerateDesc) p_cuDevResourceGenerateDesc = nullptr; + +decltype(&cuGreenCtxStreamCreate) p_cuGreenCtxStreamCreate = nullptr; decltype(&cuStreamCreateWithPriority) p_cuStreamCreateWithPriority = nullptr; decltype(&cuStreamDestroy) p_cuStreamDestroy = nullptr; @@ -223,12 +229,108 @@ void clear_last_error() noexcept { namespace { struct ContextBox { CUcontext resource; + GreenCtxHandle h_green_ctx; +}; + +struct GreenCtxBox { + CUgreenCtx resource; }; + +static const ContextBox* get_box(const ContextHandle& h) noexcept { + const CUcontext* p = h.get(); + return reinterpret_cast( + reinterpret_cast(p) - offsetof(ContextBox, resource) + ); +} + +// See REGISTRY_DESIGN.md (Level 1: Driver Handle -> Resource Handle) +static HandleRegistry context_registry; + +// Create a context handle reference, with optional green context as source. +ContextHandle create_context_handle_ref(CUcontext ctx, GreenCtxHandle h_green_ctx) { + if (!ctx) { + return {}; + } + if (auto h = context_registry.lookup(ctx)) { + return h; + } + auto box = std::shared_ptr( + new ContextBox{ctx, std::move(h_green_ctx)}, + [](const ContextBox* b) { + context_registry.unregister_handle(b->resource); + delete b; + } + ); + ContextHandle h(box, &box->resource); + context_registry.register_handle(ctx, h); + return h; +} } // namespace ContextHandle create_context_handle_ref(CUcontext ctx) { - auto box = std::make_shared(ContextBox{ctx}); - return ContextHandle(box, &box->resource); + return create_context_handle_ref(ctx, {}); +} + +ContextHandle create_context_handle_from_green_ctx(const GreenCtxHandle& h_green_ctx) { + GILReleaseGuard gil; + if (!h_green_ctx) { + return {}; + } + if (!p_cuCtxFromGreenCtx) { + err = CUDA_ERROR_NOT_SUPPORTED; + return {}; + } + + CUcontext ctx = nullptr; + if (CUDA_SUCCESS != (err = p_cuCtxFromGreenCtx(&ctx, as_cu(h_green_ctx)))) { + return {}; + } + + return create_context_handle_ref(ctx, h_green_ctx); +} + +GreenCtxHandle get_context_green_ctx(const ContextHandle& h) noexcept { + if (!h) { + return {}; + } + return get_box(h)->h_green_ctx; +} + +GreenCtxHandle create_green_ctx_handle(CUdevResource* resources, unsigned int nbResources, + CUdevice dev, unsigned int flags) { + GILReleaseGuard gil; + if (!p_cuDevResourceGenerateDesc || !p_cuGreenCtxCreate || !p_cuGreenCtxDestroy) { + err = CUDA_ERROR_NOT_SUPPORTED; + return {}; + } + + CUdevResourceDesc desc = nullptr; + if (CUDA_SUCCESS != (err = p_cuDevResourceGenerateDesc(&desc, resources, nbResources))) { + return {}; + } + + CUgreenCtx green_ctx = nullptr; + if (CUDA_SUCCESS != (err = p_cuGreenCtxCreate(&green_ctx, desc, dev, flags))) { + return {}; + } + + auto box = std::shared_ptr( + new GreenCtxBox{green_ctx}, + [](const GreenCtxBox* b) { + GILReleaseGuard gil; + p_cuGreenCtxDestroy(b->resource); + delete b; + } + ); + return GreenCtxHandle(box, &box->resource); +} + +GreenCtxHandle create_green_ctx_handle_ref(CUgreenCtx green_ctx) { + if (!green_ctx) { + return {}; + } + auto box = std::make_shared(GreenCtxBox{green_ctx}); + return GreenCtxHandle(box, &box->resource); } // Thread-local cache of primary contexts indexed by device ID @@ -250,14 +352,16 @@ ContextHandle get_primary_context(int device_id) { } auto box = std::shared_ptr( - new ContextBox{ctx}, + new ContextBox{ctx, {}}, [device_id](const ContextBox* b) { + context_registry.unregister_handle(b->resource); GILReleaseGuard gil; p_cuDevicePrimaryCtxRelease(device_id); delete b; } ); auto h = ContextHandle(box, &box->resource); + context_registry.register_handle(ctx, h); // Update cache if (static_cast(device_id) >= primary_context_cache.size()) { @@ -286,33 +390,78 @@ ContextHandle get_current_context() { namespace { struct StreamBox { CUstream resource; + ContextHandle h_context; }; + +static const StreamBox* get_box(const StreamHandle& h) noexcept { + const CUstream* p = h.get(); + return reinterpret_cast( + reinterpret_cast(p) - offsetof(StreamBox, resource) + ); +} + +// See REGISTRY_DESIGN.md (Level 1: Driver Handle -> Resource Handle) +static HandleRegistry stream_registry; } // namespace StreamHandle create_stream_handle(const ContextHandle& h_ctx, unsigned int flags, int priority) { GILReleaseGuard gil; CUstream stream; - if (CUDA_SUCCESS != (err = p_cuStreamCreateWithPriority(&stream, flags, priority))) { - return {}; + + // Dispatch: green context uses cuGreenCtxStreamCreate, primary uses cuStreamCreateWithPriority + GreenCtxHandle h_green = get_context_green_ctx(h_ctx); + if (h_green) { + if (!p_cuGreenCtxStreamCreate) { + err = CUDA_ERROR_NOT_SUPPORTED; + return {}; + } + if (CUDA_SUCCESS != (err = p_cuGreenCtxStreamCreate(&stream, as_cu(h_green), flags, priority))) { + return {}; + } + } else { + if (CUDA_SUCCESS != (err = p_cuStreamCreateWithPriority(&stream, flags, priority))) { + return {}; + } } auto box = std::shared_ptr( - new StreamBox{stream}, - [h_ctx](const StreamBox* b) { + new StreamBox{stream, h_ctx}, + [](const StreamBox* b) { + stream_registry.unregister_handle(b->resource); GILReleaseGuard gil; p_cuStreamDestroy(b->resource); delete b; } ); - return StreamHandle(box, &box->resource); + StreamHandle h(box, &box->resource); + stream_registry.register_handle(stream, h); + return h; } StreamHandle create_stream_handle_ref(CUstream stream) { - auto box = std::make_shared(StreamBox{stream}); - return StreamHandle(box, &box->resource); + if (auto h = stream_registry.lookup(stream)) { + return h; + } + auto box = std::shared_ptr( + new StreamBox{stream, {}}, + [](const StreamBox* b) { + stream_registry.unregister_handle(b->resource); + delete b; + } + ); + StreamHandle h(box, &box->resource); + stream_registry.register_handle(stream, h); + return h; } StreamHandle create_stream_handle_with_owner(CUstream stream, PyObject* owner) { + if (auto h = stream_registry.lookup(stream)) { + // Reuse handles that already carry structural context metadata, e.g. + // cuda-core-owned streams. + if (get_box(h)->h_context) { + return h; + } + } if (!owner) { return create_stream_handle_ref(stream); } @@ -323,8 +472,11 @@ StreamHandle create_stream_handle_with_owner(CUstream stream, PyObject* owner) { return create_stream_handle_ref(stream); } Py_INCREF(owner); + // Owner-backed handles are NOT registered in the stream registry to avoid + // corruption when multiple owners wrap the same CUstream (each stacks its + // own Py_INCREF/Py_DECREF independently). auto box = std::shared_ptr( - new StreamBox{stream}, + new StreamBox{stream, {}}, [owner](const StreamBox* b) { GILAcquireGuard gil; if (gil.acquired()) { @@ -336,6 +488,10 @@ StreamHandle create_stream_handle_with_owner(CUstream stream, PyObject* owner) { return StreamHandle(box, &box->resource); } +ContextHandle get_stream_context(const StreamHandle& h) noexcept { + return h ? get_box(h)->h_context : ContextHandle{}; +} + StreamHandle get_legacy_stream() { static StreamHandle handle = create_stream_handle_ref(CU_STREAM_LEGACY); return handle; diff --git a/cuda_core/cuda/core/_cpp/resource_handles.hpp b/cuda_core/cuda/core/_cpp/resource_handles.hpp index 2e6ebb6271c..3c195f1f1ad 100644 --- a/cuda_core/cuda/core/_cpp/resource_handles.hpp +++ b/cuda_core/cuda/core/_cpp/resource_handles.hpp @@ -59,6 +59,12 @@ void clear_last_error() noexcept; extern decltype(&cuDevicePrimaryCtxRetain) p_cuDevicePrimaryCtxRetain; extern decltype(&cuDevicePrimaryCtxRelease) p_cuDevicePrimaryCtxRelease; extern decltype(&cuCtxGetCurrent) p_cuCtxGetCurrent; +extern decltype(&cuGreenCtxCreate) p_cuGreenCtxCreate; +extern decltype(&cuGreenCtxDestroy) p_cuGreenCtxDestroy; +extern decltype(&cuCtxFromGreenCtx) p_cuCtxFromGreenCtx; +extern decltype(&cuDevResourceGenerateDesc) p_cuDevResourceGenerateDesc; + +extern decltype(&cuGreenCtxStreamCreate) p_cuGreenCtxStreamCreate; extern decltype(&cuStreamCreateWithPriority) p_cuStreamCreateWithPriority; extern decltype(&cuStreamDestroy) p_cuStreamDestroy; @@ -142,6 +148,7 @@ extern NvJitLinkDestroyFn p_nvJitLinkDestroy; // ============================================================================ using ContextHandle = std::shared_ptr; +using GreenCtxHandle = std::shared_ptr; using StreamHandle = std::shared_ptr; using EventHandle = std::shared_ptr; using MemoryPoolHandle = std::shared_ptr; @@ -164,6 +171,21 @@ using FileDescriptorHandle = std::shared_ptr; // Function to create a non-owning context handle (references existing context). ContextHandle create_context_handle_ref(CUcontext ctx); +// Create a context handle for the CUcontext view of the provided green context. +// The returned ContextHandle keeps the green context alive, but the CUcontext +// view is non-owning and is not destroyed independently. +ContextHandle create_context_handle_from_green_ctx(const GreenCtxHandle& h_green_ctx); + +// Return the green context dependency associated with a ContextHandle, if any. +GreenCtxHandle get_context_green_ctx(const ContextHandle& h) noexcept; + +// Create an owning green context handle from a list of device resources. +GreenCtxHandle create_green_ctx_handle(CUdevResource* resources, unsigned int nbResources, + CUdevice dev, unsigned int flags); + +// Create a non-owning green context handle. +GreenCtxHandle create_green_ctx_handle_ref(CUgreenCtx ctx); + // Get handle to the primary context for a device (with thread-local caching) // Returns empty handle on error (caller must check) ContextHandle get_primary_context(int device_id); @@ -193,6 +215,9 @@ StreamHandle create_stream_handle_ref(CUstream stream); // The owner is responsible for keeping the stream's context alive. StreamHandle create_stream_handle_with_owner(CUstream stream, PyObject* owner); +// Return the context dependency associated with a stream handle, if any. +ContextHandle get_stream_context(const StreamHandle& h) noexcept; + // Get non-owning handle to the legacy default stream (CU_STREAM_LEGACY) // Note: Legacy stream has no specific context dependency. StreamHandle get_legacy_stream(); @@ -501,6 +526,10 @@ inline CUcontext as_cu(const ContextHandle& h) noexcept { return h ? *h : nullptr; } +inline CUgreenCtx as_cu(const GreenCtxHandle& h) noexcept { + return h ? *h : nullptr; +} + inline CUstream as_cu(const StreamHandle& h) noexcept { return h ? *h : nullptr; } @@ -559,6 +588,10 @@ inline std::intptr_t as_intptr(const ContextHandle& h) noexcept { return reinterpret_cast(as_cu(h)); } +inline std::intptr_t as_intptr(const GreenCtxHandle& h) noexcept { + return reinterpret_cast(as_cu(h)); +} + inline std::intptr_t as_intptr(const StreamHandle& h) noexcept { return reinterpret_cast(as_cu(h)); } @@ -649,6 +682,10 @@ inline PyObject* as_py(const ContextHandle& h) noexcept { return detail::make_py("cuda.bindings.driver", "CUcontext", as_intptr(h)); } +inline PyObject* as_py(const GreenCtxHandle& h) noexcept { + return detail::make_py("cuda.bindings.driver", "CUgreenCtx", as_intptr(h)); +} + inline PyObject* as_py(const StreamHandle& h) noexcept { return detail::make_py("cuda.bindings.driver", "CUstream", as_intptr(h)); } diff --git a/cuda_core/cuda/core/_device.pyx b/cuda_core/cuda/core/_device.pyx index c0d7f09ee44..c35633c97af 100644 --- a/cuda_core/cuda/core/_device.pyx +++ b/cuda_core/cuda/core/_device.pyx @@ -7,19 +7,24 @@ from __future__ import annotations cimport cpython from cuda.bindings cimport cydriver -from cuda.core._utils.cuda_utils cimport HANDLE_RETURN +from cuda.core._utils.cuda_utils cimport check_or_create_options, HANDLE_RETURN +from libcpp.vector cimport vector import threading from cuda.core._context cimport Context from cuda.core._context import ContextOptions +from cuda.core._device_resources cimport DeviceResources, SMResource, WorkqueueResource from cuda.core._event cimport Event as cyEvent from cuda.core._event import Event, EventOptions from cuda.core._memory._buffer cimport Buffer, MemoryResource from cuda.core._resource_handles cimport ( ContextHandle, + GreenCtxHandle, create_context_handle_ref, + create_green_ctx_handle, get_primary_context, + get_last_error, as_cu, ) @@ -954,7 +959,16 @@ class Device: Default value of `None` return the currently used device. """ - __slots__ = ("_device_id", "_memory_resource", "_has_inited", "_properties", "_uuid", "_context", "__weakref__") + __slots__ = ( + "_device_id", + "_memory_resource", + "_has_inited", + "_properties", + "_resources", + "_uuid", + "_context", + "__weakref__", + ) def __new__(cls, device_id: Device | int | None = None): if isinstance(device_id, Device): @@ -1100,6 +1114,13 @@ class Device: return self._properties + @property + def resources(self) -> DeviceResources: + """Return the hardware resource query namespace for this device.""" + if self._resources is None: + self._resources = DeviceResources._init(self._device_id) + return self._resources + @property def compute_capability(self) -> ComputeCapability: """Return a named tuple with 2 fields: major and minor.""" @@ -1219,6 +1240,7 @@ class Device: """ cdef ContextHandle h_context cdef cydriver.CUcontext prev_ctx, curr_ctx + cdef Context prev_owned = None if ctx is not None: # TODO: revisit once Context is cythonized @@ -1228,6 +1250,8 @@ class Device: "the provided context was created on the device with" f" id={ctx._device_id}, which is different from the target id={self._device_id}" ) + if self._has_inited and self._context is not None: + prev_owned = self._context # prev_ctx is the previous context curr_ctx = as_cu(ctx._h_context) prev_ctx = NULL @@ -1237,6 +1261,8 @@ class Device: self._has_inited = True self._context = ctx # Store owning context reference if prev_ctx != NULL: + if prev_owned is not None and as_cu(prev_owned._h_context) == prev_ctx: + return prev_owned return Context._from_handle(Context, create_context_handle_ref(prev_ctx), self._device_id) else: # use primary ctx @@ -1266,7 +1292,54 @@ class Device: Newly created context object. """ - raise NotImplementedError("WIP: https://github.com/NVIDIA/cuda-python/issues/189") + cdef int i + cdef object resources + cdef object res + cdef SMResource sm_res + cdef WorkqueueResource wq_res + cdef GreenCtxHandle h_green + + if options is None: + raise ValueError( + "options with device resources must be provided to create a green context" + ) + + options = check_or_create_options(ContextOptions, options, "Context options") + if options.resources is None: + raise ValueError( + "ContextOptions.resources must be provided to create a green context" + ) + + resources = tuple(options.resources) + if len(resources) == 0: + raise ValueError("ContextOptions.resources must not be empty") + + cdef vector[cydriver.CUdevResource] c_resources + c_resources.resize(len(resources)) + + for i, res in enumerate(resources): + if isinstance(res, SMResource): + sm_res = res + if not sm_res._is_usable: + raise ValueError("dry-run SMResource objects cannot be used to create a context") + c_resources[i] = sm_res._resource + elif isinstance(res, WorkqueueResource): + wq_res = res + c_resources[i] = wq_res._wq_config_resource + else: + raise TypeError(f"Unsupported context resource type: {type(res)}") + + h_green = create_green_ctx_handle( + c_resources.data(), + (c_resources.size()), + (self._device_id), + (cydriver.CUgreenCtxCreate_flags.CU_GREEN_CTX_DEFAULT_STREAM), + ) + if h_green.get() == NULL: + HANDLE_RETURN(get_last_error()) + raise RuntimeError("Failed to create CUDA green context") + + return Context._from_green_ctx(Context, h_green, self._device_id) def create_stream(self, obj: IsStreamT | None = None, options: StreamOptions | None = None) -> Stream: """Create a :obj:`~_stream.Stream` object. @@ -1429,6 +1502,7 @@ cdef inline list Device_ensure_tls_devices(cls): device._memory_resource = None device._has_inited = False device._properties = None + device._resources = None device._uuid = None device._context = None devices.append(device) diff --git a/cuda_core/cuda/core/_device_resources.pxd b/cuda_core/cuda/core/_device_resources.pxd new file mode 100644 index 00000000000..d618c24cf10 --- /dev/null +++ b/cuda_core/cuda/core/_device_resources.pxd @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from cuda.bindings cimport cydriver +from cuda.core._resource_handles cimport ContextHandle, GreenCtxHandle + + +cdef class SMResource: + cdef: + cydriver.CUdevResource _resource + unsigned int _sm_count + unsigned int _min_partition_size + unsigned int _coscheduled_alignment + unsigned int _flags + bint _is_usable + object __weakref__ + + @staticmethod + cdef SMResource _from_dev_resource(cydriver.CUdevResource res, int device_id) + + @staticmethod + cdef SMResource _from_split_resource(cydriver.CUdevResource res, SMResource parent, bint is_usable) + + +cdef class WorkqueueResource: + cdef: + cydriver.CUdevResource _wq_config_resource + cydriver.CUdevResource _wq_resource + object __weakref__ + + @staticmethod + cdef WorkqueueResource _from_dev_resources( + cydriver.CUdevResource wq_config, + cydriver.CUdevResource wq, + ) + + +cdef class DeviceResources: + cdef: + int _device_id + ContextHandle _h_context # NULL for device-level queries + object __weakref__ + + @staticmethod + cdef DeviceResources _init(int device_id) + + @staticmethod + cdef DeviceResources _init_from_ctx(ContextHandle h_context, int device_id) + + cdef inline int _query_sm(self, cydriver.CUdevResource* res) except?-1 nogil diff --git a/cuda_core/cuda/core/_device_resources.pyx b/cuda_core/cuda/core/_device_resources.pyx new file mode 100644 index 00000000000..e4851a73a41 --- /dev/null +++ b/cuda_core/cuda/core/_device_resources.pyx @@ -0,0 +1,663 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from collections.abc import Sequence as SequenceABC +from dataclasses import dataclass + +from libc.stdint cimport intptr_t +from libc.stdlib cimport free, malloc +from libc.string cimport memset + +from cuda.bindings cimport cydriver +from cuda.core._resource_handles cimport ContextHandle, GreenCtxHandle, as_cu, get_context_green_ctx +from cuda.core._utils.cuda_utils cimport check_or_create_options, HANDLE_RETURN +from cuda.core._utils.cuda_utils import is_sequence +from cuda.core._utils.version cimport cy_binding_version, cy_driver_version + + +__all__ = [ + "DeviceResources", + "SMResource", + "SMResourceOptions", + "WorkqueueResource", + "WorkqueueResourceOptions", +] + + +# Module-level cached version checks (trinary: 0=unchecked, 1=supported, -1=unsupported) +cdef int _green_ctx_checked = 0 +cdef int _workqueue_checked = 0 +cdef str _green_ctx_err_msg = "" +cdef str _workqueue_err_msg = "" + + +cdef inline int _check_green_ctx_support() except?-1: + global _green_ctx_checked, _green_ctx_err_msg + if _green_ctx_checked == 1: + return 0 + if _green_ctx_checked == -1: + raise RuntimeError(_green_ctx_err_msg) + cdef tuple drv = cy_driver_version() + cdef tuple bind = cy_binding_version() + if drv < (12, 4, 0): + _green_ctx_err_msg = ( + "Green context support requires CUDA driver 12.4 or newer " + f"(current driver: {'.'.join(map(str, drv))})" + ) + _green_ctx_checked = -1 + raise RuntimeError(_green_ctx_err_msg) + if bind < (12, 4, 0): + _green_ctx_err_msg = ( + "Green context support requires cuda.bindings 12.4 or newer " + f"(current bindings: {'.'.join(map(str, bind))})" + ) + _green_ctx_checked = -1 + raise RuntimeError(_green_ctx_err_msg) + _green_ctx_checked = 1 + return 0 + + +cdef inline int _check_workqueue_support() except?-1: + global _workqueue_checked, _workqueue_err_msg + if _workqueue_checked == 1: + return 0 + if _workqueue_checked == -1: + raise RuntimeError(_workqueue_err_msg) + cdef tuple drv = cy_driver_version() + cdef tuple bind = cy_binding_version() + if drv < (13, 1, 0): + _workqueue_err_msg = ( + "WorkqueueResource requires CUDA driver 13.1 or newer " + f"(current driver: {'.'.join(map(str, drv))})" + ) + _workqueue_checked = -1 + raise RuntimeError(_workqueue_err_msg) + if bind < (13, 1, 0): + _workqueue_err_msg = ( + "WorkqueueResource requires cuda.bindings 13.1 or newer " + f"(current bindings: {'.'.join(map(str, bind))})" + ) + _workqueue_checked = -1 + raise RuntimeError(_workqueue_err_msg) + _workqueue_checked = 1 + return 0 + + +@dataclass +cdef class SMResourceOptions: + """Customizable :obj:`SMResource.split` options. + + Each field accepts a scalar (for a single group) or a ``Sequence`` + (for multiple groups). ``count`` drives the number of groups; other + ``Sequence`` fields must match its length. + + Attributes + ---------- + count : int or Sequence[int], optional + Requested SM count per group. ``None`` means discovery mode + (auto-detect). (Default to ``None``) + coscheduled_sm_count : int or Sequence[int], optional + Minimum number of SMs guaranteed to be co-scheduled in each + group. (Default to ``None``) + preferred_coscheduled_sm_count : int or Sequence[int], optional + Preferred co-scheduled SM count; the driver tries to satisfy + this but may fall back to ``coscheduled_sm_count``. + (Default to ``None``) + """ + + count: int | SequenceABC | None = None + coscheduled_sm_count: int | SequenceABC | None = None + preferred_coscheduled_sm_count: int | SequenceABC | None = None + + +@dataclass +cdef class WorkqueueResourceOptions: + """Customizable :obj:`WorkqueueResource.configure` options. + + Attributes + ---------- + sharing_scope : str, optional + Workqueue sharing scope. Accepted values: ``"device_ctx"`` + or ``"green_ctx_balanced"``. (Default to ``None``) + """ + + sharing_scope: str | None = None + + +cdef inline int _validate_split_field_length( + object value, str field_name, int n_groups, bint count_is_scalar +) except?-1: + if count_is_scalar: + if is_sequence(value): + raise ValueError( + f"{field_name} is a Sequence but count is scalar; " + "count must be a Sequence to specify multiple groups" + ) + elif is_sequence(value) and len(value) != n_groups: + raise ValueError( + f"{field_name} has length {len(value)}, expected {n_groups} " + "(must match count)" + ) + return 0 + + +cdef inline int _resolve_group_count(SMResourceOptions options) except?-1: + cdef object count = options.count + cdef int n_groups + cdef bint count_is_scalar + + if count is None or isinstance(count, int): + n_groups = 1 + count_is_scalar = True + elif is_sequence(count): + n_groups = len(count) + if n_groups == 0: + raise ValueError("count sequence must not be empty") + count_is_scalar = False + else: + raise TypeError(f"count must be int, Sequence, or None, got {type(count)}") + + _validate_split_field_length( + options.coscheduled_sm_count, + "coscheduled_sm_count", + n_groups, + count_is_scalar, + ) + _validate_split_field_length( + options.preferred_coscheduled_sm_count, + "preferred_coscheduled_sm_count", + n_groups, + count_is_scalar, + ) + return n_groups + + +cdef inline object _broadcast_field(object value, int n_groups): + if is_sequence(value): + return list(value) + return [value] * n_groups + + +cdef inline unsigned int _to_sm_count(object value) except? 0: + """Convert a count value to unsigned int. None maps to 0 (discovery).""" + if value is None: + return 0 + if value < 0: + raise ValueError(f"count must be non-negative, got {value}") + return (value) + + +cdef int _structured_split_checked = 0 + +cdef inline bint _can_use_structured_sm_split(): + """Check if cuDevSmResourceSplit (13.1+) is available. Cached.""" + global _structured_split_checked + if _structured_split_checked != 0: + return _structured_split_checked == 1 + IF CUDA_CORE_BUILD_MAJOR >= 13: + if cy_driver_version() >= (13, 1, 0) and cy_binding_version() >= (13, 1, 0): + _structured_split_checked = 1 + return True + _structured_split_checked = -1 + return False + + +cdef object _resolve_split_by_count_request(SMResourceOptions options): + cdef int n_groups = _resolve_group_count(options) + cdef list counts = _broadcast_field(options.count, n_groups) + cdef object first = counts[0] + cdef object value + cdef unsigned int min_count + + if options.coscheduled_sm_count is not None: + raise RuntimeError( + "SMResourceOptions.coscheduled_sm_count requires the CUDA 13.1 " + "structured SM split API" + ) + if options.preferred_coscheduled_sm_count is not None: + raise RuntimeError( + "SMResourceOptions.preferred_coscheduled_sm_count requires the " + "CUDA 13.1 structured SM split API" + ) + + for value in counts[1:]: + if value != first: + raise RuntimeError( + "CUDA 12 SM splitting only supports homogeneous count values; " + "use CUDA 13.1 or newer for per-group counts" + ) + + min_count = _to_sm_count(first) + return n_groups, min_count + + +IF CUDA_CORE_BUILD_MAJOR >= 13: + cdef inline int _fill_group_params( + cydriver.CU_DEV_SM_RESOURCE_GROUP_PARAMS* params, + int n_groups, + SMResourceOptions options, + ) except?-1: + cdef list counts = _broadcast_field(options.count, n_groups) + cdef list coscheduled = _broadcast_field(options.coscheduled_sm_count, n_groups) + cdef list preferred = _broadcast_field(options.preferred_coscheduled_sm_count, n_groups) + cdef int i + + for i in range(n_groups): + memset(¶ms[i], 0, sizeof(cydriver.CU_DEV_SM_RESOURCE_GROUP_PARAMS)) + params[i].smCount = _to_sm_count(counts[i]) + if coscheduled[i] is not None: + params[i].coscheduledSmCount = (coscheduled[i]) + if preferred[i] is not None: + params[i].preferredCoscheduledSmCount = (preferred[i]) + params[i].flags = 0 + return 0 + + + cdef object _split_with_general_api(SMResource sm, SMResourceOptions options, bint dry_run): + cdef int n_groups = _resolve_group_count(options) + cdef cydriver.CUdevResource* result = NULL + cdef cydriver.CUdevResource remaining + cdef cydriver.CUdevResource synth + cdef cydriver.CU_DEV_SM_RESOURCE_GROUP_PARAMS* params = NULL + cdef list groups = [] + cdef int i + + params = malloc( + n_groups * sizeof(cydriver.CU_DEV_SM_RESOURCE_GROUP_PARAMS) + ) + if params == NULL: + raise MemoryError() + + try: + _fill_group_params(params, n_groups, options) + + if not dry_run: + result = malloc( + n_groups * sizeof(cydriver.CUdevResource) + ) + if result == NULL: + raise MemoryError() + + memset(&remaining, 0, sizeof(cydriver.CUdevResource)) + with nogil: + HANDLE_RETURN(cydriver.cuDevSmResourceSplit( + result, + (n_groups), + &sm._resource, + &remaining, + 0, + params, + )) + + if result != NULL: + for i in range(n_groups): + groups.append(SMResource._from_split_resource(result[i], sm, True)) + return groups, SMResource._from_split_resource(remaining, sm, True) + + for i in range(n_groups): + memset(&synth, 0, sizeof(cydriver.CUdevResource)) + synth.type = cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_SM + synth.sm.smCount = params[i].smCount + groups.append(SMResource._from_split_resource(synth, sm, False)) + return groups, SMResource._from_split_resource(remaining, sm, False) + finally: + if params != NULL: + free(params) + if result != NULL: + free(result) +ELSE: + cdef object _split_with_general_api(SMResource sm, SMResourceOptions options, bint dry_run): + raise RuntimeError( + "SMResource.split() requires cuda.core to be built with CUDA 13.x bindings" + ) + + +cdef object _split_with_count_api(SMResource sm, SMResourceOptions options, bint dry_run): + cdef object request = _resolve_split_by_count_request(options) + cdef unsigned int nb_groups = (request[0]) + cdef unsigned int min_count = (request[1]) + cdef unsigned int actual_groups = nb_groups + cdef cydriver.CUdevResource* result = NULL + cdef cydriver.CUdevResource remaining + cdef list groups = [] + cdef int i + + result = malloc(nb_groups * sizeof(cydriver.CUdevResource)) + if result == NULL: + raise MemoryError() + + try: + memset(&remaining, 0, sizeof(cydriver.CUdevResource)) + with nogil: + HANDLE_RETURN(cydriver.cuDevSmResourceSplitByCount( + result, + &actual_groups, + &sm._resource, + &remaining, + 0, + min_count, + )) + + for i in range(actual_groups): + if dry_run: + groups.append(SMResource._from_split_resource(result[i], sm, False)) + else: + groups.append(SMResource._from_split_resource(result[i], sm, True)) + if dry_run: + return groups, SMResource._from_split_resource(remaining, sm, False) + return groups, SMResource._from_split_resource(remaining, sm, True) + finally: + free(result) + + +cdef inline unsigned int _sm_resource_granularity(int device_id) except? 0: + cdef int major + + with nogil: + HANDLE_RETURN(cydriver.cuDeviceGetAttribute( + &major, + cydriver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, + (device_id), + )) + if major >= 9: + return 8 + return 2 + + +cdef inline unsigned int _fallback_if_zero(unsigned int value, unsigned int fallback) noexcept: + if value != 0: + return value + return fallback + + +cdef class SMResource: + """Represent an SM (streaming multiprocessor) resource partition. + + Instances are returned by :obj:`DeviceResources.sm` or + :meth:`SMResource.split` and cannot be instantiated directly. + """ + + def __init__(self, *args, **kwargs): + raise RuntimeError( + "SMResource cannot be instantiated directly. " + "Use dev.resources.sm or SMResource.split()." + ) + + @staticmethod + cdef SMResource _from_dev_resource(cydriver.CUdevResource res, int device_id): + cdef SMResource self = SMResource.__new__(SMResource) + self._resource = res + self._sm_count = res.sm.smCount + IF CUDA_CORE_BUILD_MAJOR >= 13: + self._min_partition_size = res.sm.minSmPartitionSize + self._coscheduled_alignment = res.sm.smCoscheduledAlignment + self._flags = res.sm.flags + ELSE: + self._min_partition_size = _sm_resource_granularity(device_id) + self._coscheduled_alignment = self._min_partition_size + self._flags = 0 + self._is_usable = True + return self + + @staticmethod + cdef SMResource _from_split_resource(cydriver.CUdevResource res, SMResource parent, bint is_usable): + cdef SMResource self = SMResource.__new__(SMResource) + self._resource = res + self._sm_count = res.sm.smCount + IF CUDA_CORE_BUILD_MAJOR >= 13: + self._min_partition_size = _fallback_if_zero( + res.sm.minSmPartitionSize, + parent._min_partition_size, + ) + self._coscheduled_alignment = _fallback_if_zero( + res.sm.smCoscheduledAlignment, + parent._coscheduled_alignment, + ) + self._flags = res.sm.flags + ELSE: + self._min_partition_size = parent._min_partition_size + self._coscheduled_alignment = parent._coscheduled_alignment + self._flags = parent._flags + self._is_usable = is_usable + return self + + @property + def handle(self) -> int: + """Return the address of the underlying ``CUdevResource`` struct.""" + return (&self._resource) + + @property + def sm_count(self) -> int: + """Total SMs available in this resource.""" + return self._sm_count + + @property + def min_partition_size(self) -> int: + """Minimum SM count required to create a partition.""" + return self._min_partition_size + + @property + def coscheduled_alignment(self) -> int: + """Number of SMs guaranteed to be co-scheduled.""" + return self._coscheduled_alignment + + @property + def flags(self) -> int: + """Raw flags from the underlying SM resource.""" + return self._flags + + def split(self, options not None, *, bint dry_run=False): + """Split this SM resource into groups and a remainder. + + Parameters + ---------- + options : :obj:`SMResourceOptions` + Split configuration (count, co-scheduling constraints). + dry_run : bool, optional + If ``True``, return filled-in metadata without creating + usable resource objects. (Default to ``False``) + + Returns + ------- + tuple[list[:obj:`SMResource`], :obj:`SMResource`] + ``(groups, remainder)`` where each group holds a disjoint + SM partition and *remainder* holds any unassigned SMs. + """ + cdef SMResourceOptions opts = check_or_create_options( + SMResourceOptions, options, "SM resource options" + ) + _resolve_group_count(opts) + _check_green_ctx_support() + if _can_use_structured_sm_split(): + return _split_with_general_api(self, opts, dry_run) + # SplitByCount requires the same 12.4+ as green ctx support (already checked above) + return _split_with_count_api(self, opts, dry_run) + + +cdef class WorkqueueResource: + """Represent a workqueue resource for a device or green context. + + Merges ``CU_DEV_RESOURCE_TYPE_WORKQUEUE_CONFIG`` and + ``CU_DEV_RESOURCE_TYPE_WORKQUEUE`` under one user-facing type. + Instances are returned by :obj:`DeviceResources.workqueue` and + cannot be instantiated directly. + """ + + def __init__(self, *args, **kwargs): + raise RuntimeError( + "WorkqueueResource cannot be instantiated directly. " + "Use dev.resources.workqueue." + ) + + @staticmethod + cdef WorkqueueResource _from_dev_resources( + cydriver.CUdevResource wq_config, + cydriver.CUdevResource wq, + ): + cdef WorkqueueResource self = WorkqueueResource.__new__(WorkqueueResource) + self._wq_config_resource = wq_config + self._wq_resource = wq + return self + + @property + def handle(self) -> int: + """Return the address of the underlying config ``CUdevResource`` struct.""" + return (&self._wq_config_resource) + + def configure(self, options not None): + """Configure the workqueue resource in place. + + Parameters + ---------- + options : :obj:`WorkqueueResourceOptions` + Configuration options (sharing scope, etc.). + """ + cdef WorkqueueResourceOptions opts = check_or_create_options( + WorkqueueResourceOptions, options, "Workqueue resource options" + ) + _check_green_ctx_support() + _check_workqueue_support() + if opts.sharing_scope is None: + return None + + IF CUDA_CORE_BUILD_MAJOR >= 13: + if opts.sharing_scope == "device_ctx": + self._wq_config_resource.wqConfig.sharingScope = ( + cydriver.CUdevWorkqueueConfigScope.CU_WORKQUEUE_SCOPE_DEVICE_CTX + ) + elif opts.sharing_scope == "green_ctx_balanced": + self._wq_config_resource.wqConfig.sharingScope = ( + cydriver.CUdevWorkqueueConfigScope.CU_WORKQUEUE_SCOPE_GREEN_CTX_BALANCED + ) + else: + raise ValueError( + f"Unknown sharing_scope: {opts.sharing_scope!r}. " + "Expected 'device_ctx' or 'green_ctx_balanced'." + ) + ELSE: + raise RuntimeError( + "WorkqueueResource requires cuda.core to be built with CUDA 13.x bindings" + ) + + +cdef class DeviceResources: + """Namespace for hardware resource queries. + + When obtained via :obj:`Device.resources`, queries return full device + resources. When obtained via :obj:`Context.resources` or + :obj:`Stream.resources`, queries return the resources provisioned for + that context. + + This class cannot be instantiated directly. + """ + + def __init__(self, *args, **kwargs): + raise RuntimeError( + "DeviceResources cannot be instantiated directly. " + "Use dev.resources or ctx.resources." + ) + + @staticmethod + cdef DeviceResources _init(int device_id): + cdef DeviceResources self = DeviceResources.__new__(DeviceResources) + self._device_id = device_id + # _h_context is default empty — queries use cuDeviceGetDevResource + return self + + @staticmethod + cdef DeviceResources _init_from_ctx(ContextHandle h_context, int device_id): + cdef DeviceResources self = DeviceResources.__new__(DeviceResources) + self._device_id = device_id + self._h_context = h_context + return self + + cdef inline int _query_sm(self, cydriver.CUdevResource* res) except?-1 nogil: + """Query SM resource from either device or context.""" + cdef GreenCtxHandle h_green + if self._h_context: + h_green = get_context_green_ctx(self._h_context) + if h_green: + HANDLE_RETURN(cydriver.cuGreenCtxGetDevResource( + as_cu(h_green), res, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_SM, + )) + else: + HANDLE_RETURN(cydriver.cuCtxGetDevResource( + as_cu(self._h_context), res, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_SM, + )) + else: + HANDLE_RETURN(cydriver.cuDeviceGetDevResource( + (self._device_id), res, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_SM, + )) + return 0 + + @property + def sm(self) -> SMResource: + """Return the :obj:`SMResource` for this device or context.""" + _check_green_ctx_support() + cdef cydriver.CUdevResource res + with nogil: + self._query_sm(&res) + return SMResource._from_dev_resource(res, self._device_id) + + @property + def workqueue(self) -> WorkqueueResource: + """Return the :obj:`WorkqueueResource` for this device or context.""" + _check_green_ctx_support() + _check_workqueue_support() + cdef cydriver.CUdevResource _wq_config + cdef cydriver.CUdevResource _wq + + IF CUDA_CORE_BUILD_MAJOR >= 13: + cdef GreenCtxHandle h_green + if self._h_context: + h_green = get_context_green_ctx(self._h_context) + if h_green: + # Green context query + with nogil: + HANDLE_RETURN(cydriver.cuGreenCtxGetDevResource( + as_cu(h_green), + &_wq_config, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_WORKQUEUE_CONFIG, + )) + HANDLE_RETURN(cydriver.cuGreenCtxGetDevResource( + as_cu(h_green), + &_wq, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_WORKQUEUE, + )) + else: + # Primary context query + with nogil: + HANDLE_RETURN(cydriver.cuCtxGetDevResource( + as_cu(self._h_context), + &_wq_config, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_WORKQUEUE_CONFIG, + )) + HANDLE_RETURN(cydriver.cuCtxGetDevResource( + as_cu(self._h_context), + &_wq, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_WORKQUEUE, + )) + else: + # Device-level query + with nogil: + HANDLE_RETURN(cydriver.cuDeviceGetDevResource( + (self._device_id), + &_wq_config, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_WORKQUEUE_CONFIG, + )) + HANDLE_RETURN(cydriver.cuDeviceGetDevResource( + (self._device_id), + &_wq, + cydriver.CUdevResourceType.CU_DEV_RESOURCE_TYPE_WORKQUEUE, + )) + return WorkqueueResource._from_dev_resources(_wq_config, _wq) + ELSE: + raise RuntimeError( + "WorkqueueResource requires cuda.core to be built with CUDA 13.x bindings" + ) diff --git a/cuda_core/cuda/core/_resource_handles.pxd b/cuda_core/cuda/core/_resource_handles.pxd index 0d7d20e574c..8d07c27dedb 100644 --- a/cuda_core/cuda/core/_resource_handles.pxd +++ b/cuda_core/cuda/core/_resource_handles.pxd @@ -20,6 +20,7 @@ from cuda.bindings cimport cynvjitlink cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": # Handle types ctypedef shared_ptr[const cydriver.CUcontext] ContextHandle + ctypedef shared_ptr[const cydriver.CUgreenCtx] GreenCtxHandle ctypedef shared_ptr[const cydriver.CUstream] StreamHandle ctypedef shared_ptr[const cydriver.CUevent] EventHandle ctypedef shared_ptr[const cydriver.CUmemoryPool] MemoryPoolHandle @@ -45,6 +46,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": # as_cu() - extract the raw CUDA handle (inline C++) cydriver.CUcontext as_cu(ContextHandle h) noexcept nogil + cydriver.CUgreenCtx as_cu(GreenCtxHandle h) noexcept nogil cydriver.CUstream as_cu(StreamHandle h) noexcept nogil cydriver.CUevent as_cu(EventHandle h) noexcept nogil cydriver.CUmemoryPool as_cu(MemoryPoolHandle h) noexcept nogil @@ -61,6 +63,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": # as_intptr() - extract handle as intptr_t for Python interop (inline C++) intptr_t as_intptr(ContextHandle h) noexcept nogil + intptr_t as_intptr(GreenCtxHandle h) noexcept nogil intptr_t as_intptr(StreamHandle h) noexcept nogil intptr_t as_intptr(EventHandle h) noexcept nogil intptr_t as_intptr(MemoryPoolHandle h) noexcept nogil @@ -78,6 +81,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": # as_py() - convert handle to Python wrapper object (inline C++; requires GIL) object as_py(ContextHandle h) + object as_py(GreenCtxHandle h) object as_py(StreamHandle h) object as_py(EventHandle h) object as_py(MemoryPoolHandle h) @@ -107,6 +111,12 @@ cdef void clear_last_error() noexcept nogil # Context handles cdef ContextHandle create_context_handle_ref(cydriver.CUcontext ctx) except+ nogil +cdef ContextHandle create_context_handle_from_green_ctx(const GreenCtxHandle& h_green_ctx) except+ nogil +cdef GreenCtxHandle get_context_green_ctx(const ContextHandle& h) noexcept nogil +cdef GreenCtxHandle create_green_ctx_handle( + cydriver.CUdevResource* resources, unsigned int nbResources, + cydriver.CUdevice dev, unsigned int flags) except+ nogil +cdef GreenCtxHandle create_green_ctx_handle_ref(cydriver.CUgreenCtx ctx) except+ nogil cdef ContextHandle get_primary_context(int device_id) except+ nogil cdef ContextHandle get_current_context() except+ nogil @@ -115,6 +125,7 @@ cdef StreamHandle create_stream_handle( const ContextHandle& h_ctx, unsigned int flags, int priority) except+ nogil cdef StreamHandle create_stream_handle_ref(cydriver.CUstream stream) except+ nogil cdef StreamHandle create_stream_handle_with_owner(cydriver.CUstream stream, object owner) except+ nogil +cdef ContextHandle get_stream_context(const StreamHandle& h) noexcept nogil cdef StreamHandle get_legacy_stream() except+ nogil cdef StreamHandle get_per_thread_stream() except+ nogil diff --git a/cuda_core/cuda/core/_resource_handles.pyx b/cuda_core/cuda/core/_resource_handles.pyx index d30993cc5e8..a1dc05464ac 100644 --- a/cuda_core/cuda/core/_resource_handles.pyx +++ b/cuda_core/cuda/core/_resource_handles.pyx @@ -20,6 +20,7 @@ from cuda.bindings cimport cynvjitlink from ._resource_handles cimport ( ContextHandle, + GreenCtxHandle, StreamHandle, EventHandle, MemoryPoolHandle, @@ -55,6 +56,15 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": # Context handles ContextHandle create_context_handle_ref "cuda_core::create_context_handle_ref" ( cydriver.CUcontext ctx) except+ nogil + ContextHandle create_context_handle_from_green_ctx "cuda_core::create_context_handle_from_green_ctx" ( + const GreenCtxHandle& h_green_ctx) except+ nogil + GreenCtxHandle get_context_green_ctx "cuda_core::get_context_green_ctx" ( + const ContextHandle& h) noexcept nogil + GreenCtxHandle create_green_ctx_handle "cuda_core::create_green_ctx_handle" ( + cydriver.CUdevResource* resources, unsigned int nbResources, + cydriver.CUdevice dev, unsigned int flags) except+ nogil + GreenCtxHandle create_green_ctx_handle_ref "cuda_core::create_green_ctx_handle_ref" ( + cydriver.CUgreenCtx ctx) except+ nogil ContextHandle get_primary_context "cuda_core::get_primary_context" ( int device_id) except+ nogil ContextHandle get_current_context "cuda_core::get_current_context" () except+ nogil @@ -66,6 +76,8 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": cydriver.CUstream stream) except+ nogil StreamHandle create_stream_handle_with_owner "cuda_core::create_stream_handle_with_owner" ( cydriver.CUstream stream, object owner) except+ nogil + ContextHandle get_stream_context "cuda_core::get_stream_context" ( + const StreamHandle& h) noexcept nogil StreamHandle get_legacy_stream "cuda_core::get_legacy_stream" () except+ nogil StreamHandle get_per_thread_stream "cuda_core::get_per_thread_stream" () except+ nogil @@ -223,6 +235,11 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core": void* p_cuDevicePrimaryCtxRetain "reinterpret_cast(cuda_core::p_cuDevicePrimaryCtxRetain)" void* p_cuDevicePrimaryCtxRelease "reinterpret_cast(cuda_core::p_cuDevicePrimaryCtxRelease)" void* p_cuCtxGetCurrent "reinterpret_cast(cuda_core::p_cuCtxGetCurrent)" + void* p_cuGreenCtxCreate "reinterpret_cast(cuda_core::p_cuGreenCtxCreate)" + void* p_cuGreenCtxDestroy "reinterpret_cast(cuda_core::p_cuGreenCtxDestroy)" + void* p_cuCtxFromGreenCtx "reinterpret_cast(cuda_core::p_cuCtxFromGreenCtx)" + void* p_cuDevResourceGenerateDesc "reinterpret_cast(cuda_core::p_cuDevResourceGenerateDesc)" + void* p_cuGreenCtxStreamCreate "reinterpret_cast(cuda_core::p_cuGreenCtxStreamCreate)" # Stream void* p_cuStreamCreateWithPriority "reinterpret_cast(cuda_core::p_cuStreamCreateWithPriority)" @@ -288,10 +305,23 @@ cdef void* _get_driver_fn(str name): capsule = cydriver.__pyx_capi__[name] return PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)) + +cdef void* _get_optional_driver_fn(str name): + try: + capsule = cydriver.__pyx_capi__[name] + except KeyError: + return NULL + return PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)) + # Context p_cuDevicePrimaryCtxRetain = _get_driver_fn("cuDevicePrimaryCtxRetain") p_cuDevicePrimaryCtxRelease = _get_driver_fn("cuDevicePrimaryCtxRelease") p_cuCtxGetCurrent = _get_driver_fn("cuCtxGetCurrent") +p_cuGreenCtxCreate = _get_optional_driver_fn("cuGreenCtxCreate") +p_cuGreenCtxDestroy = _get_optional_driver_fn("cuGreenCtxDestroy") +p_cuCtxFromGreenCtx = _get_optional_driver_fn("cuCtxFromGreenCtx") +p_cuDevResourceGenerateDesc = _get_optional_driver_fn("cuDevResourceGenerateDesc") +p_cuGreenCtxStreamCreate = _get_optional_driver_fn("cuGreenCtxStreamCreate") # Stream p_cuStreamCreateWithPriority = _get_driver_fn("cuStreamCreateWithPriority") diff --git a/cuda_core/cuda/core/_stream.pyx b/cuda_core/cuda/core/_stream.pyx index fdb617f0325..e2b477b8614 100644 --- a/cuda_core/cuda/core/_stream.pyx +++ b/cuda_core/cuda/core/_stream.pyx @@ -21,6 +21,7 @@ from dataclasses import dataclass from typing import Protocol from cuda.core._context cimport Context +from cuda.core._device_resources cimport DeviceResources from cuda.core._event import Event, EventOptions from cuda.core._resource_handles cimport ( ContextHandle, @@ -31,8 +32,10 @@ from cuda.core._resource_handles cimport ( create_stream_handle, create_stream_handle_with_owner, get_current_context, + get_last_error, get_legacy_stream, get_per_thread_stream, + get_stream_context, as_intptr, as_cu, as_py, @@ -96,7 +99,7 @@ cdef class Stream: """Create a Stream from an existing StreamHandle (cdef-only factory).""" cdef Stream s = cls.__new__(cls) s._h_stream = h_stream - # _h_context is default-initialized to empty ContextHandle by C++ + s._h_context = get_stream_context(h_stream) s._device_id = -1 # lazy init'd (invalid sentinel) s._nonblocking = -1 # lazy init'd s._priority = INT32_MIN # lazy init'd @@ -142,8 +145,15 @@ cdef class Stream: else cydriver.CUstream_flags.CU_STREAM_DEFAULT) # TODO: we might want to consider memoizing high/low per CUDA context and avoid this call cdef int high, low + cdef cydriver.CUresult res_code with nogil: - HANDLE_RETURN(cydriver.cuCtxGetStreamPriorityRange(&high, &low)) + res_code = cydriver.cuCtxGetStreamPriorityRange(&high, &low) + if res_code != cydriver.CUresult.CUDA_SUCCESS: + if res_code == cydriver.CUresult.CUDA_ERROR_INVALID_CONTEXT: + raise RuntimeError( + "No current CUDA context. Call dev.set_current() before creating streams." + ) + HANDLE_RETURN(res_code) cdef int prio if priority is not None: prio = priority @@ -152,10 +162,25 @@ cdef class Stream: else: prio = high - # C++ creates the stream and returns owning handle with context dependency + # C++ creates the stream and returns owning handle with context dependency. + # For green contexts, the C++ layer auto-dispatches to cuGreenCtxStreamCreate. h_stream = create_stream_handle(h_context, flags, prio) if not h_stream: - raise RuntimeError("Failed to create CUDA stream") + res_code = get_last_error() + if not nonblocking and res_code == cydriver.CUresult.CUDA_ERROR_INVALID_VALUE: + # cuGreenCtxStreamCreate rejects CU_STREAM_DEFAULT; + # no need to check is_green since primary streams don't fail this way + raise ValueError( + "Green context streams must be non-blocking. " + "Use StreamOptions(nonblocking=True) or omit the option (True is the default)." + ) + elif res_code == cydriver.CUresult.CUDA_ERROR_NOT_SUPPORTED: + raise RuntimeError( + "cuGreenCtxStreamCreate is not available. " + "Green context stream creation requires CUDA 12.5 or newer." + ) + else: + HANDLE_RETURN(res_code) self = Stream._from_handle(cls, h_stream) self._nonblocking = int(nonblocking) self._priority = prio @@ -329,6 +354,18 @@ cdef class Stream: Stream_ensure_ctx_device(self) return Context._from_handle(Context, self._h_context, self._device_id) + @property + def resources(self): + """Query the hardware resources provisioned for this stream's context. + + For streams created from a green context, returns the resources + that context was provisioned with. For streams on the primary + context, returns the full device resources. + """ + Stream_ensure_ctx(self) + Stream_ensure_ctx_device(self) + return DeviceResources._init_from_ctx(self._h_context, self._device_id) + @staticmethod def from_handle(handle: int) -> Stream: """Create a new :obj:`~_stream.Stream` object from a foreign stream handle. @@ -413,7 +450,11 @@ cdef inline int Stream_ensure_ctx(Stream self) except?-1 nogil: """Ensure the stream's context handle is populated.""" cdef cydriver.CUcontext ctx if not self._h_context: - HANDLE_RETURN(cydriver.cuStreamGetCtx(as_cu(self._h_stream), &ctx)) + self._h_context = get_stream_context(self._h_stream) + if self._h_context: + return 0 + HANDLE_RETURN(cydriver.cuStreamGetCtx(as_cu(self._h_stream), &ctx)) + if ctx != NULL: with gil: self._h_context = create_context_handle_ref(ctx) return 0 @@ -423,13 +464,15 @@ cdef inline int Stream_ensure_ctx_device(Stream self) except?-1: """Ensure the stream's context and device_id are populated.""" cdef cydriver.CUcontext ctx cdef cydriver.CUdevice target_dev + cdef ContextHandle current_context cdef bint switch_context if self._device_id < 0: with nogil: # Get device ID from context, switching context temporarily if needed Stream_ensure_ctx(self) - switch_context = (get_current_context() != self._h_context) + current_context = get_current_context() + switch_context = (as_cu(current_context) != as_cu(self._h_context)) if switch_context: HANDLE_RETURN(cydriver.cuCtxPushCurrent(as_cu(self._h_context))) HANDLE_RETURN(cydriver.cuCtxGetDevice(&target_dev)) diff --git a/cuda_core/cuda/core/typing.py b/cuda_core/cuda/core/typing.py index e95331d463f..3146e5d0bc3 100644 --- a/cuda_core/cuda/core/typing.py +++ b/cuda_core/cuda/core/typing.py @@ -6,6 +6,7 @@ from typing import Literal as _Literal +from cuda.core._context import DeviceResourcesT from cuda.core._memory._buffer import DevicePointerT from cuda.core._stream import IsStreamT @@ -13,6 +14,7 @@ __all__ = [ "DevicePointerT", + "DeviceResourcesT", "IsStreamT", "ProcessStateT", ] diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 7e8632b1893..5b2ee954d0c 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -26,12 +26,18 @@ Devices and execution Stream Event + Context + SMResource + WorkqueueResource :template: dataclass.rst StreamOptions EventOptions LaunchConfig + ContextOptions + SMResourceOptions + WorkqueueResourceOptions .. data:: LEGACY_DEFAULT_STREAM diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index b10898841cc..82cf9823219 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -17,6 +17,7 @@ CUDA runtime :toctree: generated/ typing.DevicePointerT + typing.DeviceResourcesT typing.ProcessStateT _memory._virtual_memory_resource.VirtualMemoryAllocationTypeT _memory._virtual_memory_resource.VirtualMemoryLocationTypeT @@ -31,6 +32,7 @@ CUDA runtime :template: autosummary/cyclass.rst _device.DeviceProperties + _device_resources.DeviceResources _memory._ipc.IPCAllocationHandle _memory._ipc.IPCBufferDescriptor diff --git a/cuda_core/tests/test_green_context.py b/cuda_core/tests/test_green_context.py new file mode 100644 index 00000000000..8eb32f7c1aa --- /dev/null +++ b/cuda_core/tests/test_green_context.py @@ -0,0 +1,465 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +import contextlib + +import numpy as np +import pytest + +from cuda.core import ( + ContextOptions, + DeviceResources, + LaunchConfig, + LegacyPinnedMemoryResource, + Program, + ProgramOptions, + SMResource, + SMResourceOptions, + WorkqueueResource, + WorkqueueResourceOptions, + launch, +) +from cuda.core._utils.cuda_utils import CUDAError + +# --------------------------------------------------------------------------- +# Kernel source +# --------------------------------------------------------------------------- + +_FILL_KERNEL = r""" +extern "C" __global__ void fill(int* out, int value, int n) { + int tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid < n) { + out[tid] = value; + } +} +""" + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def sm_resource(init_cuda): + """Query SM resources from the device, skip if unsupported.""" + try: + return init_cuda.resources.sm + except (RuntimeError, ValueError, CUDAError) as exc: + pytest.skip(str(exc)) + + +@pytest.fixture +def wq_resource(init_cuda): + """Query workqueue resources from the device, skip if unsupported.""" + try: + return init_cuda.resources.workqueue + except (RuntimeError, ValueError, CUDAError) as exc: + pytest.skip(str(exc)) + + +@pytest.fixture +def green_ctx(init_cuda, sm_resource): + """Create a single-group green context with proper teardown.""" + groups, _ = sm_resource.split(SMResourceOptions(count=None)) + try: + ctx = init_cuda.create_context(ContextOptions(resources=[groups[0]])) + except CUDAError as exc: + pytest.skip(str(exc)) + yield ctx + ctx.close() + + +@pytest.fixture +def fill_kernel(init_cuda): + """Compile the fill kernel for the current device.""" + dev = init_cuda + opts = ProgramOptions(std="c++17", arch=f"sm_{dev.arch}") + prog = Program(_FILL_KERNEL, code_type="c++", options=opts) + mod = prog.compile("cubin") + return mod.get_kernel("fill") + + +def _aligned_half(sm): + """Compute half the SM count, rounded down to min_partition_size alignment.""" + min_size = sm.min_partition_size + half = (sm.sm_count // 2 // min_size) * min_size + return half + + +@contextlib.contextmanager +def _use_green_ctx(dev, ctx): + """Context manager: set green ctx current, restore previous on exit.""" + prev = dev.set_current(ctx) + try: + yield + finally: + dev.set_current(prev) + + +# --------------------------------------------------------------------------- +# Construction / type tests +# --------------------------------------------------------------------------- + + +def test_not_user_constructible(): + with pytest.raises(RuntimeError): + DeviceResources() + with pytest.raises(RuntimeError): + SMResource() + with pytest.raises(RuntimeError): + WorkqueueResource() + + +def test_create_context_requires_resources(init_cuda): + with pytest.raises(ValueError, match="resources must be provided"): + init_cuda.create_context() + with pytest.raises(ValueError, match="resources must be provided"): + init_cuda.create_context(ContextOptions(resources=None)) + with pytest.raises(TypeError): + init_cuda.create_context(object()) + + +# --------------------------------------------------------------------------- +# SM resource query +# --------------------------------------------------------------------------- + + +class TestSMResourceQuery: + def test_properties(self, sm_resource): + assert sm_resource.handle != 0 + assert sm_resource.sm_count > 0 + assert sm_resource.min_partition_size > 0 + assert sm_resource.coscheduled_alignment > 0 + assert isinstance(sm_resource.flags, int) + + def test_no_memory_node_id_in_v1(self, sm_resource): + """memory_node_id is deferred to v1.1 (CUDA 13.4).""" + assert not hasattr(sm_resource, "memory_node_id") + + def test_arch_constraints_pre_hopper(self, init_cuda, sm_resource): + if init_cuda.compute_capability >= (9, 0): + pytest.skip("Test is for pre-Hopper architectures") + assert sm_resource.min_partition_size >= 2 + assert sm_resource.coscheduled_alignment >= 2 + + def test_arch_constraints_hopper_plus(self, init_cuda, sm_resource): + if init_cuda.compute_capability < (9, 0): + pytest.skip("Test is for Hopper+ architectures") + assert sm_resource.min_partition_size >= 8 + assert sm_resource.coscheduled_alignment >= 8 + + +# --------------------------------------------------------------------------- +# Workqueue resource +# --------------------------------------------------------------------------- + + +class TestWorkqueueResource: + def test_query(self, wq_resource): + assert wq_resource.handle != 0 + + def test_configure_none_is_noop(self, wq_resource): + assert wq_resource.configure(WorkqueueResourceOptions(sharing_scope=None)) is None + + def test_configure_valid_scope(self, wq_resource): + wq_resource.configure(WorkqueueResourceOptions(sharing_scope="green_ctx_balanced")) + + def test_configure_invalid_scope_raises(self, wq_resource): + with pytest.raises(ValueError, match="Unknown sharing_scope"): + wq_resource.configure(WorkqueueResourceOptions(sharing_scope="bogus")) + + +# --------------------------------------------------------------------------- +# SM resource split — validation +# --------------------------------------------------------------------------- + + +class TestSMResourceSplitValidation: + def test_scalar_count_with_sequence_field_raises(self, sm_resource): + count = sm_resource.min_partition_size + with pytest.raises(ValueError, match="count is scalar"): + sm_resource.split( + SMResourceOptions( + count=count, + coscheduled_sm_count=(count, count), + ) + ) + + def test_sequence_length_mismatch_raises(self, sm_resource): + count = sm_resource.min_partition_size + with pytest.raises(ValueError, match="expected 2"): + sm_resource.split( + SMResourceOptions( + count=(count, count), + coscheduled_sm_count=(count, count, count), + ) + ) + + def test_negative_count_raises(self, sm_resource): + with pytest.raises(ValueError, match="count must be non-negative"): + sm_resource.split(SMResourceOptions(count=-1)) + + def test_dry_run_cannot_create_context(self, init_cuda, sm_resource): + groups, _ = sm_resource.split(SMResourceOptions(count=None), dry_run=True) + assert len(groups) == 1 + with pytest.raises(ValueError, match="dry-run SMResource"): + init_cuda.create_context(ContextOptions(resources=[groups[0]])) + + +# --------------------------------------------------------------------------- +# SM resource split — functional +# --------------------------------------------------------------------------- + + +class TestSMResourceSplit: + def test_single_group_counts(self, sm_resource): + """Single-group split: group gets at least requested SMs.""" + requested = sm_resource.min_partition_size + groups, rem = sm_resource.split(SMResourceOptions(count=requested)) + + assert len(groups) == 1 + assert groups[0].sm_count >= requested + assert groups[0].sm_count + rem.sm_count <= sm_resource.sm_count + + def test_discovery_mode(self, sm_resource): + """count=None auto-detects a valid SM count.""" + groups, _ = sm_resource.split(SMResourceOptions(count=None)) + + assert len(groups) == 1 + assert groups[0].sm_count >= sm_resource.min_partition_size + + def test_discovery_respects_alignment(self, sm_resource): + groups, _ = sm_resource.split(SMResourceOptions(count=None)) + + if sm_resource.coscheduled_alignment > 0: + assert groups[0].sm_count % sm_resource.coscheduled_alignment == 0 + + def test_two_groups(self, sm_resource): + """Two-group split with explicit aligned counts.""" + half = _aligned_half(sm_resource) + if half < sm_resource.min_partition_size: + pytest.skip("Not enough SMs for a 2-group split") + + groups, rem = sm_resource.split(SMResourceOptions(count=(half, half))) + + assert len(groups) == 2 + assert groups[0].sm_count > 0 + assert groups[1].sm_count > 0 + total = groups[0].sm_count + groups[1].sm_count + rem.sm_count + assert total <= sm_resource.sm_count + + def test_two_groups_each_meets_request(self, sm_resource): + min_size = sm_resource.min_partition_size + half = _aligned_half(sm_resource) + if half < min_size: + pytest.skip("Not enough SMs for a 2-group split") + + groups, _ = sm_resource.split(SMResourceOptions(count=(min_size, min_size))) + + assert len(groups) == 2 + assert groups[0].sm_count >= min_size + assert groups[1].sm_count >= min_size + + def test_dry_run_matches_real(self, sm_resource): + """Dry-run reports the same SM counts as a real split.""" + opts = SMResourceOptions(count=None) + + dry_groups, _ = sm_resource.split(opts, dry_run=True) + real_groups, _ = sm_resource.split(opts, dry_run=False) + + assert len(dry_groups) == len(real_groups) + for dg, rg in zip(dry_groups, real_groups): + assert dg.sm_count == rg.sm_count + + +# --------------------------------------------------------------------------- +# Green context lifecycle +# --------------------------------------------------------------------------- + + +class TestGreenContextLifecycle: + def test_is_green(self, green_ctx): + assert green_ctx.is_green + assert green_ctx.handle is not None + + def test_create_stream_on_primary_raises(self, init_cuda): + """create_stream is only for green contexts.""" + # The init_cuda fixture sets the primary context + # Get the primary context via device internals + ctx = init_cuda._context + with pytest.raises(RuntimeError, match="only supported on green contexts"): + ctx.create_stream() + + def test_create_stream_blocking_raises(self, green_ctx): + """Green context streams must be non-blocking.""" + from cuda.core import StreamOptions + + with pytest.raises(ValueError, match="must be non-blocking"): + green_ctx.create_stream(StreamOptions(nonblocking=False)) + + def test_create_stream_explicit(self, green_ctx): + """Create a stream directly from the green context (no set_current).""" + stream = green_ctx.create_stream() + assert stream is not None + assert stream.context.is_green + assert stream.context == green_ctx + + def test_stream_and_event_track_green_context(self, green_ctx): + stream = green_ctx.create_stream() + event = stream.record() + assert stream.context.is_green + assert stream.context == green_ctx + assert event.context.is_green + assert event.context == green_ctx + stream.sync() + event.sync() + + def test_close_while_current_raises(self, init_cuda, green_ctx): + """close() on a current context raises — test via set_current.""" + dev = init_cuda + with _use_green_ctx(dev, green_ctx), pytest.raises(RuntimeError, match="while it is current"): + green_ctx.close() + + def test_set_current_swap_regression(self, init_cuda, green_ctx): + """set_current still works (backward compat) and preserves identity.""" + dev = init_cuda + with _use_green_ctx(dev, green_ctx): + pass # just verify push/pop works + # Swap again and check identity round-trip + prev = dev.set_current(green_ctx) + try: + assert prev is not None + finally: + restored = dev.set_current(prev) + assert restored is green_ctx + assert restored.is_green + + +# --------------------------------------------------------------------------- +# Context.resources +# --------------------------------------------------------------------------- + + +class TestContextResources: + def test_green_ctx_sm_resources(self, green_ctx, sm_resource): + """Green context's SM resources should be a subset of device SMs.""" + ctx_sm = green_ctx.resources.sm + assert ctx_sm.sm_count > 0 + assert ctx_sm.sm_count <= sm_resource.sm_count + + def test_green_ctx_resources_reflect_partition(self, init_cuda, sm_resource): + """Two green contexts should have disjoint SM partitions.""" + half = _aligned_half(sm_resource) + if half < sm_resource.min_partition_size: + pytest.skip("Not enough SMs for a 2-group split") + + groups, _ = sm_resource.split(SMResourceOptions(count=(half, half))) + + ctx_a = ctx_b = None + try: + ctx_a = init_cuda.create_context(ContextOptions(resources=[groups[0]])) + ctx_b = init_cuda.create_context(ContextOptions(resources=[groups[1]])) + + sm_a = ctx_a.resources.sm.sm_count + sm_b = ctx_b.resources.sm.sm_count + assert sm_a > 0 + assert sm_b > 0 + assert sm_a + sm_b <= sm_resource.sm_count + finally: + if ctx_b is not None: + ctx_b.close() + if ctx_a is not None: + ctx_a.close() + + def test_stream_resources_match_context(self, green_ctx, sm_resource): + """stream.resources should return the same as ctx.resources.""" + stream = green_ctx.create_stream() + + stream_sm = stream.resources.sm + ctx_sm = green_ctx.resources.sm + assert stream_sm.sm_count == ctx_sm.sm_count + assert stream_sm.sm_count > 0 + assert stream_sm.sm_count <= sm_resource.sm_count + + try: + stream_wq = stream.resources.workqueue + ctx_wq = green_ctx.resources.workqueue + assert stream_wq.handle != 0 + assert ctx_wq.handle != 0 + except (RuntimeError, ValueError, CUDAError): + pass # workqueue not available on this driver/build + + +# --------------------------------------------------------------------------- +# Kernel launch in green context (explicit model) +# --------------------------------------------------------------------------- + + +def _launch_fill_and_verify(dev, stream, kernel, n, value): + """Launch the fill kernel and verify results on host.""" + dev_buf = dev.allocate(n * np.dtype(np.int32).itemsize, stream=stream) + + config = LaunchConfig(grid=(n + 31) // 32, block=32) + launch(stream, config, kernel, dev_buf, np.int32(value), np.int32(n)) + + host_mr = LegacyPinnedMemoryResource() + host_buf = host_mr.allocate(n * np.dtype(np.int32).itemsize) + host_arr = np.from_dlpack(host_buf).view(np.int32) + host_arr[:] = 0 + + dev_buf.copy_to(host_buf, stream=stream) + stream.sync() + + np.testing.assert_array_equal(host_arr, np.full(n, value, dtype=np.int32)) + + +class TestGreenContextKernelLaunch: + def test_launch_and_verify(self, init_cuda, green_ctx, fill_kernel): + """Launch kernel via ctx.create_stream (explicit model, no set_current).""" + stream = green_ctx.create_stream() + _launch_fill_and_verify(init_cuda, stream, fill_kernel, n=64, value=42) + + def test_two_green_contexts_independent(self, init_cuda, sm_resource, fill_kernel): + """Two SM groups -> two green contexts -> two independent kernels.""" + dev = init_cuda + half = _aligned_half(sm_resource) + if half < sm_resource.min_partition_size: + pytest.skip("Not enough SMs for a 2-group split") + + groups, _ = sm_resource.split(SMResourceOptions(count=(half, half))) + assert len(groups) == 2 + + ctx_a = ctx_b = None + try: + ctx_a = dev.create_context(ContextOptions(resources=[groups[0]])) + ctx_b = dev.create_context(ContextOptions(resources=[groups[1]])) + + for ctx, value in [(ctx_a, 10), (ctx_b, 20)]: + stream = ctx.create_stream() + _launch_fill_and_verify(dev, stream, fill_kernel, n=64, value=value) + finally: + if ctx_b is not None: + ctx_b.close() + if ctx_a is not None: + ctx_a.close() + + def test_with_workqueue_resource(self, init_cuda, sm_resource, wq_resource, fill_kernel): + """Green context with SM + workqueue resources can launch a kernel.""" + dev = init_cuda + groups, _ = sm_resource.split(SMResourceOptions(count=None)) + + try: + ctx = dev.create_context(ContextOptions(resources=[groups[0], wq_resource])) + except CUDAError as exc: + pytest.skip(str(exc)) + + assert ctx.is_green + + try: + stream = ctx.create_stream() + _launch_fill_and_verify(dev, stream, fill_kernel, n=32, value=99) + finally: + ctx.close() From cec0efbb647527a363542c771e531d0d14208601 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Tue, 5 May 2026 16:23:44 -0400 Subject: [PATCH 071/244] Fix #1995: Use StrEnum for enum-like strings (#2016) * Fix #1995: Use StrEnum for enum-like strings * Address renamings from PR comments * Add release notes --- cuda_core/cuda/core/_context.pyx | 6 +- cuda_core/cuda/core/_device.pyx | 6 +- cuda_core/cuda/core/_launcher.pyx | 2 +- cuda_core/cuda/core/_linker.pyx | 13 +- cuda_core/cuda/core/_memory/_buffer.pyx | 19 +- .../core/_memory/_graph_memory_resource.pyx | 4 +- cuda_core/cuda/core/_memory/_legacy.py | 6 +- .../core/_memory/_managed_memory_resource.pyx | 12 +- cuda_core/cuda/core/_memory/_memory_pool.pyx | 4 +- .../core/_memory/_virtual_memory_resource.py | 70 ++++---- cuda_core/cuda/core/_module.pyx | 19 +- cuda_core/cuda/core/_program.pyx | 68 ++++--- cuda_core/cuda/core/_stream.pyx | 4 +- cuda_core/cuda/core/checkpoint.py | 8 +- .../cuda/core/graph/_graph_definition.pyx | 6 +- cuda_core/cuda/core/graph/_graph_node.pyx | 2 +- cuda_core/cuda/core/graph/_subclasses.pyx | 15 +- cuda_core/cuda/core/system/_device.pyx | 1 + cuda_core/cuda/core/system/_temperature.pxi | 1 + cuda_core/cuda/core/typing.py | 119 +++++++++++-- cuda_core/docs/source/api_private.rst | 27 +-- cuda_core/docs/source/interoperability.rst | 2 +- cuda_core/docs/source/release/1.0.0-notes.rst | 23 +++ .../tests/graph/test_graph_definition.py | 11 +- cuda_core/tests/helpers/misc.py | 2 +- cuda_core/tests/test_checkpoint.py | 2 +- cuda_core/tests/test_enum_coverage.py | 168 +++++++++++++----- cuda_core/tests/test_launcher.py | 5 +- cuda_core/tests/test_memory.py | 30 ++-- cuda_core/tests/test_program.py | 5 +- cuda_core/tests/test_typing_imports.py | 28 +-- 31 files changed, 454 insertions(+), 234 deletions(-) diff --git a/cuda_core/cuda/core/_context.pyx b/cuda_core/cuda/core/_context.pyx index 225500c7093..48dc2c09778 100644 --- a/cuda_core/cuda/core/_context.pyx +++ b/cuda_core/cuda/core/_context.pyx @@ -27,7 +27,7 @@ from cuda.core._utils.cuda_utils cimport HANDLE_RETURN __all__ = ['Context', 'ContextOptions'] -DeviceResourcesT = Sequence[SMResource | WorkqueueResource] +DeviceResourcesType = Sequence[SMResource | WorkqueueResource] cdef class Context: @@ -149,7 +149,7 @@ cdef class ContextOptions: Attributes ---------- - resources : :obj:`~cuda.core.typing.DeviceResourcesT` + resources : :obj:`~cuda.core.typing.DeviceResourcesType` Device resources used to create a green context. """ - resources: DeviceResourcesT + resources: DeviceResourcesType diff --git a/cuda_core/cuda/core/_device.pyx b/cuda_core/cuda/core/_device.pyx index c35633c97af..233ce6de791 100644 --- a/cuda_core/cuda/core/_device.pyx +++ b/cuda_core/cuda/core/_device.pyx @@ -28,7 +28,7 @@ from cuda.core._resource_handles cimport ( as_cu, ) -from cuda.core._stream import IsStreamT, Stream, StreamOptions +from cuda.core._stream import IsStreamType, Stream, StreamOptions from cuda.core._utils.clear_error_support import assert_type from cuda.core._utils.cuda_utils import ( ComputeCapability, @@ -1341,7 +1341,7 @@ class Device: return Context._from_green_ctx(Context, h_green, self._device_id) - def create_stream(self, obj: IsStreamT | None = None, options: StreamOptions | None = None) -> Stream: + def create_stream(self, obj: IsStreamType | None = None, options: StreamOptions | None = None) -> Stream: """Create a :obj:`~_stream.Stream` object. New stream objects can be created in two different ways: @@ -1358,7 +1358,7 @@ class Device: Parameters ---------- - obj : :obj:`~_stream.IsStreamT`, optional + obj : :obj:`~_stream.IsStreamType`, optional Any object supporting the ``__cuda_stream__`` protocol. options : :obj:`~_stream.StreamOptions`, optional Customizable dataclass for stream creation options. diff --git a/cuda_core/cuda/core/_launcher.pyx b/cuda_core/cuda/core/_launcher.pyx index 87d18f2b881..e6a07ad28e6 100644 --- a/cuda_core/cuda/core/_launcher.pyx +++ b/cuda_core/cuda/core/_launcher.pyx @@ -20,7 +20,7 @@ from cuda.core._stream import Stream from math import prod -def launch(stream: Stream | GraphBuilder | IsStreamT, config: LaunchConfig, kernel: Kernel, *kernel_args): +def launch(stream: Stream | GraphBuilder | IsStreamType, config: LaunchConfig, kernel: Kernel, *kernel_args): """Launches a :obj:`~_module.Kernel` object with launch-time configuration. diff --git a/cuda_core/cuda/core/_linker.pyx b/cuda_core/cuda/core/_linker.pyx index e89e780b34c..2a3d0b514c8 100644 --- a/cuda_core/cuda/core/_linker.pyx +++ b/cuda_core/cuda/core/_linker.pyx @@ -39,6 +39,7 @@ from cuda.core._utils.cuda_utils import ( driver, is_sequence, ) +from cuda.core.typing import CompilerBackendType ctypedef const char* const_char_ptr ctypedef void* void_ptr @@ -70,12 +71,12 @@ cdef class Linker: def __init__(self, *object_codes: ObjectCode, options: "LinkerOptions" = None): Linker_init(self, object_codes, options) - def link(self, target_type) -> ObjectCode: + def link(self, target_type: ObjectCodeFormatType | str) -> ObjectCode: """Link the provided object codes into a single output of the specified target type. Parameters ---------- - target_type : str + target_type : ObjectCodeFormatType | str The type of the target output. Must be either "cubin" or "ptx". Returns @@ -88,7 +89,7 @@ cdef class Linker: Ensure that input object codes were compiled with appropriate flags for linking (e.g., relocatable device code enabled). """ - return Linker_link(self, target_type) + return Linker_link(self, str(target_type)) def get_error_log(self) -> str: """Get the error log generated by the linker. @@ -168,9 +169,9 @@ cdef class Linker: return as_py(self._culink_handle) @property - def backend(self) -> str: - """Return this Linker instance's underlying backend.""" - return "nvJitLink" if self._use_nvjitlink else "driver" + def backend(self) -> CompilerBackendType: + """Return this Linker instance's underlying :class:`CompilerBackendType`.""" + return CompilerBackendType.NVJITLINK if self._use_nvjitlink else CompilerBackendType.DRIVER # ============================================================================= diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index a56657a3564..dd12ae005d4 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -22,6 +22,7 @@ from cuda.core._resource_handles cimport ( as_cu, set_deallocation_stream, ) +from cuda.core.typing import DevicePointerType from cuda.core._stream cimport Stream, Stream_accept from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value @@ -35,7 +36,6 @@ else: BufferProtocol = object from cuda.core._dlpack import classify_dl_device, make_py_capsule -from cuda.core._utils.cuda_utils import driver from cuda.core._device import Device @@ -65,11 +65,6 @@ register_mr_dealloc_callback(_mr_dealloc_callback) __all__ = ['Buffer', 'MemoryResource'] -DevicePointerT = driver.CUdeviceptr | int | None -""" -A type union of :obj:`~driver.CUdeviceptr`, `int` and `None` for hinting -:attr:`Buffer.handle`. -""" cdef class Buffer: """Represent a handle to allocated memory. @@ -97,7 +92,7 @@ cdef class Buffer: @classmethod def _init( - cls, ptr: DevicePointerT, size_t size, mr: MemoryResource | None = None, + cls, ptr: DevicePointerType, size_t size, mr: MemoryResource | None = None, ipc_descriptor: IPCBufferDescriptor | None = None, owner : object | None = None ): @@ -132,14 +127,14 @@ cdef class Buffer: @staticmethod def from_handle( - ptr: DevicePointerT, size_t size, mr: MemoryResource | None = None, + ptr: DevicePointerType, size_t size, mr: MemoryResource | None = None, owner: object | None = None, ) -> Buffer: """Create a new :class:`Buffer` object from a pointer. Parameters ---------- - ptr : :obj:`~_memory.DevicePointerT` + ptr : :obj:`~_memory.DevicePointerType` Allocated buffer handle object size : int Memory size of the buffer @@ -347,7 +342,7 @@ cdef class Buffer: return self._mem_attrs.device_id @property - def handle(self) -> DevicePointerT: + def handle(self) -> DevicePointerType: """Return the buffer handle object. .. caution:: @@ -515,12 +510,12 @@ cdef class MemoryResource: """ raise TypeError("MemoryResource.allocate must be implemented by subclasses.") - def deallocate(self, ptr: DevicePointerT, size_t size, stream: Stream | GraphBuilder | None = None): + def deallocate(self, ptr: DevicePointerType, size_t size, stream: Stream | GraphBuilder | None = None): """Deallocate a buffer previously allocated by this resource. Parameters ---------- - ptr : :obj:`~_memory.DevicePointerT` + ptr : :obj:`~_memory.DevicePointerType` The pointer or handle to the buffer to deallocate. size : int The size of the buffer to deallocate, in bytes. diff --git a/cuda_core/cuda/core/_memory/_graph_memory_resource.pyx b/cuda_core/cuda/core/_memory/_graph_memory_resource.pyx index e04f25f1581..2180276ed87 100644 --- a/cuda_core/cuda/core/_memory/_graph_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_graph_memory_resource.pyx @@ -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 @@ -111,7 +111,7 @@ cdef class cyGraphMemoryResource(MemoryResource): stream = Stream_accept(stream) if stream is not None else default_stream() return GMR_allocate(self, size, stream) - def deallocate(self, ptr: "DevicePointerT", size_t size, stream: Stream | GraphBuilder | None = None): + def deallocate(self, ptr: "DevicePointerType", size_t size, stream: Stream | GraphBuilder | None = None): """ Deallocate a buffer of the requested size. See documentation for :obj:`~_memory.MemoryResource`. """ diff --git a/cuda_core/cuda/core/_memory/_legacy.py b/cuda_core/cuda/core/_memory/_legacy.py index 24ce88487ea..036b89abdcc 100644 --- a/cuda_core/cuda/core/_memory/_legacy.py +++ b/cuda_core/cuda/core/_memory/_legacy.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from cuda.core._memory._buffer import DevicePointerT + from cuda.core._memory._buffer import DevicePointerType from cuda.core._memory._buffer import Buffer, MemoryResource from cuda.core._utils.cuda_utils import ( @@ -53,12 +53,12 @@ def allocate(self, size, stream=None) -> Buffer: ptr = 0 return Buffer._init(ptr, size, self) - def deallocate(self, ptr: DevicePointerT, size, stream): + def deallocate(self, ptr: DevicePointerType, size, stream): """Deallocate a buffer previously allocated by this resource. Parameters ---------- - ptr : :obj:`~_memory.DevicePointerT` + ptr : :obj:`~_memory.DevicePointerType` The pointer or handle to the buffer to deallocate. size : int The size of the buffer to deallocate, in bytes. diff --git a/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx b/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx index 205d3c77545..f37a4f18ee1 100644 --- a/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx @@ -16,6 +16,8 @@ from dataclasses import dataclass import threading import warnings +from cuda.core.typing import ManagedMemoryLocationType + __all__ = ['ManagedMemoryResource', 'ManagedMemoryResourceOptions'] @@ -30,7 +32,7 @@ cdef class ManagedMemoryResourceOptions: meaning depends on ``preferred_location_type``. (Default to ``None``) - preferred_location_type : ``"device"`` | ``"host"`` | ``"host_numa"`` | None, optional + preferred_location_type : ManagedMemoryLocationType | str | None, optional Controls how ``preferred_location`` is interpreted. When set to ``None`` (the default), legacy behavior is used: @@ -54,7 +56,7 @@ cdef class ManagedMemoryResourceOptions: (Default to ``None``) """ preferred_location: int | None = None - preferred_location_type: str | None = None + preferred_location_type: ManagedMemoryLocationType | str | None = None cdef class ManagedMemoryResource(_MemPool): @@ -97,7 +99,7 @@ cdef class ManagedMemoryResource(_MemPool): return -1 @property - def preferred_location(self) -> tuple | None: + def preferred_location(self) -> tuple[ManagedMemoryLocationType, int | None] | None: """The preferred location for managed memory allocations. Returns ``None`` if no preferred location is set (driver decides), @@ -108,8 +110,8 @@ cdef class ManagedMemoryResource(_MemPool): if self._pref_loc_type is None: return None if self._pref_loc_type == "host": - return ("host", None) - return (self._pref_loc_type, self._pref_loc_id) + return (ManagedMemoryLocationType.HOST, None) + return (ManagedMemoryLocationType(self._pref_loc_type), self._pref_loc_id) @property def is_device_accessible(self) -> bool: diff --git a/cuda_core/cuda/core/_memory/_memory_pool.pyx b/cuda_core/cuda/core/_memory/_memory_pool.pyx index f8f3b683d12..4e0f99d4529 100644 --- a/cuda_core/cuda/core/_memory/_memory_pool.pyx +++ b/cuda_core/cuda/core/_memory/_memory_pool.pyx @@ -144,12 +144,12 @@ cdef class _MemPool(MemoryResource): stream = Stream_accept(stream) if stream is not None else default_stream() return _MP_allocate(self, size, stream) - def deallocate(self, ptr: "DevicePointerT", size_t size, stream: Stream | GraphBuilder | None = None): + def deallocate(self, ptr: "DevicePointerType", size_t size, stream: Stream | GraphBuilder | None = None): """Deallocate a buffer previously allocated by this resource. Parameters ---------- - ptr : :obj:`~_memory.DevicePointerT` + ptr : :obj:`~_memory.DevicePointerType` The pointer or handle to the buffer to deallocate. size : int The size of the buffer to deallocate, in bytes. diff --git a/cuda_core/cuda/core/_memory/_virtual_memory_resource.py b/cuda_core/cuda/core/_memory/_virtual_memory_resource.py index 7d952e102fd..a60436a4305 100644 --- a/cuda_core/cuda/core/_memory/_virtual_memory_resource.py +++ b/cuda_core/cuda/core/_memory/_virtual_memory_resource.py @@ -1,11 +1,11 @@ -# 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 from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Iterable, Literal +from typing import TYPE_CHECKING, Iterable if TYPE_CHECKING: from cuda.core._stream import Stream @@ -21,15 +21,16 @@ _check_driver_error as raise_if_driver_error, ) from cuda.core._utils.version import binding_version +from cuda.core.typing import ( + VirtualMemoryAccessType, + VirtualMemoryAllocationType, + VirtualMemoryGranularityType, + VirtualMemoryHandleType, + VirtualMemoryLocationType, +) __all__ = ["VirtualMemoryResource", "VirtualMemoryResourceOptions"] -VirtualMemoryHandleTypeT = Literal["posix_fd", "generic", "win32_kmt", "fabric"] | None -VirtualMemoryLocationTypeT = Literal["device", "host", "host_numa", "host_numa_current"] -VirtualMemoryGranularityT = Literal["minimum", "recommended"] -VirtualMemoryAccessTypeT = Literal["rw", "r"] | None -VirtualMemoryAllocationTypeT = Literal["pinned", "managed"] - @dataclass class VirtualMemoryResourceOptions: @@ -38,18 +39,18 @@ class VirtualMemoryResourceOptions: Attributes ---------- - allocation_type: :obj:`~_memory.VirtualMemoryAllocationTypeT` + allocation_type: :obj:`~_memory.VirtualMemoryAllocationType` | str Controls the type of allocation. - location_type: :obj:`~_memory.VirtualMemoryLocationTypeT` + location_type: :obj:`~_memory.VirtualMemoryLocationType` | str Controls the location of the allocation. - handle_type: :obj:`~_memory.VirtualMemoryHandleTypeT` + handle_type: :obj:`~_memory.VirtualMemoryHandleType` | str Export handle type for the physical allocation. Use ``"posix_fd"`` on Linux if you plan to import/export the allocation (required for cuMemRetainAllocationHandle). Use `None` if you don't need an exportable handle. gpu_direct_rdma: bool Hint that the allocation should be GDR-capable (if supported). - granularity: :obj:`~_memory.VirtualMemoryGranularityT` + granularity: :obj:`~_memory.VirtualMemoryGranularityType` | str Controls granularity query and size rounding. addr_hint: int A (optional) virtual address hint to try to reserve at. Setting it to 0 lets the CUDA driver decide. @@ -57,50 +58,53 @@ class VirtualMemoryResourceOptions: Alignment for the VA reservation. If `None`, use the queried granularity. peers: Iterable[int] Extra device IDs that should be granted access in addition to ``device``. - self_access: :obj:`~_memory.VirtualMemoryAccessTypeT` + self_access: :obj:`~_memory.VirtualMemoryAccessType` | None | str Access flags for the owning device. - peer_access: :obj:`~_memory.VirtualMemoryAccessTypeT` + peer_access: :obj:`~_memory.VirtualMemoryAccessType` | None | str Access flags for peers. """ - # Human-friendly strings; normalized in __post_init__ - allocation_type: VirtualMemoryAllocationTypeT = "pinned" - location_type: VirtualMemoryLocationTypeT = "device" - handle_type: VirtualMemoryHandleTypeT = "posix_fd" - granularity: VirtualMemoryGranularityT = "recommended" + allocation_type: VirtualMemoryAllocationType = VirtualMemoryAllocationType.PINNED + location_type: VirtualMemoryLocationType = VirtualMemoryLocationType.DEVICE + handle_type: VirtualMemoryHandleType = VirtualMemoryHandleType.POSIX_FD + granularity: VirtualMemoryGranularityType = VirtualMemoryGranularityType.RECOMMENDED gpu_direct_rdma: bool = False addr_hint: int | None = 0 addr_align: int | None = None peers: Iterable[int] = field(default_factory=tuple) - self_access: VirtualMemoryAccessTypeT = "rw" - peer_access: VirtualMemoryAccessTypeT = "rw" + self_access: VirtualMemoryAccessType = VirtualMemoryAccessType.READ_WRITE + peer_access: VirtualMemoryAccessType = VirtualMemoryAccessType.READ_WRITE _a = driver.CUmemAccess_flags - _access_flags = {"rw": _a.CU_MEM_ACCESS_FLAGS_PROT_READWRITE, "r": _a.CU_MEM_ACCESS_FLAGS_PROT_READ, None: 0} # noqa: RUF012 + _access_flags = { # noqa: RUF012 + VirtualMemoryAccessType.READ_WRITE: _a.CU_MEM_ACCESS_FLAGS_PROT_READWRITE, + VirtualMemoryAccessType.READ: _a.CU_MEM_ACCESS_FLAGS_PROT_READ, + None: 0, + } _h = driver.CUmemAllocationHandleType _handle_types = { # noqa: RUF012 None: _h.CU_MEM_HANDLE_TYPE_NONE, - "posix_fd": _h.CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR, - "win32_kmt": _h.CU_MEM_HANDLE_TYPE_WIN32_KMT, - "fabric": _h.CU_MEM_HANDLE_TYPE_FABRIC, + VirtualMemoryHandleType.POSIX_FD: _h.CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR, + VirtualMemoryHandleType.WIN32_KMT: _h.CU_MEM_HANDLE_TYPE_WIN32_KMT, + VirtualMemoryHandleType.FABRIC: _h.CU_MEM_HANDLE_TYPE_FABRIC, } _g = driver.CUmemAllocationGranularity_flags _granularity = { # noqa: RUF012 - "recommended": _g.CU_MEM_ALLOC_GRANULARITY_RECOMMENDED, - "minimum": _g.CU_MEM_ALLOC_GRANULARITY_MINIMUM, + VirtualMemoryGranularityType.RECOMMENDED: _g.CU_MEM_ALLOC_GRANULARITY_RECOMMENDED, + VirtualMemoryGranularityType.MINIMUM: _g.CU_MEM_ALLOC_GRANULARITY_MINIMUM, } _l = driver.CUmemLocationType _location_type = { # noqa: RUF012 - "device": _l.CU_MEM_LOCATION_TYPE_DEVICE, - "host": _l.CU_MEM_LOCATION_TYPE_HOST, - "host_numa": _l.CU_MEM_LOCATION_TYPE_HOST_NUMA, - "host_numa_current": _l.CU_MEM_LOCATION_TYPE_HOST_NUMA_CURRENT, + VirtualMemoryLocationType.DEVICE: _l.CU_MEM_LOCATION_TYPE_DEVICE, + VirtualMemoryLocationType.HOST: _l.CU_MEM_LOCATION_TYPE_HOST, + VirtualMemoryLocationType.HOST_NUMA: _l.CU_MEM_LOCATION_TYPE_HOST_NUMA, + VirtualMemoryLocationType.HOST_NUMA_CURRENT: _l.CU_MEM_LOCATION_TYPE_HOST_NUMA_CURRENT, } _t = driver.CUmemAllocationType # CUDA 13+ exposes MANAGED in CUmemAllocationType; older 12.x does not - _allocation_type = {"pinned": _t.CU_MEM_ALLOCATION_TYPE_PINNED} # noqa: RUF012 + _allocation_type = {VirtualMemoryAllocationType.PINNED: _t.CU_MEM_ALLOCATION_TYPE_PINNED} # noqa: RUF012 if binding_version() >= (13, 0, 0): - _allocation_type["managed"] = _t.CU_MEM_ALLOCATION_TYPE_MANAGED + _allocation_type[VirtualMemoryAllocationType.MANAGED] = _t.CU_MEM_ALLOCATION_TYPE_MANAGED @staticmethod def _access_to_flags(spec: str): diff --git a/cuda_core/cuda/core/_module.pyx b/cuda_core/cuda/core/_module.pyx index d6c9481b82f..fee979b6130 100644 --- a/cuda_core/cuda/core/_module.pyx +++ b/cuda_core/cuda/core/_module.pyx @@ -12,6 +12,7 @@ from cuda.core._device import Device from cuda.core._launch_config cimport LaunchConfig from cuda.core._launch_config import LaunchConfig from cuda.core._stream cimport Stream +from cuda.core._program import ObjectCodeFormatType from cuda.core._resource_handles cimport ( LibraryHandle, KernelHandle, @@ -569,7 +570,7 @@ cdef class Kernel: CodeTypeT = bytes | bytearray | str -cdef tuple _supported_code_type = ("cubin", "ptx", "ltoir", "fatbin", "object", "library") +cdef tuple _supported_code_type = tuple(ObjectCodeFormatType.__members__.values()) cdef class ObjectCode: """Represent a compiled program to be loaded onto the device. @@ -599,7 +600,7 @@ cdef class ObjectCode: # _h_library is assigned during _lazy_load_module self._h_library = LibraryHandle() # Empty handle - self._code_type = code_type + self._code_type = str(code_type) self._module = module self._sym_map = {} if symbol_mapping is None else symbol_mapping self._name = name if name else "" @@ -629,7 +630,7 @@ cdef class ObjectCode: should be mapped to the mangled names before trying to retrieve them (default to no mappings). """ - return ObjectCode._init(module, "cubin", name=name, symbol_mapping=symbol_mapping) + return ObjectCode._init(module, ObjectCodeFormatType.CUBIN, name=name, symbol_mapping=symbol_mapping) @staticmethod def from_ptx(module: bytes | str, *, name: str = "", symbol_mapping: dict | None = None) -> ObjectCode: @@ -647,7 +648,7 @@ cdef class ObjectCode: should be mapped to the mangled names before trying to retrieve them (default to no mappings). """ - return ObjectCode._init(module, "ptx", name=name, symbol_mapping=symbol_mapping) + return ObjectCode._init(module, ObjectCodeFormatType.PTX, name=name, symbol_mapping=symbol_mapping) @staticmethod def from_ltoir(module: bytes | str, *, name: str = "", symbol_mapping: dict | None = None) -> ObjectCode: @@ -665,7 +666,7 @@ cdef class ObjectCode: should be mapped to the mangled names before trying to retrieve them (default to no mappings). """ - return ObjectCode._init(module, "ltoir", name=name, symbol_mapping=symbol_mapping) + return ObjectCode._init(module, ObjectCodeFormatType.LTOIR, name=name, symbol_mapping=symbol_mapping) @staticmethod def from_fatbin(module: bytes | str, *, name: str = "", symbol_mapping: dict | None = None) -> ObjectCode: @@ -683,7 +684,7 @@ cdef class ObjectCode: should be mapped to the mangled names before trying to retrieve them (default to no mappings). """ - return ObjectCode._init(module, "fatbin", name=name, symbol_mapping=symbol_mapping) + return ObjectCode._init(module, ObjectCodeFormatType.FATBIN, name=name, symbol_mapping=symbol_mapping) @staticmethod def from_object(module: bytes | str, *, name: str = "", symbol_mapping: dict | None = None) -> ObjectCode: @@ -701,7 +702,7 @@ cdef class ObjectCode: should be mapped to the mangled names before trying to retrieve them (default to no mappings). """ - return ObjectCode._init(module, "object", name=name, symbol_mapping=symbol_mapping) + return ObjectCode._init(module, ObjectCodeFormatType.OBJECT, name=name, symbol_mapping=symbol_mapping) @staticmethod def from_library(module: bytes | str, *, name: str = "", symbol_mapping: dict | None = None) -> ObjectCode: @@ -719,7 +720,7 @@ cdef class ObjectCode: should be mapped to the mangled names before trying to retrieve them (default to no mappings). """ - return ObjectCode._init(module, "library", name=name, symbol_mapping=symbol_mapping) + return ObjectCode._init(module, ObjectCodeFormatType.LIBRARY, name=name, symbol_mapping=symbol_mapping) # TODO: do we want to unload in a finalizer? Probably not.. @@ -758,7 +759,7 @@ cdef class ObjectCode: """ self._lazy_load_module() - supported_code_types = ("cubin", "ptx", "fatbin") + supported_code_types = (ObjectCodeFormatType.CUBIN, ObjectCodeFormatType.PTX, ObjectCodeFormatType.FATBIN) if self._code_type not in supported_code_types: raise RuntimeError(f'Unsupported code type "{self._code_type}" ({supported_code_types=})') try: diff --git a/cuda_core/cuda/core/_program.pyx b/cuda_core/cuda/core/_program.pyx index cfc66451c86..12d52198b2a 100644 --- a/cuda_core/cuda/core/_program.pyx +++ b/cuda_core/cuda/core/_program.pyx @@ -39,6 +39,7 @@ from cuda.core._utils.cuda_utils import ( is_sequence, ) from cuda.core._utils.version import binding_version, driver_version +from cuda.core.typing import ObjectCodeFormatType, CompilerBackendType, PCHStatusType, SourceCodeType __all__ = ["Program", "ProgramOptions"] @@ -67,14 +68,13 @@ cdef class Program: code : str | bytes | bytearray The source code to compile. For C++ and PTX, must be a string. For NVVM IR, can be str, bytes, or bytearray. - code_type : str + code_type : SourceCodeType | str The type of source code. Must be one of ``"c++"``, ``"ptx"``, or ``"nvvm"``. options : :class:`ProgramOptions`, optional Options to customize the compilation process. """ - - def __init__(self, code: str | bytes | bytearray, code_type: str, options: ProgramOptions | None = None): - Program_init(self, code, code_type, options) + def __init__(self, code: str | bytes | bytearray, code_type: SourceCodeType | str, options: ProgramOptions | None = None): + Program_init(self, code, str(code_type), options) def close(self): """Destroy this program.""" @@ -85,13 +85,13 @@ cdef class Program: self._h_nvvm.reset() def compile( - self, target_type: str, name_expressions: tuple | list = (), logs = None + self, target_type: ObjectCodeFormatType | str, name_expressions: tuple | list = (), logs = None ) -> ObjectCode: """Compile the program to the specified target type. Parameters ---------- - target_type : str + target_type : ObjectCodeFormatType | str The compilation target. Must be one of ``"ptx"``, ``"cubin"``, or ``"ltoir"``. name_expressions : tuple | list, optional Sequence of name expressions to make accessible in the compiled code. @@ -104,10 +104,10 @@ cdef class Program: :class:`~cuda.core.ObjectCode` The compiled object code. """ - return Program_compile(self, target_type, name_expressions, logs) + return Program_compile(self, str(target_type), name_expressions, logs) @property - def pch_status(self) -> str | None: + def pch_status(self) -> PCHStatusType | None: """PCH creation outcome from the most recent :meth:`compile` call. Possible values: @@ -130,12 +130,14 @@ cdef class Program: use the NVRTC backend. For PTX and NVVM programs this property always returns ``None``. """ - return self._pch_status + if self._pch_status is None: + return None + return PCHStatusType(self._pch_status) @property - def backend(self) -> str: - """Return this Program instance's underlying backend.""" - return self._backend + def backend(self) -> CompilerBackendType: + """Return this Program instance's underlying :class:`CompilerBackendType`.""" + return CompilerBackendType(self._backend) @property def handle(self) -> ProgramHandleT: @@ -435,7 +437,7 @@ class ProgramOptions: def _prepare_nvvm_options(self, as_bytes: bool = True) -> list[bytes] | list[str]: return _prepare_nvvm_options_impl(self, as_bytes) - def as_bytes(self, backend: str, target_type: str | None = None) -> list[bytes]: + def as_bytes(self, backend: CompilerBackendType | str, target_type: ObjectCodeFormatType | str | None = None) -> list[bytes]: """Convert program options to bytes format for the specified backend. This method transforms the program options into a format suitable for the @@ -444,9 +446,9 @@ class ProgramOptions: Parameters ---------- - backend : str + backend : CompilerBackendType | str The compiler backend to prepare options for. Must be either "nvrtc" or "nvvm". - target_type : str, optional + target_type : ObjectCodeFormatType | str, optional The compilation target type (e.g., "ptx", "cubin", "ltoir"). Some backends require additional options based on the target type. @@ -467,7 +469,7 @@ class ProgramOptions: >>> options = ProgramOptions(arch="sm_80", debug=True) >>> nvrtc_options = options.as_bytes("nvrtc") """ - backend = backend.lower() + backend = str(backend).lower() if backend == "nvrtc": return self._prepare_nvrtc_options() elif backend == "nvvm": @@ -639,7 +641,7 @@ cdef inline int Program_init(Program self, object code, str code_type, object op &nvrtc_prog, code_ptr, name_ptr, 0, NULL, NULL)) self._h_nvrtc = create_nvrtc_program_handle(nvrtc_prog) self._nvrtc_code = code_bytes - self._backend = "NVRTC" + self._backend = str(CompilerBackendType.NVRTC) self._linker = None elif code_type == "ptx": @@ -649,7 +651,7 @@ cdef inline int Program_init(Program self, object code, str code_type, object op self._linker = Linker( ObjectCode._init(code.encode(), code_type), options=_translate_program_options(options) ) - self._backend = self._linker.backend + self._backend = str(self._linker.backend) elif code_type == "nvvm": _get_nvvm_module() # Validate NVVM availability @@ -683,12 +685,11 @@ cdef inline int Program_init(Program self, object code, str code_type, object op if options.use_libdevice: self._use_libdevice = True - self._backend = "NVVM" + self._backend = str(CompilerBackendType.NVVM) self._linker = None else: - supported_code_types = ("c++", "ptx", "nvvm") - assert code_type not in supported_code_types, f"{code_type=}" + supported_code_types = tuple(x.value for x in SourceCodeType) if options.use_libdevice: raise ValueError("use_libdevice is only supported by the NVVM backend") raise RuntimeError(f"Unsupported {code_type=} ({supported_code_types=})") @@ -780,23 +781,18 @@ cdef bint _has_nvrtc_pch_apis(): return _nvrtc_pch_apis_cached -cdef str _PCH_STATUS_CREATED = "created" -cdef str _PCH_STATUS_NOT_ATTEMPTED = "not_attempted" -cdef str _PCH_STATUS_FAILED = "failed" - - -cdef str _read_pch_status(cynvrtc.nvrtcProgram prog): +cdef object _read_pch_status(cynvrtc.nvrtcProgram prog): """Query nvrtcGetPCHCreateStatus and translate to a high-level string.""" cdef cynvrtc.nvrtcResult err with nogil: err = cynvrtc.nvrtcGetPCHCreateStatus(prog) if err == cynvrtc.nvrtcResult.NVRTC_SUCCESS: - return _PCH_STATUS_CREATED + return PCHStatusType.CREATED if err == cynvrtc.nvrtcResult.NVRTC_ERROR_PCH_CREATE_HEAP_EXHAUSTED: return None # sentinel: caller should auto-retry if err == cynvrtc.nvrtcResult.NVRTC_ERROR_NO_PCH_CREATE_ATTEMPTED: - return _PCH_STATUS_NOT_ATTEMPTED - return _PCH_STATUS_FAILED + return PCHStatusType.NOT_ATTEMPTED + return PCHStatusType.FAILED cdef object Program_compile_nvrtc(Program self, str target_type, object name_expressions, object logs): @@ -822,7 +818,7 @@ cdef object Program_compile_nvrtc(Program self, str target_type, object name_exp ) from e if status is not None: - self._pch_status = status + self._pch_status = str(status) return result # Heap exhausted — auto-resize and retry with a fresh program @@ -844,7 +840,7 @@ cdef object Program_compile_nvrtc(Program self, str target_type, object name_exp ) status = _read_pch_status(retry_prog) - self._pch_status = status if status is not None else _PCH_STATUS_FAILED + self._pch_status = status if status is not None else str(PCHStatusType.FAILED) return result @@ -904,10 +900,10 @@ cdef object Program_compile_nvvm(Program self, str target_type, object logs): # Supported target types per backend cdef dict SUPPORTED_TARGETS = { - "NVRTC": ("ptx", "cubin", "ltoir"), - "NVVM": ("ptx", "ltoir"), - "nvJitLink": ("cubin", "ptx"), - "driver": ("cubin", "ptx"), + CompilerBackendType.NVRTC: (ObjectCodeFormatType.PTX, ObjectCodeFormatType.CUBIN, ObjectCodeFormatType.LTOIR), + CompilerBackendType.NVVM: (ObjectCodeFormatType.PTX, ObjectCodeFormatType.LTOIR), + CompilerBackendType.NVJITLINK: (ObjectCodeFormatType.CUBIN, ObjectCodeFormatType.PTX), + CompilerBackendType.DRIVER: (ObjectCodeFormatType.CUBIN, ObjectCodeFormatType.PTX), } diff --git a/cuda_core/cuda/core/_stream.pyx b/cuda_core/cuda/core/_stream.pyx index e2b477b8614..a93f8906969 100644 --- a/cuda_core/cuda/core/_stream.pyx +++ b/cuda_core/cuda/core/_stream.pyx @@ -61,7 +61,7 @@ cdef class StreamOptions: priority: int | None = None -class IsStreamT(Protocol): +class IsStreamType(Protocol): def __cuda_stream__(self) -> tuple[int, int]: """ For any Python object that is meant to be interpreted as a CUDA stream, the intent @@ -116,7 +116,7 @@ cdef class Stream: return Stream._from_handle(cls, get_per_thread_stream()) @classmethod - def _init(cls, obj: IsStreamT | None = None, options=None, device_id: int = None, + def _init(cls, obj: IsStreamType | None = None, options=None, device_id: int = None, ctx: Context = None): cdef StreamHandle h_stream cdef cydriver.CUstream borrowed diff --git a/cuda_core/cuda/core/checkpoint.py b/cuda_core/cuda/core/checkpoint.py index b5831f030ed..7f811013d19 100644 --- a/cuda_core/cuda/core/checkpoint.py +++ b/cuda_core/cuda/core/checkpoint.py @@ -9,7 +9,7 @@ from cuda.core._utils.cuda_utils import handle_return as _handle_cuda_return from cuda.core._utils.version import binding_version as _binding_version from cuda.core._utils.version import driver_version as _driver_version -from cuda.core.typing import ProcessStateT as _ProcessStateT +from cuda.core.typing import ProcessStateType as _ProcessStateType try: from cuda.bindings import driver as _driver @@ -17,7 +17,7 @@ from cuda import cuda as _driver -_PROCESS_STATE_NAME_ATTRS: tuple[tuple[str, _ProcessStateT], ...] = ( +_PROCESS_STATE_NAME_ATTRS: tuple[tuple[str, _ProcessStateType], ...] = ( ("CU_PROCESS_STATE_RUNNING", "running"), ("CU_PROCESS_STATE_LOCKED", "locked"), ("CU_PROCESS_STATE_CHECKPOINTED", "checkpointed"), @@ -63,7 +63,7 @@ def pid(self) -> int: return self._pid @property - def state(self) -> _ProcessStateT: + def state(self) -> _ProcessStateType: """ CUDA checkpoint state for this process. """ @@ -164,7 +164,7 @@ def _binding_version_supports_checkpoint(version) -> bool: return (major == 12 and (minor, patch) >= (8, 0)) or (major == 13 and (minor, patch) >= (0, 2)) or major > 13 -def _get_process_state_names(driver) -> dict[_Any, _ProcessStateT]: +def _get_process_state_names(driver) -> dict[_Any, _ProcessStateType]: return {getattr(driver.CUprocessState, attr): state_name for attr, state_name in _PROCESS_STATE_NAME_ATTRS} diff --git a/cuda_core/cuda/core/graph/_graph_definition.pyx b/cuda_core/cuda/core/graph/_graph_definition.pyx index 9a08232c556..5e4fa60d055 100644 --- a/cuda_core/cuda/core/graph/_graph_definition.pyx +++ b/cuda_core/cuda/core/graph/_graph_definition.pyx @@ -27,6 +27,8 @@ from dataclasses import dataclass from cuda.core._utils.cuda_utils import driver +from cuda.core.typing import GraphMemoryType + __all__ = ['GraphCondition', 'GraphAllocOptions', 'GraphDefinition'] @@ -78,7 +80,7 @@ class GraphAllocOptions: device : int or Device, optional The device on which to allocate memory. If None (default), uses the current CUDA context's device. - memory_type : str, optional + memory_type : GraphMemoryType | str, optional Type of memory to allocate. One of: - ``"device"`` (default): Pinned device memory, optimal for GPU kernels. @@ -101,7 +103,7 @@ class GraphAllocOptions: """ device: int | "Device" | None = None - memory_type: str = "device" + memory_type: GraphMemoryType = GraphMemoryType.DEVICE peer_access: list | None = None diff --git a/cuda_core/cuda/core/graph/_graph_node.pyx b/cuda_core/cuda/core/graph/_graph_node.pyx index 36401776600..c9d1786caa6 100644 --- a/cuda_core/cuda/core/graph/_graph_node.pyx +++ b/cuda_core/cuda/core/graph/_graph_node.pyx @@ -689,7 +689,7 @@ cdef inline AllocNode GN_alloc(GraphNode self, size_t size, object options): cdef str memory_type = "device" if options is not None and options.memory_type is not None: - memory_type = options.memory_type + memory_type = str(options.memory_type) c_memset(&alloc_params, 0, sizeof(alloc_params)) alloc_params.poolProps.handleTypes = cydriver.CUmemAllocationHandleType.CU_MEM_HANDLE_TYPE_NONE diff --git a/cuda_core/cuda/core/graph/_subclasses.pyx b/cuda_core/cuda/core/graph/_subclasses.pyx index 69505d83353..7c6f3c2b002 100644 --- a/cuda_core/cuda/core/graph/_subclasses.pyx +++ b/cuda_core/cuda/core/graph/_subclasses.pyx @@ -33,6 +33,7 @@ from cuda.core._utils.cuda_utils cimport HANDLE_RETURN from cuda.core.graph._utils cimport _is_py_host_trampoline from cuda.core._utils.cuda_utils import driver, handle_return +from cuda.core.typing import GraphConditionalType __all__ = [ 'AllocNode', @@ -169,8 +170,8 @@ cdef class AllocNode(GraphNode): The number of bytes allocated. device_id : int The device on which the allocation was made. - memory_type : str - The type of memory allocated (``"device"``, ``"host"``, or ``"managed"``). + memory_type : GraphMemoryType | str + The type of memory allocated. peer_access : tuple of int Device IDs that have read-write access to this allocation. options : GraphAllocOptions @@ -698,8 +699,8 @@ cdef class ConditionalNode(GraphNode): return self._condition @property - def cond_type(self) -> str | None: - """The conditional type as a string: 'if', 'while', or 'switch'. + def cond_type(self) -> GraphConditionalType | None: + """The conditional type: GraphConditionalType.IF, .WHILE, or .SWITCH Returns None when reconstructed from the driver pre-CUDA 13.2, as the conditional type cannot be determined. @@ -707,11 +708,11 @@ cdef class ConditionalNode(GraphNode): if self._condition is None: return None if self._cond_type == cydriver.CU_GRAPH_COND_TYPE_IF: - return "if" + return GraphConditionalType("if") elif self._cond_type == cydriver.CU_GRAPH_COND_TYPE_WHILE: - return "while" + return GraphConditionalType("while") else: - return "switch" + return GraphConditionalType("switch") @property def branches(self) -> tuple: diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index 189f778ff4f..1519127a220 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -170,6 +170,7 @@ _GPU_P2P_CAPS_INDEX_MAPPING = { GpuP2PCapsIndex.ATOMICS: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_ATOMICS, GpuP2PCapsIndex.PCI: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_PCI, GpuP2PCapsIndex.PROP: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_PROP, + GpuP2PCapsIndex.UNKNOWN: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_UNKNOWN, } diff --git a/cuda_core/cuda/core/system/_temperature.pxi b/cuda_core/cuda/core/system/_temperature.pxi index b322df4a591..d890ba0d128 100644 --- a/cuda_core/cuda/core/system/_temperature.pxi +++ b/cuda_core/cuda/core/system/_temperature.pxi @@ -71,6 +71,7 @@ _THERMAL_CONTROLLER_MAPPING = { nvml.ThermalController.NVSYSCON_E551: ThermalController.NVSYSCON_E551, nvml.ThermalController.MAX6649R: ThermalController.MAX6649R, nvml.ThermalController.ADT7473S: ThermalController.ADT7473S, + nvml.ThermalController.UNKNOWN: ThermalController.UNKNOWN, } diff --git a/cuda_core/cuda/core/typing.py b/cuda_core/cuda/core/typing.py index 3146e5d0bc3..1a6d377579d 100644 --- a/cuda_core/cuda/core/typing.py +++ b/cuda_core/cuda/core/typing.py @@ -2,19 +2,118 @@ # # SPDX-License-Identifier: Apache-2.0 -"""Public type aliases and protocols used in cuda.core API signatures.""" +"""Public type aliases, protocols, and enumerations used in cuda.core API signatures.""" +try: + from enum import StrEnum +except ImportError: + from backports.strenum import StrEnum from typing import Literal as _Literal -from cuda.core._context import DeviceResourcesT -from cuda.core._memory._buffer import DevicePointerT -from cuda.core._stream import IsStreamT - -ProcessStateT = _Literal["running", "locked", "checkpointed", "failed"] +from cuda.core._context import DeviceResourcesType +from cuda.core._stream import IsStreamType +from cuda.core._utils.cuda_utils import driver __all__ = [ - "DevicePointerT", - "DeviceResourcesT", - "IsStreamT", - "ProcessStateT", + "CompilerBackendType", + "DevicePointerType", + "DeviceResourcesType", + "GraphConditionalType", + "GraphMemoryType", + "IsStreamType", + "ManagedMemoryLocationType", + "ObjectCodeFormatType", + "PCHStatusType", + "ProcessStateType", + "SourceCodeType", + "VirtualMemoryAccessType", + "VirtualMemoryAllocationType", + "VirtualMemoryGranularityType", + "VirtualMemoryHandleType", + "VirtualMemoryLocationType", ] + + +# A type union of :obj:`~driver.CUdeviceptr`, `int` and `None` for hinting +# :attr:`Buffer.handle`. +DevicePointerType = driver.CUdeviceptr | int | None + + +ProcessStateType = _Literal["running", "locked", "checkpointed", "failed"] + + +class SourceCodeType(StrEnum): + CXX = "c++" + PTX = "ptx" + NVVM = "nvvm" + + +class ObjectCodeFormatType(StrEnum): + PTX = "ptx" + CUBIN = "cubin" + LTOIR = "ltoir" + FATBIN = "fatbin" + OBJECT = "object" + LIBRARY = "library" + + +class CompilerBackendType(StrEnum): + NVRTC = "NVRTC" + NVVM = "NVVM" + NVJITLINK = "nvJitLink" + DRIVER = "driver" + + +class PCHStatusType(StrEnum): + CREATED = "created" + NOT_ATTEMPTED = "not_attempted" + FAILED = "failed" + + +class GraphConditionalType(StrEnum): + IF = "if" + WHILE = "while" + SWITCH = "switch" + + +class GraphMemoryType(StrEnum): + DEVICE = "device" + HOST = "host" + MANAGED = "managed" + + +class ManagedMemoryLocationType(StrEnum): + DEVICE = "device" + HOST = "host" + HOST_NUMA = "host_numa" + + +class VirtualMemoryHandleType(StrEnum): + POSIX_FD = "posix_fd" + WIN32_KMT = "win32_kmt" + FABRIC = "fabric" + + +class VirtualMemoryLocationType(StrEnum): + DEVICE = "device" + HOST = "host" + HOST_NUMA = "host_numa" + HOST_NUMA_CURRENT = "host_numa_current" + + +class VirtualMemoryGranularityType(StrEnum): + MINIMUM = "minimum" + RECOMMENDED = "recommended" + + +class VirtualMemoryAccessType(StrEnum): + READ_WRITE = "rw" + READ = "r" + + +class VirtualMemoryAllocationType(StrEnum): + PINNED = "pinned" + MANAGED = "managed" + + +del StrEnum diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index 82cf9823219..158e9f913a7 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -16,18 +16,25 @@ CUDA runtime .. autosummary:: :toctree: generated/ - typing.DevicePointerT - typing.DeviceResourcesT - typing.ProcessStateT - _memory._virtual_memory_resource.VirtualMemoryAllocationTypeT - _memory._virtual_memory_resource.VirtualMemoryLocationTypeT - _memory._virtual_memory_resource.VirtualMemoryGranularityT - _memory._virtual_memory_resource.VirtualMemoryAccessTypeT - _memory._virtual_memory_resource.VirtualMemoryHandleTypeT _module.KernelAttributes _module.KernelOccupancy - _module.ParamInfo _module.MaxPotentialBlockSizeOccupancyResult + _module.ParamInfo + typing.CompilerBackendType + typing.DevicePointerType + typing.DeviceResourcesType + typing.GraphConditionalType + typing.GraphMemoryType + typing.ManagedMemoryLocationType + typing.ObjectCodeFormatType + typing.PCHStatusType + typing.ProcessStateType + typing.SourceCodeType + typing.VirtualMemoryAccessType + typing.VirtualMemoryAllocationType + typing.VirtualMemoryGranularityType + typing.VirtualMemoryHandleType + typing.VirtualMemoryLocationType :template: autosummary/cyclass.rst @@ -53,7 +60,7 @@ CUDA protocols :toctree: generated/ :template: protocol.rst - typing.IsStreamT + typing.IsStreamType NVML ---- diff --git a/cuda_core/docs/source/interoperability.rst b/cuda_core/docs/source/interoperability.rst index ae109bbad0b..4aac89d13df 100644 --- a/cuda_core/docs/source/interoperability.rst +++ b/cuda_core/docs/source/interoperability.rst @@ -35,7 +35,7 @@ in Python. While we encourage new Python projects to start using streams (and ot CUDA types) from ``cuda.core``, we understand that there are already several projects exposing their own stream types. -To address this issue, we propose the :attr:`~_stream.IsStreamT.__cuda_stream__` protocol +To address this issue, we propose the :attr:`~_stream.IsStreamType.__cuda_stream__` protocol (currently version 0) as follows: For any Python objects that are meant to be interpreted as a stream, they should add a ``__cuda_stream__`` *method* that returns a 2-tuple: The version number (``0``) and the address of ``cudaStream_t`` (both as Python ``int``): diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst index d3820876b14..7f0ced8c10b 100644 --- a/cuda_core/docs/source/release/1.0.0-notes.rst +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -120,6 +120,10 @@ Breaking changes ``CUgraphConditionalHandle`` value. Previously, ``.handle`` had to be extracted explicitly. +- Consistent naming of types annotation helpers + (`#2016 `__): + - :obj:`cuda.core.typing.DevicePointerT` -> :obj:`cuda.core.typing.DevicePointerType` + - :obj:`cuda.core.typing.IsStreamT` -> :obj:`cuda.core.typing.IsStreamType` Fixes and enhancements ----------------------- @@ -135,3 +139,22 @@ Fixes and enhancements stream and the consumer stream, matching the DLPack synchronization contract. Requires PyTorch >= 2.3. (`#749 `__) + +- Enums are not available in places where a small number of string values are + accepted or returned. You may continue to use the string values, or use + enumerations for better linting and type-checking. + (`#2016 `__) + The new enums are: + + - :class:`cuda.core.typing.CompilerBackendType` + - :class:`cuda.core.typing.GraphConditionalType` + - :class:`cuda.core.typing.GraphMemoryType` + - :class:`cuda.core.typing.ManagedMemoryLocationType` + - :class:`cuda.core.typing.ObjectCodeFormatType` + - :class:`cuda.core.typing.PCHStatusType` + - :class:`cuda.core.typing.SourceCodeType` + - :class:`cuda.core.typing.VirtualMemoryAccessType` + - :class:`cuda.core.typing.VirtualMemoryAllocationType` + - :class:`cuda.core.typing.VirtualMemoryGranularityType` + - :class:`cuda.core.typing.VirtualMemoryHandleType` + - :class:`cuda.core.typing.VirtualMemoryLocationType` diff --git a/cuda_core/tests/graph/test_graph_definition.py b/cuda_core/tests/graph/test_graph_definition.py index 5c7696d26bb..82791223d17 100644 --- a/cuda_core/tests/graph/test_graph_definition.py +++ b/cuda_core/tests/graph/test_graph_definition.py @@ -33,6 +33,7 @@ SwitchNode, WhileNode, ) +from cuda.core.typing import GraphConditionalType, GraphMemoryType ALLOC_SIZE = 1024 @@ -275,7 +276,7 @@ def _build_alloc_node(g): def _build_alloc_managed_node(g): _skip_if_no_managed_mempool() device_id = Device().device_id - options = GraphAllocOptions(memory_type="managed") + options = GraphAllocOptions(memory_type=GraphMemoryType.MANAGED) entry = g.allocate(ALLOC_SIZE) node = entry.allocate(ALLOC_SIZE, options) return node, { @@ -421,7 +422,7 @@ def _build_if_then_node(g): node = g.if_then(condition) return node, { "condition": condition, - "cond_type": "if", + "cond_type": lambda v: isinstance(v, GraphConditionalType) and v == "if", "branches": lambda v: isinstance(v, tuple) and len(v) == 1, "then": lambda v: isinstance(v, GraphDefinition), } @@ -432,7 +433,7 @@ def _build_if_else_node(g): node = g.if_else(condition) return node, { "condition": condition, - "cond_type": "if", + "cond_type": lambda v: isinstance(v, GraphConditionalType) and v == "if", "branches": lambda v: isinstance(v, tuple) and len(v) == 2, "then": lambda v: isinstance(v, GraphDefinition), "else_": lambda v: isinstance(v, GraphDefinition), @@ -444,7 +445,7 @@ def _build_while_loop_node(g): node = g.while_loop(condition) return node, { "condition": condition, - "cond_type": "while", + "cond_type": lambda v: isinstance(v, GraphConditionalType) and v == "while", "branches": lambda v: isinstance(v, tuple) and len(v) == 1, "body": lambda v: isinstance(v, GraphDefinition), } @@ -455,7 +456,7 @@ def _build_switch_node(g): node = g.switch(condition, 3) return node, { "condition": condition, - "cond_type": "switch", + "cond_type": lambda v: isinstance(v, GraphConditionalType) and v == "switch", "branches": lambda v: isinstance(v, tuple) and len(v) == 3, } diff --git a/cuda_core/tests/helpers/misc.py b/cuda_core/tests/helpers/misc.py index 89bc175a97d..ec879755cd2 100644 --- a/cuda_core/tests/helpers/misc.py +++ b/cuda_core/tests/helpers/misc.py @@ -16,7 +16,7 @@ def try_create_condition(g, default_value=1): class StreamWrapper: """ - A wrapper around Stream for testing IsStreamT conversions. + A wrapper around Stream for testing IsStreamType conversions. """ def __init__(self, stream): diff --git a/cuda_core/tests/test_checkpoint.py b/cuda_core/tests/test_checkpoint.py index 8bf62e82901..fd683aedcde 100644 --- a/cuda_core/tests/test_checkpoint.py +++ b/cuda_core/tests/test_checkpoint.py @@ -382,7 +382,7 @@ def test_process_rejects_invalid_pid(self, args, error_type, match): def test_public_symbols(self): assert checkpoint.__all__ == ["Process"] - assert not hasattr(checkpoint, "ProcessStateT") + assert not hasattr(checkpoint, "ProcessStateType") def test_pid_is_read_only(self): proc = checkpoint.Process(1) diff --git a/cuda_core/tests/test_enum_coverage.py b/cuda_core/tests/test_enum_coverage.py index 9c70c9f6042..a02ca8f15f9 100644 --- a/cuda_core/tests/test_enum_coverage.py +++ b/cuda_core/tests/test_enum_coverage.py @@ -7,14 +7,15 @@ # mapping dicts at import time, so it runs on any CI host that has a # compatible cuda.bindings version. -import importlib import inspect -import pkgutil import sys +from typing import Any import pytest import cuda.core +import cuda.core.typing +from cuda.bindings import driver from cuda.core import system if sys.version_info >= (3, 11): @@ -22,6 +23,8 @@ else: from backports.strenum import StrEnum +_MODULES = [cuda.core.typing] + # Each entry is: # (cuda_binding_enum, str_enum, mapping_dict, binding_unmapped, str_enum_unmapped) # @@ -38,7 +41,64 @@ # in binding_unmapped appears as either a key or a value of the mapping dict, # and conversely that every str_enum member not in str_enum_unmapped also # appears. -_CASES = [] + +_CASES: list[tuple[Any, StrEnum, dict | None, set[str], set[str]]] = [ + ( + driver.CUgraphConditionalNodeType, + cuda.core.typing.GraphConditionalType, + None, + set(), + set(), + ), + ( + driver.CUmemLocationType, + cuda.core.typing.ManagedMemoryLocationType, + None, + # We have some explicitly unsupported memory location types + { + "CU_MEM_LOCATION_TYPE_NONE", + "CU_MEM_LOCATION_TYPE_HOST_NUMA_CURRENT", + "CU_MEM_LOCATION_TYPE_INVISIBLE", + "CU_MEM_LOCATION_TYPE_MAX", + "CU_MEM_LOCATION_TYPE_INVALID", + }, + set(), + ), + ( + driver.CUmemAccess_flags, + cuda.core.typing.VirtualMemoryAccessType, + cuda.core.VirtualMemoryResourceOptions._access_flags, + {"CU_MEM_ACCESS_FLAGS_PROT_NONE", "CU_MEM_ACCESS_FLAGS_PROT_MAX"}, + set(), + ), + ( + driver.CUmemAllocationGranularity_flags, + cuda.core.typing.VirtualMemoryGranularityType, + cuda.core.VirtualMemoryResourceOptions._granularity, + set(), + set(), + ), + ( + driver.CUmemAllocationHandleType, + cuda.core.typing.VirtualMemoryHandleType, + cuda.core.VirtualMemoryResourceOptions._handle_types, + {"CU_MEM_HANDLE_TYPE_NONE", "CU_MEM_HANDLE_TYPE_WIN32", "CU_MEM_HANDLE_TYPE_MAX"}, + {"GENERIC"}, + ), + ( + driver.CUmemLocationType, + cuda.core.typing.VirtualMemoryLocationType, + None, + # We have some explicitly unsupported memory location types + { + "CU_MEM_LOCATION_TYPE_NONE", + "CU_MEM_LOCATION_TYPE_INVISIBLE", + "CU_MEM_LOCATION_TYPE_MAX", + "CU_MEM_LOCATION_TYPE_INVALID", + }, + set(), + ), +] if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: # Populated below only when NVML bindings are compatible, so that importing @@ -70,7 +130,7 @@ _device._GPU_P2P_STATUS_MAPPING, # Both the typo'd (SUPPORED) and corrected (SUPPORTED) spellings # share the same integer value; the mapping covers both via aliases - set(), + {"P2P_STATUS_CHIPSET_NOT_SUPPORED"}, set(), ), ( @@ -116,11 +176,8 @@ nvml.ThermalController, _device.ThermalController, _device._THERMAL_CONTROLLER_MAPPING, - # NONE and UNKNOWN are both handled by the .get() fallback that - # returns ThermalController.UNKNOWN when the value is not in the mapping - {"NONE", "UNKNOWN"}, - # UNKNOWN is the default returned by .get() for unrecognised controllers - {"UNKNOWN"}, + {"NONE"}, + {"NONE"}, ), ( nvml.ThermalTarget, @@ -156,11 +213,8 @@ nvml.GpuP2PCapsIndex, _device.GpuP2PCapsIndex, _device._GPU_P2P_CAPS_INDEX_MAPPING, - # UNKNOWN is returned by the driver when an index is unrecognised; - # it is not a capability the caller selects - {"P2P_CAPS_INDEX_UNKNOWN"}, - # UNKNOWN is a driver-side fallback, not a caller-selectable index - {"UNKNOWN"}, + set(), + set(), ), ( nvml.GpuTopologyLevel, @@ -208,7 +262,20 @@ # StrEnum subclasses that intentionally have no associated cuda_binding. # Add classes here (with a comment explaining why) when a new StrEnum is # introduced that wraps something other than a cuda_binding enum. -_UNBOUND_STR_ENUMS: frozenset[type] = frozenset() +_UNBOUND_STR_ENUMS: set[StrEnum] = { + cuda.core.typing.ObjectCodeFormatType, + cuda.core.typing.CompilerBackendType, + # This one enum coordinates values in two cuda_binding enums: + # CUmemAllocationType and CUmemLocationType + cuda.core.typing.GraphMemoryType, + # This should support all of the PCH-related values in nvrtcResult, but + # there is no easy way to check that since they are mixed in with other + # unrelated things + cuda.core.typing.PCHStatusType, + cuda.core.typing.SourceCodeType, + # This enum is dynamic depending on the version of CTK installed. + cuda.core.typing.VirtualMemoryAllocationType, +} @pytest.mark.parametrize( @@ -223,21 +290,41 @@ def test_wrapper_covers_all_binding_members(binding, str_enum, mapping, binding_ mapping (or be listed in the per-entry str_enum_unmapped set). """ required = set(binding.__members__) - binding_unmapped - # Compare by integer value so that enum aliases (two names, one integer) - # are treated as covered when the canonical member appears in the mapping. - covered_values = frozenset(int(m) for m in (*mapping.keys(), *mapping.values()) if isinstance(m, binding)) - missing = {name for name in required if int(binding.__members__[name]) not in covered_values} - assert not missing, f"{binding.__name__} has members not covered by the wrapper mapping: {missing}" + if mapping is not None: + # Compare by integer value so that enum aliases (two names, one integer) + # are treated as covered when the canonical member appears in the mapping. + covered_values = frozenset(int(m) for m in (*mapping.keys(), *mapping.values()) if isinstance(m, binding)) + missing = {name for name in required if int(binding.__members__[name]) not in covered_values} + assert not missing, f"{binding.__name__} has members not covered by the wrapper mapping: {missing}" # Reverse check: every StrEnum member must also appear in the mapping. if str_enum is not None: - required_str = set(str_enum.__members__) - str_enum_unmapped - covered_str = {m.name for m in (*mapping.keys(), *mapping.values()) if isinstance(m, str_enum)} - missing_str = required_str - covered_str - assert not missing_str, f"{str_enum.__name__} has members not covered by the wrapper mapping: {missing_str}" + if mapping is not None: + required_str = set(str_enum.__members__) - str_enum_unmapped + covered_str = {m.name for m in (*mapping.keys(), *mapping.values()) if isinstance(m, str_enum)} + missing_str = required_str - covered_str + assert not missing_str, f"{str_enum.__name__} has members not covered by the wrapper mapping: {missing_str}" + + # For checking a StrEnum against a cuda_binding enum directly, without a + # mapping, the best we can do is count them, since it's reasonable that + # they have been renamed for clarity. + required_count = len(required) + covered_str_enum = set(str_enum.__members__) - str_enum_unmapped + covered_count = len(covered_str_enum) + if required_count != covered_count: + raise AssertionError( + f"{str_enum.__name__} has {covered_count} members, but expected {required_count} based on {binding.__name__} " + "after accounting for unmapped members. This may indicate that some members are missing from the wrapper, " + "or that some wrapper members do not correspond to actual binding members." + ) -def test_all_str_enums_in_cases(): +@pytest.mark.skipif(sys.version_info < (3, 11), reason="Requires Python 3.11+ for StrEnum") +@pytest.mark.parametrize( + "module", + _MODULES, +) +def test_all_str_enums_in_cases(module): """Every StrEnum subclass in cuda.core must appear in _CASES or _UNBOUND_STR_ENUMS. This ensures that when a new StrEnum wrapper is added to cuda.core, the @@ -245,31 +332,18 @@ def test_all_str_enums_in_cases(): declare it as unbound in _UNBOUND_STR_ENUMS). """ - def discover_str_enums() -> set[type]: - """Walk all submodules of cuda.core and return every StrEnum subclass found.""" - found: set[type] = set() - for _, modname, _ in pkgutil.walk_packages( - path=cuda.core.__path__, - prefix=cuda.core.__name__ + ".", - onerror=lambda _: None, - ): - try: - mod = importlib.import_module(modname) - except Exception: # noqa - continue - try: - members = inspect.getmembers(mod, inspect.isclass) - except Exception: # noqa - continue - for _, obj in members: - if obj is not StrEnum and issubclass(obj, StrEnum): - found.add(obj) - return found + found = set() + + members = inspect.getmembers(module, inspect.isclass) + for _, obj in members: + if obj is not StrEnum and issubclass(obj, StrEnum): + found.add(obj) covered = {x[1] for x in _CASES if x[1] is not None} - uncovered = discover_str_enums() - covered - _UNBOUND_STR_ENUMS + uncovered = found - covered - _UNBOUND_STR_ENUMS + uncovered_names = sorted({c.__qualname__ for c in uncovered}) assert not uncovered, ( f"StrEnum subclasses in cuda.core not covered by _CASES: " - f"{sorted(c.__qualname__ for c in uncovered)}\n" + f"{uncovered_names}\n" "Add a _CASES entry for each, or add to _UNBOUND_STR_ENUMS if it does not wrap a cuda_binding enum." ) diff --git a/cuda_core/tests/test_launcher.py b/cuda_core/tests/test_launcher.py index b3461f5a371..f4858cdaef7 100644 --- a/cuda_core/tests/test_launcher.py +++ b/cuda_core/tests/test_launcher.py @@ -26,6 +26,7 @@ ) from cuda.core._memory._legacy import _SynchronousMemoryResource from cuda.core._utils.cuda_utils import CUDAError +from cuda.core.typing import ObjectCodeFormatType, SourceCodeType def test_launch_config_init(init_cuda): @@ -126,8 +127,8 @@ def test_launch_config_native_conversion(init_cuda): def test_launch_invalid_values(init_cuda): code = 'extern "C" __global__ void my_kernel() {}' - program = Program(code, "c++") - mod = program.compile("cubin") + program = Program(code, SourceCodeType.CXX) + mod = program.compile(ObjectCodeFormatType.CUBIN) stream = Device().create_stream() ker = mod.get_kernel("my_kernel") diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index fb99895616d..3ae6960fc29 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -46,6 +46,14 @@ from cuda.core._dlpack import DLDeviceType from cuda.core._memory import IPCBufferDescriptor from cuda.core._utils.cuda_utils import CUDAError, handle_return +from cuda.core.typing import ( + ManagedMemoryLocationType, + VirtualMemoryAccessType, + VirtualMemoryAllocationType, + VirtualMemoryGranularityType, + VirtualMemoryHandleType, + VirtualMemoryLocationType, +) from cuda.core.utils import StridedMemoryView POOL_SIZE = 2097152 # 2MB size @@ -134,19 +142,19 @@ def is_host_accessible(self) -> bool: def test_package_contents(): expected = [ "Buffer", - "MemoryResource", "DeviceMemoryResource", "DeviceMemoryResourceOptions", "GraphMemoryResource", - "IPCBufferDescriptor", "IPCAllocationHandle", + "IPCBufferDescriptor", "LegacyPinnedMemoryResource", "ManagedMemoryResource", "ManagedMemoryResourceOptions", - "PinnedMemoryResourceOptions", + "MemoryResource", "PinnedMemoryResource", - "VirtualMemoryResourceOptions", + "PinnedMemoryResourceOptions", "VirtualMemoryResource", + "VirtualMemoryResourceOptions", ] d = {} exec("from cuda.core._memory import *", d) # noqa: S102 @@ -800,14 +808,14 @@ def test_vmm_allocator_policy_configuration(): # Test with custom VMM config custom_config = VirtualMemoryResourceOptions( - allocation_type="pinned", - location_type="device", - granularity="minimum", + allocation_type=VirtualMemoryAllocationType.PINNED, + location_type=VirtualMemoryLocationType.DEVICE, + granularity=VirtualMemoryGranularityType.MINIMUM, gpu_direct_rdma=True, - handle_type="posix_fd" if not IS_WINDOWS else "win32_kmt", + handle_type=VirtualMemoryHandleType.POSIX_FD if not IS_WINDOWS else VirtualMemoryHandleType.WIN32_KMT, peers=(), - self_access="rw", - peer_access="rw", + self_access=VirtualMemoryAccessType.READ_WRITE, + peer_access=VirtualMemoryAccessType.READ_WRITE, ) vmm_mr = VirtualMemoryResource(device, config=custom_config) @@ -1090,7 +1098,7 @@ def test_managed_memory_resource_preferred_location_device(init_cuda): # Explicit style opts = ManagedMemoryResourceOptions( preferred_location=device.device_id, - preferred_location_type="device", + preferred_location_type=ManagedMemoryLocationType.DEVICE, ) mr = create_managed_memory_resource_or_skip(opts) assert mr.preferred_location == ("device", device.device_id) diff --git a/cuda_core/tests/test_program.py b/cuda_core/tests/test_program.py index 992ce336555..eda78bd0fd6 100644 --- a/cuda_core/tests/test_program.py +++ b/cuda_core/tests/test_program.py @@ -13,6 +13,7 @@ from cuda.core._module import Kernel, ObjectCode from cuda.core._program import Program, ProgramOptions from cuda.core._utils.cuda_utils import CUDAError, handle_return +from cuda.core.typing import CompilerBackendType, PCHStatusType pytest_plugins = ("cuda_python_test_helpers.nvvm_bitcode",) @@ -241,6 +242,7 @@ def test_cpp_program_with_various_options(init_cuda, options): code = 'extern "C" __global__ void my_kernel() {}' program = Program(code, "c++", options) assert program.backend == "NVRTC" + assert isinstance(program.backend, CompilerBackendType) program.compile("ptx") program.close() @@ -281,6 +283,7 @@ def test_cpp_program_pch_auto_creates(init_cuda, tmp_path): assert program.pch_status is None # not compiled yet program.compile("ptx") assert program.pch_status in ("created", "not_attempted", "failed") + assert isinstance(program.pch_status, PCHStatusType) program.close() @@ -681,7 +684,7 @@ def test_cpp_program_with_extra_sources(): def test_program_options_as_bytes_nvrtc(): """Test ProgramOptions.as_bytes() for NVRTC backend""" options = ProgramOptions(arch="sm_80", debug=True, lineinfo=True, ftz=True) - nvrtc_options = options.as_bytes("nvrtc") + nvrtc_options = options.as_bytes(CompilerBackendType.NVRTC) assert isinstance(nvrtc_options, list) assert all(isinstance(opt, bytes) for opt in nvrtc_options) options_str = [opt.decode() for opt in nvrtc_options] diff --git a/cuda_core/tests/test_typing_imports.py b/cuda_core/tests/test_typing_imports.py index 2e207d55d8b..237e028e853 100644 --- a/cuda_core/tests/test_typing_imports.py +++ b/cuda_core/tests/test_typing_imports.py @@ -8,26 +8,26 @@ def test_typing_module_imports(): """All type aliases and protocols are importable from cuda.core.typing.""" from cuda.core.typing import ( - DevicePointerT, - IsStreamT, - ProcessStateT, + DevicePointerType, + IsStreamType, + ProcessStateType, ) - assert DevicePointerT is not None - assert IsStreamT is not None - assert set(ProcessStateT.__args__) == {"running", "locked", "checkpointed", "failed"} + assert DevicePointerType is not None + assert IsStreamType is not None + assert set(ProcessStateType.__args__) == {"running", "locked", "checkpointed", "failed"} def test_typing_matches_private_definitions(): """cuda.core.typing re-exports match the original private definitions.""" - from cuda.core._memory._buffer import DevicePointerT as _DevicePointerT - from cuda.core._stream import IsStreamT as _IsStreamT + from cuda.core._memory._buffer import DevicePointerType as _DevicePointerT + from cuda.core._stream import IsStreamType as _IsStreamT from cuda.core.typing import ( - DevicePointerT, - IsStreamT, - ProcessStateT, + DevicePointerType, + IsStreamType, + ProcessStateType, ) - assert DevicePointerT is _DevicePointerT - assert IsStreamT is _IsStreamT - assert set(ProcessStateT.__args__) == {"running", "locked", "checkpointed", "failed"} + assert DevicePointerType is _DevicePointerT + assert IsStreamType is _IsStreamT + assert set(ProcessStateType.__args__) == {"running", "locked", "checkpointed", "failed"} From 38c032f86a1011eb02ca1fe4562254d454679000 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Tue, 5 May 2026 17:26:37 -0400 Subject: [PATCH 072/244] Move all cuda.core.system enums into cuda.core.system.typing (#2022) * Move all cuda.core.system enums into cuda.core.system.typing * Don't re-expose things in cuda.core.system.typing * Fix tests * Fix imports * Fix missing StrEnum test * Fix imports * Fix del and fix a test --- .../core/_memory/_device_memory_resource.pyx | 2 +- cuda_core/cuda/core/system/__init__.py | 4 +- cuda_core/cuda/core/system/_clock.pxi | 39 -- cuda_core/cuda/core/system/_cooler.pxi | 34 -- cuda_core/cuda/core/system/_device.pyx | 146 +------ cuda_core/cuda/core/system/_event.pxi | 30 -- cuda_core/cuda/core/system/_fan.pxi | 8 - cuda_core/cuda/core/system/_field_values.pxi | 3 - cuda_core/cuda/core/system/_inforom.pxi | 16 - cuda_core/cuda/core/system/_nvml_context.pyx | 2 +- cuda_core/cuda/core/system/_system.pyx | 2 +- cuda_core/cuda/core/system/_system_events.pyx | 16 +- cuda_core/cuda/core/system/_temperature.pxi | 62 --- cuda_core/cuda/core/system/typing.py | 367 ++++++++++++++++++ cuda_core/docs/source/api.rst | 1 - cuda_core/docs/source/api_private.rst | 39 +- cuda_core/tests/system/test_system_device.py | 55 +-- cuda_core/tests/system/test_system_events.py | 3 +- cuda_core/tests/test_enum_coverage.py | 37 +- 19 files changed, 465 insertions(+), 401 deletions(-) create mode 100644 cuda_core/cuda/core/system/typing.py diff --git a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx index 62df3cfb305..c19d7358b00 100644 --- a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx @@ -25,7 +25,7 @@ import multiprocessing import platform # no-cython-lint import uuid -from ._peer_access_utils import plan_peer_access_update +from cuda.core._memory._peer_access_utils import plan_peer_access_update from cuda.core._utils.cuda_utils import check_multiprocessing_start_method __all__ = ['DeviceMemoryResource', 'DeviceMemoryResourceOptions'] diff --git a/cuda_core/cuda/core/system/__init__.py b/cuda_core/cuda/core/system/__init__.py index 918d135901f..436b04577b6 100644 --- a/cuda_core/cuda/core/system/__init__.py +++ b/cuda_core/cuda/core/system/__init__.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 @@ -18,6 +18,8 @@ ] +from cuda.core.system import typing + from ._system import * if CUDA_BINDINGS_NVML_IS_COMPATIBLE: diff --git a/cuda_core/cuda/core/system/_clock.pxi b/cuda_core/cuda/core/system/_clock.pxi index d30a4192afe..a1fd940fc43 100644 --- a/cuda_core/cuda/core/system/_clock.pxi +++ b/cuda_core/cuda/core/system/_clock.pxi @@ -3,41 +3,12 @@ # SPDX-License-Identifier: Apache-2.0 -class ClockId(StrEnum): - """ - Clock Ids. These are used in combination with :class:`ClockType` to specify a single clock value. - """ - CURRENT = "current" - CUSTOMER_BOOST_MAX = "customer_boost_max" - # APP_CLOCK_TARGET and APP_CLOCK_DEFAULT are deprecated so not included here - - -ClockId.CURRENT.__doc__ = "Current actual clock value." -ClockId.CUSTOMER_BOOST_MAX.__doc__ = "OEM-defined maximum clock rate" - - _CLOCK_ID_MAPPING = { ClockId.CURRENT: nvml.ClockId.CURRENT, ClockId.CUSTOMER_BOOST_MAX: nvml.ClockId.CUSTOMER_BOOST_MAX, } -class ClocksEventReasons(StrEnum): - """ - Reasons for a clocks event. These are used in combination with :class:`ClockType` to specify the reason for a clocks event. - """ - NONE = "none" - GPU_IDLE = "gpu_idle" - APPLICATIONS_CLOCKS_SETTING = "applications_clocks_setting" - SW_POWER_CAP = "sw_power_cap" - HW_SLOWDOWN = "hw_slowdown" - SYNC_BOOST = "sync_boost" - SW_THERMAL_SLOWDOWN = "sw_thermal_slowdown" - HW_THERMAL_SLOWDOWN = "hw_thermal_slowdown" - HW_POWER_BRAKE_SLOWDOWN = "hw_power_brake_slowdown" - DISPLAY_CLOCK_SETTING = "display_clock_setting" - - _CLOCKS_EVENT_REASONS_MAPPING = { nvml.ClocksEventReasons.EVENT_REASON_NONE: ClocksEventReasons.NONE, nvml.ClocksEventReasons.EVENT_REASON_GPU_IDLE: ClocksEventReasons.GPU_IDLE, @@ -52,16 +23,6 @@ _CLOCKS_EVENT_REASONS_MAPPING = { } -class ClockType(StrEnum): - """ - Clock types. All speeds are in Mhz. - """ - GRAPHICS = "graphics" - SM = "sm" - MEMORY = "memory" - VIDEO = "video" - - _CLOCK_TYPE_MAPPING = { ClockType.GRAPHICS: nvml.ClockType.CLOCK_GRAPHICS, ClockType.SM: nvml.ClockType.CLOCK_SM, diff --git a/cuda_core/cuda/core/system/_cooler.pxi b/cuda_core/cuda/core/system/_cooler.pxi index cfce09bd284..b24f9252822 100644 --- a/cuda_core/cuda/core/system/_cooler.pxi +++ b/cuda_core/cuda/core/system/_cooler.pxi @@ -3,46 +3,12 @@ # SPDX-License-Identifier: Apache-2.0 -class CoolerControl(StrEnum): - """ - Cooler control type. - """ - TOGGLE = "toggle" - VARIABLE = "variable" - - -CoolerControl.TOGGLE.__doc__ = """ -This cooler can only be toggled either ON or OFF (e.g. a switch). -""" -CoolerControl.VARIABLE.__doc__ = """ -This cooler's level can be adjusted from some minimum to some maximum (e.g. a knob). -""" - - _COOLER_CONTROL_MAPPING = { nvml.CoolerControl.THERMAL_COOLER_SIGNAL_TOGGLE: CoolerControl.TOGGLE, nvml.CoolerControl.THERMAL_COOLER_SIGNAL_VARIABLE: CoolerControl.VARIABLE, } -class CoolerTarget(StrEnum): - """ - Cooler target. - """ - NONE = "none" - GPU = "gpu" - MEMORY = "memory" - POWER_SUPPLY = "power_supply" - # THERMAL_GPU_RELATED is a composite target, so it is omitted here and will - # get returned as 3 separate targets: GPU, MEMORY, and POWER_SUPPLY. - - -CoolerTarget.NONE.__doc__ = "This cooler controls nothing." -CoolerTarget.GPU.__doc__ = "This cooler can cool the GPU." -CoolerTarget.MEMORY.__doc__ = "This cooler can cool the memory." -CoolerTarget.POWER_SUPPLY.__doc__ = "This cooler can cool the power supply." - - _COOLER_TARGET_MAPPING = { nvml.CoolerTarget.THERMAL_NONE: CoolerTarget.NONE, nvml.CoolerTarget.THERMAL_GPU: CoolerTarget.GPU, diff --git a/cuda_core/cuda/core/system/_device.pyx b/cuda_core/cuda/core/system/_device.pyx index 1519127a220..9c8224e54aa 100644 --- a/cuda_core/cuda/core/system/_device.pyx +++ b/cuda_core/cuda/core/system/_device.pyx @@ -5,22 +5,34 @@ from libc.stdint cimport intptr_t, uint64_t from libc.math cimport ceil -import sys -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum from multiprocessing import cpu_count from typing import Iterable import warnings from cuda.bindings import nvml -try: - from cuda.bindings._internal._fast_enum import FastEnum -except ImportError: - from enum import IntEnum as FastEnum from ._nvml_context cimport initialize +from cuda.core.system.typing import ( + AddressingMode, + AffinityScope, + DeviceArch, + ClockId, + ClocksEventReasons, + ClockType, + CoolerControl, + CoolerTarget, + DeviceArch, + EventType, + FanControlPolicy, + FieldId, + GpuP2PCapsIndex, + GpuP2PStatus, + GpuTopologyLevel, + InforomObject, + TemperatureThresholds, + ThermalController, + ThermalTarget, +) cdef object _pstate_to_int(object pstate): @@ -57,53 +69,12 @@ include "_temperature.pxi" include "_utilization.pxi" -class AddressingMode(StrEnum): - """ - Addressing mode of a device. - - For Kepler™ or newer fully supported devices. - """ - HMM = "hmm" - ATS = "ats" - - -AddressingMode.HMM.__doc__ = """ - System allocated memory (``malloc``, ``mmap``) is addressable from the device - (GPU), via software-based mirroring of the CPU's page tables, on the GPU. -""" - - -AddressingMode.ATS.__doc__ = """ - System allocated memory (``malloc``, ``mmap``) is addressable from the device - (GPU), via Address Translation Services. This means that there is (effectively) - a single set of page tables, and the CPU and GPU both use them. -""" - - _ADDRESSING_MODE_MAPPING = { nvml.DeviceAddressingModeType.DEVICE_ADDRESSING_MODE_HMM: AddressingMode.HMM, nvml.DeviceAddressingModeType.DEVICE_ADDRESSING_MODE_ATS: AddressingMode.ATS, } -class AffinityScope(StrEnum): - """ - Scope for affinity queries. - """ - NODE = "node" - SOCKET = "socket" - - -AffinityScope.NODE.__doc__ = """ -The NUMA node is the scope of the affinity query. This is the default scope. -""" - - -AffinityScope.SOCKET.__doc__ = """ -The CPU socket is the scope of the affinity query. -""" - - _AFFINITY_SCOPE_MAPPING = { AffinityScope.NODE: nvml.AffinityScope.NODE, AffinityScope.SOCKET: nvml.AffinityScope.SOCKET, @@ -132,37 +103,6 @@ _BRAND_TYPE_MAPPING = { } -# This uses FastEnum instead of StrEnum because the ordering of the values is -# meaningful, e.g. Kepler "or later" -class DeviceArch(FastEnum): - """ - Device architecture. - """ - KEPLER = int(nvml.DeviceArch.KEPLER) - MAXWELL = int(nvml.DeviceArch.MAXWELL) - PASCAL = int(nvml.DeviceArch.PASCAL) - VOLTA = int(nvml.DeviceArch.VOLTA) - TURING = int(nvml.DeviceArch.TURING) - AMPERE = int(nvml.DeviceArch.AMPERE) - ADA = int(nvml.DeviceArch.ADA) - HOPPER = int(nvml.DeviceArch.HOPPER) - BLACKWELL = int(nvml.DeviceArch.BLACKWELL) - UNKNOWN = int(nvml.DeviceArch.UNKNOWN) - - -class GpuP2PCapsIndex(StrEnum): - """ - GPU peer-to-peer capabilities index. - """ - READ = "read" - WRITE = "write" - NVLINK = "nvlink" - ATOMICS = "atomics" - PCI = "pci" - PROP = "prop" - UNKNOWN = "unknown" - - _GPU_P2P_CAPS_INDEX_MAPPING = { GpuP2PCapsIndex.READ: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_READ, GpuP2PCapsIndex.WRITE: nvml.GpuP2PCapsIndex.P2P_CAPS_INDEX_WRITE, @@ -174,19 +114,6 @@ _GPU_P2P_CAPS_INDEX_MAPPING = { } -class GpuP2PStatus(StrEnum): - """ - GPU peer-to-peer status. - """ - OK = "ok" - CHIPSET_NOT_SUPPORTED = "chipset not supported" - GPU_NOT_SUPPORTED = "GPU not supported" - IOH_TOPOLOGY_NOT_SUPPORTED = "IOH topology not supported" - DISABLED_BY_REGKEY = "disabled by regkey" - NOT_SUPPORTED = "not supported" - UNKNOWN = "unknown" - - _GPU_P2P_STATUS_MAPPING = { nvml.GpuP2PStatus.P2P_STATUS_OK: GpuP2PStatus.OK, nvml.GpuP2PStatus.P2P_STATUS_CHIPSET_NOT_SUPPORTED: GpuP2PStatus.CHIPSET_NOT_SUPPORTED, @@ -198,18 +125,6 @@ _GPU_P2P_STATUS_MAPPING = { } -class GpuTopologyLevel(StrEnum): - """ - Represents level relationships within a system between two GPUs. - """ - INTERNAL = "internal" - SINGLE = "single" - MULTIPLE = "multiple" - HOSTBRIDGE = "hostbridge" - NODE = "node" - SYSTEM = "system" - - _GPU_TOPOLOGY_LEVEL_MAPPING = { GpuTopologyLevel.INTERNAL: nvml.GpuTopologyLevel.TOPOLOGY_INTERNAL, GpuTopologyLevel.SINGLE: nvml.GpuTopologyLevel.TOPOLOGY_SINGLE, @@ -1204,27 +1119,8 @@ def get_p2p_status(device1: Device, device2: Device, index: GpuP2PCapsIndex | st __all__ = [ - "AddressingMode", - "AffinityScope", - "ClockId", - "ClocksEventReasons", - "ClockType", - "CoolerControl", - "CoolerTarget", "Device", - "DeviceArch", - "EventType", - "FanControlPolicy", - "FieldId", "get_p2p_status", "get_topology_common_ancestor", - "GpuP2PCapsIndex", - "GpuP2PStatus", - "GpuTopologyLevel", - "InforomObject", "NvlinkInfo", - "TemperatureThresholds", - "ThermalController", - "ThermalTarget", - "Utilization", ] diff --git a/cuda_core/cuda/core/system/_event.pxi b/cuda_core/cuda/core/system/_event.pxi index 30aa11efa30..ef0752366db 100644 --- a/cuda_core/cuda/core/system/_event.pxi +++ b/cuda_core/cuda/core/system/_event.pxi @@ -3,36 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 -class EventType(StrEnum): - """ - Event types that can be waited on with :class:`DeviceEvents`. - """ - NONE = "none" - SINGLE_BIT_ECC_ERROR = "single_bit_ecc_error" - DOUBLE_BIT_ECC_ERROR = "double_bit_ecc_error" - PSTATE = "pstate" - XID_CRITICAL_ERROR = "xid_critical_error" - CLOCK = "clock" - POWER_SOURCE_CHANGE = "power_source_change" - MIG_CONFIG_CHANGE = "mig_config_change" - SINGLE_BIT_ECC_ERROR_STORM = "single_bit_ecc_error_storm" - DRAM_RETIREMENT_EVENT = "dram_retirement_event" - DRAM_RETIREMENT_FAILURE = "dram_retirement_failure" - NON_FATAL_POISON_ERROR = "non_fatal_poison_error" - FATAL_POISON_ERROR = "fatal_poison_error" - GPU_UNAVAILABLE_ERROR = "gpu_unavailable_error" - GPU_RECOVERY_ACTION = "gpu_recovery_action" - - -EventType.PSTATE.__doc__ = """ -Event about PState changes - -On Fermi™ architecture, PState changes are also an indicator that GPU is throttling down due to -no work being executed on the GPU, power capping or thermal capping. In a typical situation, -Fermi-based GPU should stay in P0 for the duration of the execution of the compute process. -""" - - _EVENT_TYPE_MAPPING = { nvml.EventType.NONE: EventType.NONE, nvml.EventType.SINGLE_BIT_ECC_ERROR: EventType.SINGLE_BIT_ECC_ERROR, diff --git a/cuda_core/cuda/core/system/_fan.pxi b/cuda_core/cuda/core/system/_fan.pxi index 651ab50997b..2bedc745202 100644 --- a/cuda_core/cuda/core/system/_fan.pxi +++ b/cuda_core/cuda/core/system/_fan.pxi @@ -3,14 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 -class FanControlPolicy(StrEnum): - """ - Fan control policies. - """ - TEMPERATURE_CONTROLLED = "temperature_controlled" - MANUAL = "manual" - - _FAN_CONTROL_POLICY_MAPPING = { nvml.FanControlPolicy.TEMPERATURE_CONTINUOUS_SW: FanControlPolicy.TEMPERATURE_CONTROLLED, nvml.FanControlPolicy.MANUAL: FanControlPolicy.MANUAL, diff --git a/cuda_core/cuda/core/system/_field_values.pxi b/cuda_core/cuda/core/system/_field_values.pxi index e8840160359..4a9e5cc748f 100644 --- a/cuda_core/cuda/core/system/_field_values.pxi +++ b/cuda_core/cuda/core/system/_field_values.pxi @@ -3,9 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 -FieldId = nvml.FieldId - - cdef class FieldValue: """ Represents the data from a single field value. diff --git a/cuda_core/cuda/core/system/_inforom.pxi b/cuda_core/cuda/core/system/_inforom.pxi index a76bb0a2bbd..f5ab70408d2 100644 --- a/cuda_core/cuda/core/system/_inforom.pxi +++ b/cuda_core/cuda/core/system/_inforom.pxi @@ -3,22 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 -class InforomObject(StrEnum): - """ - InfoROM objects types. - """ - OEM = "oem" - ECC = "ecc" - POWER = "power" - DEN = "den" - - -InforomObject.OEM.__doc__ = "An object defined by OEM." -InforomObject.ECC.__doc__ = "The ECC object determining the level of ECC support." -InforomObject.POWER.__doc__ = "The power management object." -InforomObject.DEN.__doc__ = "DRAM Encryption object." - - _INFOROM_OBJECT_MAPPING = { InforomObject.OEM: nvml.InforomObject.INFOROM_OEM, InforomObject.ECC: nvml.InforomObject.INFOROM_ECC, diff --git a/cuda_core/cuda/core/system/_nvml_context.pyx b/cuda_core/cuda/core/system/_nvml_context.pyx index e32ff518351..25445805642 100644 --- a/cuda_core/cuda/core/system/_nvml_context.pyx +++ b/cuda_core/cuda/core/system/_nvml_context.pyx @@ -6,7 +6,7 @@ import threading from cuda.bindings import nvml -from . import exceptions +from cuda.core.system import exceptions _NVML_STATE = _NVMLState.UNINITIALIZED diff --git a/cuda_core/cuda/core/system/_system.pyx b/cuda_core/cuda/core/system/_system.pyx index 815b366c87b..f3215506902 100644 --- a/cuda_core/cuda/core/system/_system.pyx +++ b/cuda_core/cuda/core/system/_system.pyx @@ -24,7 +24,7 @@ if CUDA_BINDINGS_NVML_IS_COMPATIBLE: except ImportError: CUDA_BINDINGS_NVML_IS_COMPATIBLE = False - from ._nvml_context import initialize + from cuda.core.system._nvml_context import initialize else: from cuda.core._utils.cuda_utils import driver, handle_return, runtime diff --git a/cuda_core/cuda/core/system/_system_events.pyx b/cuda_core/cuda/core/system/_system_events.pyx index a00a7bdf97b..ad9d6c174e3 100644 --- a/cuda_core/cuda/core/system/_system_events.pyx +++ b/cuda_core/cuda/core/system/_system_events.pyx @@ -5,25 +5,12 @@ from libc.stdint cimport intptr_t -import sys -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - from cuda.bindings import nvml from ._nvml_context cimport initialize from . import _device - - -class SystemEventType(StrEnum): - """ - System event types. - """ - UNBIND = "unbind" - BIND = "bind" +from cuda.core.system.typing import SystemEventType _SYSTEM_EVENT_TYPE_MAPPING = { @@ -193,5 +180,4 @@ def register_events(events: SystemEventType | str | list[SystemEventType | str]) __all__ = [ "register_events", - "SystemEventType", ] diff --git a/cuda_core/cuda/core/system/_temperature.pxi b/cuda_core/cuda/core/system/_temperature.pxi index d890ba0d128..577494b7f59 100644 --- a/cuda_core/cuda/core/system/_temperature.pxi +++ b/cuda_core/cuda/core/system/_temperature.pxi @@ -3,20 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 -class TemperatureThresholds(StrEnum): - """ - Temperature threshold types. - """ - SHUTDOWN = "shutdown" - SLOWDOWN = "slowdown" - MEM_MAX = "mem_max" - GPU_MAX = "gpu_max" - ACOUSTIC_MIN = "acoustic_min" - ACOUSTIC_CURR = "acoustic_curr" - ACOUSTIC_MAX = "acoustic_max" - GPS_CURR = "gps_curr" - - _TEMPERATURE_THRESHOLD_MAPPING = { TemperatureThresholds.SHUTDOWN: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_SHUTDOWN, TemperatureThresholds.SLOWDOWN: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_SLOWDOWN, @@ -29,30 +15,6 @@ _TEMPERATURE_THRESHOLD_MAPPING = { } -class ThermalController(StrEnum): - """ - Thermal controller types. - """ - GPU_INTERNAL = "gpu_internal" - ADM1032 = "adm1032" - ADT7461 = "adt7461" - MAX6649 = "max6649" - MAX1617 = "max1617" - LM99 = "lm99" - LM89 = "lm89" - LM64 = "lm64" - G781 = "g781" - ADT7473 = "adt7473" - SBMAX6649 = "sbmax6649" - VBIOSEVT = "vbiosevt" - OS = "os" - NVSYSCON_CANOAS = "nvsyscon_canoas" - NVSYSCON_E551 = "nvsyscon_e551" - MAX6649R = "max6649r" - ADT7473S = "adt7473s" - UNKNOWN = "unknown" - - _THERMAL_CONTROLLER_MAPPING = { nvml.ThermalController.GPU_INTERNAL: ThermalController.GPU_INTERNAL, nvml.ThermalController.ADM1032: ThermalController.ADM1032, @@ -75,30 +37,6 @@ _THERMAL_CONTROLLER_MAPPING = { } -class ThermalTarget(StrEnum): - """ - Thermal sensor targets. - """ - NONE = "none" - GPU = "gpu" - MEMORY = "memory" - POWER_SUPPLY = "power_supply" - BOARD = "board" - VCD_BOARD = "vcd_board" - VCD_INLET = "vcd_inlet" - VCD_OUTLET = "vcd_outlet" - ALL = "all" - - -ThermalTarget.GPU.__doc__ = "GPU core temperature requires physical GPU handle." -ThermalTarget.MEMORY.__doc__ = "GPU memory temperature requires physical GPU handle." -ThermalTarget.POWER_SUPPLY.__doc__ = "GPU power supply temperature requires physical GPU handle." -ThermalTarget.BOARD.__doc__ = "GPU board ambient temperature requires physical GPU handle." -ThermalTarget.VCD_BOARD.__doc__ = "Visual Computing Device Board temperature requires visual computing device handle." -ThermalTarget.VCD_INLET.__doc__ = "Visual Computing Device Inlet temperature requires visual computing device handle." -ThermalTarget.VCD_OUTLET.__doc__ = "Visual Computing Device Outlet temperature requires visual computing device handle." - - _THERMAL_TARGET_MAPPING = { nvml.ThermalTarget.NONE: ThermalTarget.NONE, nvml.ThermalTarget.GPU: ThermalTarget.GPU, diff --git a/cuda_core/cuda/core/system/typing.py b/cuda_core/cuda/core/system/typing.py new file mode 100644 index 00000000000..573a9ed3114 --- /dev/null +++ b/cuda_core/cuda/core/system/typing.py @@ -0,0 +1,367 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import sys + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from backports.strenum import StrEnum + + +__all__ = [ + "AddressingMode", + "AffinityScope", + "ClockId", + "ClockType", + "ClocksEventReasons", + "CoolerControl", + "CoolerTarget", + "EventType", + "FanControlPolicy", + "GpuP2PCapsIndex", + "GpuP2PStatus", + "GpuTopologyLevel", + "InforomObject", + "SystemEventType", + "TemperatureThresholds", + "ThermalController", + "ThermalTarget", +] + + +class AddressingMode(StrEnum): + """ + Addressing mode of a device. + + For Kepler™ or newer fully supported devices. + """ + + HMM = "hmm" + ATS = "ats" + + +AddressingMode.HMM.__doc__ = """ + System allocated memory (``malloc``, ``mmap``) is addressable from the device + (GPU), via software-based mirroring of the CPU's page tables, on the GPU. +""" + +AddressingMode.ATS.__doc__ = """ + System allocated memory (``malloc``, ``mmap``) is addressable from the device + (GPU), via Address Translation Services. This means that there is (effectively) + a single set of page tables, and the CPU and GPU both use them. +""" + + +class AffinityScope(StrEnum): + """ + Scope for affinity queries. + """ + + NODE = "node" + SOCKET = "socket" + + +AffinityScope.NODE.__doc__ = """ +The NUMA node is the scope of the affinity query. This is the default scope. +""" + +AffinityScope.SOCKET.__doc__ = """ +The CPU socket is the scope of the affinity query. +""" + + +class ClockId(StrEnum): + """ + Clock Ids. These are used in combination with :class:`ClockType` to specify a single clock value. + """ + + CURRENT = "current" + CUSTOMER_BOOST_MAX = "customer_boost_max" + # APP_CLOCK_TARGET and APP_CLOCK_DEFAULT are deprecated so not included here + + +ClockId.CURRENT.__doc__ = "Current actual clock value." +ClockId.CUSTOMER_BOOST_MAX.__doc__ = "OEM-defined maximum clock rate" + + +class ClocksEventReasons(StrEnum): + """ + Reasons for a clocks event. These are used in combination with :class:`ClockType` to specify the reason + for a clocks event. + """ + + NONE = "none" + GPU_IDLE = "gpu_idle" + APPLICATIONS_CLOCKS_SETTING = "applications_clocks_setting" + SW_POWER_CAP = "sw_power_cap" + HW_SLOWDOWN = "hw_slowdown" + SYNC_BOOST = "sync_boost" + SW_THERMAL_SLOWDOWN = "sw_thermal_slowdown" + HW_THERMAL_SLOWDOWN = "hw_thermal_slowdown" + HW_POWER_BRAKE_SLOWDOWN = "hw_power_brake_slowdown" + DISPLAY_CLOCK_SETTING = "display_clock_setting" + + +class ClockType(StrEnum): + """ + Clock types. All speeds are in Mhz. + """ + + GRAPHICS = "graphics" + SM = "sm" + MEMORY = "memory" + VIDEO = "video" + + +class CoolerControl(StrEnum): + """ + Cooler control type. + """ + + TOGGLE = "toggle" + VARIABLE = "variable" + + +CoolerControl.TOGGLE.__doc__ = """ +This cooler can only be toggled either ON or OFF (e.g. a switch). +""" + +CoolerControl.VARIABLE.__doc__ = """ +This cooler's level can be adjusted from some minimum to some maximum (e.g. a knob). +""" + + +class CoolerTarget(StrEnum): + """ + Cooler target. + """ + + NONE = "none" + GPU = "gpu" + MEMORY = "memory" + POWER_SUPPLY = "power_supply" + # THERMAL_GPU_RELATED is a composite target, so it is omitted here and will + # get returned as 3 separate targets: GPU, MEMORY, and POWER_SUPPLY. + + +CoolerTarget.NONE.__doc__ = "This cooler controls nothing." +CoolerTarget.GPU.__doc__ = "This cooler can cool the GPU." +CoolerTarget.MEMORY.__doc__ = "This cooler can cool the memory." +CoolerTarget.POWER_SUPPLY.__doc__ = "This cooler can cool the power supply." + + +class EventType(StrEnum): + """ + Event types that can be waited on with :class:`DeviceEvents`. + """ + + NONE = "none" + SINGLE_BIT_ECC_ERROR = "single_bit_ecc_error" + DOUBLE_BIT_ECC_ERROR = "double_bit_ecc_error" + PSTATE = "pstate" + XID_CRITICAL_ERROR = "xid_critical_error" + CLOCK = "clock" + POWER_SOURCE_CHANGE = "power_source_change" + MIG_CONFIG_CHANGE = "mig_config_change" + SINGLE_BIT_ECC_ERROR_STORM = "single_bit_ecc_error_storm" + DRAM_RETIREMENT_EVENT = "dram_retirement_event" + DRAM_RETIREMENT_FAILURE = "dram_retirement_failure" + NON_FATAL_POISON_ERROR = "non_fatal_poison_error" + FATAL_POISON_ERROR = "fatal_poison_error" + GPU_UNAVAILABLE_ERROR = "gpu_unavailable_error" + GPU_RECOVERY_ACTION = "gpu_recovery_action" + + +EventType.PSTATE.__doc__ = """ +Event about PState changes + +On Fermi™ architecture, PState changes are also an indicator that GPU is throttling down due to +no work being executed on the GPU, power capping or thermal capping. In a typical situation, +Fermi-based GPU should stay in P0 for the duration of the execution of the compute process. +""" + + +class FanControlPolicy(StrEnum): + """ + Fan control policies. + """ + + TEMPERATURE_CONTROLLED = "temperature_controlled" + MANUAL = "manual" + + +class GpuP2PCapsIndex(StrEnum): + """ + GPU peer-to-peer capabilities index. + """ + + READ = "read" + WRITE = "write" + NVLINK = "nvlink" + ATOMICS = "atomics" + PCI = "pci" + PROP = "prop" + UNKNOWN = "unknown" + + +class GpuP2PStatus(StrEnum): + """ + GPU peer-to-peer status. + """ + + OK = "ok" + CHIPSET_NOT_SUPPORTED = "chipset not supported" + GPU_NOT_SUPPORTED = "GPU not supported" + IOH_TOPOLOGY_NOT_SUPPORTED = "IOH topology not supported" + DISABLED_BY_REGKEY = "disabled by regkey" + NOT_SUPPORTED = "not supported" + UNKNOWN = "unknown" + + +class GpuTopologyLevel(StrEnum): + """ + Represents level relationships within a system between two GPUs. + """ + + INTERNAL = "internal" + SINGLE = "single" + MULTIPLE = "multiple" + HOSTBRIDGE = "hostbridge" + NODE = "node" + SYSTEM = "system" + + +class InforomObject(StrEnum): + """ + InfoROM objects types. + """ + + OEM = "oem" + ECC = "ecc" + POWER = "power" + DEN = "den" + + +InforomObject.OEM.__doc__ = "An object defined by OEM." +InforomObject.ECC.__doc__ = "The ECC object determining the level of ECC support." +InforomObject.POWER.__doc__ = "The power management object." +InforomObject.DEN.__doc__ = "DRAM Encryption object." + + +class SystemEventType(StrEnum): + """ + System event types. + """ + + UNBIND = "unbind" + BIND = "bind" + + +class TemperatureThresholds(StrEnum): + """ + Temperature threshold types. + """ + + SHUTDOWN = "shutdown" + SLOWDOWN = "slowdown" + MEM_MAX = "mem_max" + GPU_MAX = "gpu_max" + ACOUSTIC_MIN = "acoustic_min" + ACOUSTIC_CURR = "acoustic_curr" + ACOUSTIC_MAX = "acoustic_max" + GPS_CURR = "gps_curr" + + +class ThermalController(StrEnum): + """ + Thermal controller types. + """ + + GPU_INTERNAL = "gpu_internal" + ADM1032 = "adm1032" + ADT7461 = "adt7461" + MAX6649 = "max6649" + MAX1617 = "max1617" + LM99 = "lm99" + LM89 = "lm89" + LM64 = "lm64" + G781 = "g781" + ADT7473 = "adt7473" + SBMAX6649 = "sbmax6649" + VBIOSEVT = "vbiosevt" + OS = "os" + NVSYSCON_CANOAS = "nvsyscon_canoas" + NVSYSCON_E551 = "nvsyscon_e551" + MAX6649R = "max6649r" + ADT7473S = "adt7473s" + UNKNOWN = "unknown" + + +class ThermalTarget(StrEnum): + """ + Thermal sensor targets. + """ + + NONE = "none" + GPU = "gpu" + MEMORY = "memory" + POWER_SUPPLY = "power_supply" + BOARD = "board" + VCD_BOARD = "vcd_board" + VCD_INLET = "vcd_inlet" + VCD_OUTLET = "vcd_outlet" + ALL = "all" + + +ThermalTarget.GPU.__doc__ = "GPU core temperature requires physical GPU handle." +ThermalTarget.MEMORY.__doc__ = "GPU memory temperature requires physical GPU handle." +ThermalTarget.POWER_SUPPLY.__doc__ = "GPU power supply temperature requires physical GPU handle." +ThermalTarget.BOARD.__doc__ = "GPU board ambient temperature requires physical GPU handle." +ThermalTarget.VCD_BOARD.__doc__ = "Visual Computing Device Board temperature requires visual computing device handle." +ThermalTarget.VCD_INLET.__doc__ = "Visual Computing Device Inlet temperature requires visual computing device handle." +ThermalTarget.VCD_OUTLET.__doc__ = "Visual Computing Device Outlet temperature requires visual computing device handle." + + +# DeviceArch values are derived from cuda.bindings.nvml at definition time, so +# the class can only be defined when nvml is importable. +try: + from cuda.bindings import nvml as _nvml + + try: + from cuda.bindings._internal._fast_enum import FastEnum as _FastEnum + except ImportError: + from enum import IntEnum as _FastEnum + + # This uses FastEnum instead of StrEnum because the ordering of the values is + # meaningful, e.g. Kepler "or later" + class DeviceArch(_FastEnum): + """ + Device architecture. + """ + + KEPLER = int(_nvml.DeviceArch.KEPLER) + MAXWELL = int(_nvml.DeviceArch.MAXWELL) + PASCAL = int(_nvml.DeviceArch.PASCAL) + VOLTA = int(_nvml.DeviceArch.VOLTA) + TURING = int(_nvml.DeviceArch.TURING) + AMPERE = int(_nvml.DeviceArch.AMPERE) + ADA = int(_nvml.DeviceArch.ADA) + HOPPER = int(_nvml.DeviceArch.HOPPER) + BLACKWELL = int(_nvml.DeviceArch.BLACKWELL) + UNKNOWN = int(_nvml.DeviceArch.UNKNOWN) + + __all__.append("DeviceArch") + + FieldId = _nvml.FieldId + + __all__.append("FieldId") + + del _nvml, _FastEnum + +except ImportError: + pass + + +del StrEnum, sys diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 5b2ee954d0c..582f2140903 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -267,7 +267,6 @@ Events :toctree: generated/ system.register_events - system.SystemEventType Types ````` diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index 158e9f913a7..846ed928bf2 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -90,6 +90,7 @@ NVML system._device.Temperature system._device.ThermalSensor system._device.ThermalSettings + system._device.Utilization system._system_events.RegisteredSystemEvents system._system_events.SystemEvent system._system_events.SystemEvents @@ -99,22 +100,22 @@ NVML .. autosummary:: :toctree: generated/ - system.AddressingMode - system.AffinityScope - system.ClockId - system.ClocksEventReasons - system.ClockType - system.CoolerControl - system.CoolerTarget - system.DeviceArch - system.EventType - system.FanControlPolicy - system.FieldId - system.GpuP2PCapsIndex - system.GpuP2PStatus - system.GpuTopologyLevel - system.InforomObject - system.TemperatureThresholds - system.ThermalController - system.ThermalTarget - system.SystemEventType + system.typing.AddressingMode + system.typing.AffinityScope + system.typing.ClockId + system.typing.ClocksEventReasons + system.typing.ClockType + system.typing.CoolerControl + system.typing.CoolerTarget + system.typing.DeviceArch + system.typing.EventType + system.typing.FanControlPolicy + system.typing.FieldId + system.typing.GpuP2PCapsIndex + system.typing.GpuP2PStatus + system.typing.GpuTopologyLevel + system.typing.InforomObject + system.typing.SystemEventType + system.typing.TemperatureThresholds + system.typing.ThermalController + system.typing.ThermalTarget diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index 9625d16b2db..4241569f99a 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -17,6 +17,7 @@ import pytest from cuda.core import system +from cuda.core.system import typing if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: from cuda.bindings import nvml @@ -71,7 +72,7 @@ def test_to_cuda_device(): def test_device_architecture(): for device in system.Device.get_all_devices(): device_arch = device.arch - assert isinstance(device_arch, system.DeviceArch) + assert isinstance(device_arch, typing.DeviceArch) def test_device_bar1_memory(): @@ -98,8 +99,8 @@ def test_device_bar1_memory(): @pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="Device attributes not supported on WSL or Windows") def test_device_cpu_affinity(): for device in system.Device.get_all_devices(): - with unsupported_before(device, DeviceArch.KEPLER): - affinity = device.get_cpu_affinity(system.AffinityScope.NODE) + with unsupported_before(device, typing.DeviceArch.KEPLER): + affinity = device.get_cpu_affinity(typing.AffinityScope.NODE) assert isinstance(affinity, list) os.sched_setaffinity(0, affinity) assert os.sched_getaffinity(0) == set(affinity) @@ -108,8 +109,8 @@ def test_device_cpu_affinity(): @pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="Device attributes not supported on WSL or Windows") def test_affinity(): for device in system.Device.get_all_devices(): - for scope in system.AffinityScope.__members__.values(): - with unsupported_before(device, DeviceArch.KEPLER): + for scope in typing.AffinityScope.__members__.values(): + with unsupported_before(device, typing.DeviceArch.KEPLER): affinity = device.get_cpu_affinity(scope) assert isinstance(affinity, list) @@ -274,7 +275,7 @@ def test_register_events(): for device in system.Device.get_all_devices(): supported_events = device.get_supported_event_types() assert isinstance(supported_events, list) - assert all(isinstance(ev, system.EventType) for ev in supported_events) + assert all(isinstance(ev, typing.EventType) for ev in supported_events) for device in system.Device.get_all_devices(): events = device.register_events(["xid_critical_error"]) @@ -282,7 +283,7 @@ def test_register_events(): events.wait(timeout_ms=500) for device in system.Device.get_all_devices(): - events = device.register_events([system.EventType.XID_CRITICAL_ERROR]) + events = device.register_events([typing.EventType.XID_CRITICAL_ERROR]) with pytest.raises(system.TimeoutError): events.wait(timeout_ms=500) @@ -364,8 +365,8 @@ def test_field_values(): assert len(device.get_field_values([])) == 0 field_ids = [ - system.FieldId.DEV_TOTAL_ENERGY_CONSUMPTION, - system.FieldId.DEV_PCIE_COUNT_TX_BYTES, + typing.FieldId.DEV_TOTAL_ENERGY_CONSUMPTION, + typing.FieldId.DEV_PCIE_COUNT_TX_BYTES, ] field_values = device.get_field_values(field_ids) with unsupported_before(device, None): @@ -392,7 +393,7 @@ def test_field_values(): # Test only one element, because that's weirdly a special case field_ids = [ - system.FieldId.DEV_PCIE_REPLAY_COUNTER, + typing.FieldId.DEV_PCIE_REPLAY_COUNTER, ] field_values = device.get_field_values(field_ids) assert len(field_values) == 1 @@ -438,7 +439,7 @@ def test_addressing_mode(): # is also unsupported on other hardware. with unsupported_before(device, None): addressing_mode = device.addressing_mode - assert addressing_mode is None or addressing_mode in system.AddressingMode.__members__.values() + assert addressing_mode is None or addressing_mode in typing.AddressingMode.__members__.values() def test_display_mode(): @@ -474,7 +475,7 @@ def test_get_topology_common_ancestor(): devices = list(system.Device.get_all_devices()) ancestor = system.get_topology_common_ancestor(devices[0], devices[1]) - assert isinstance(ancestor, system.GpuTopologyLevel) + assert isinstance(ancestor, typing.GpuTopologyLevel) @pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="Device attributes not supported on WSL or Windows") @@ -488,8 +489,8 @@ def test_get_p2p_status(): devices = list(system.Device.get_all_devices()) - status = system.get_p2p_status(devices[0], devices[1], system.GpuP2PCapsIndex.READ) - assert isinstance(status, system.GpuP2PStatus) + status = system.get_p2p_status(devices[0], devices[1], typing.GpuP2PCapsIndex.READ) + assert isinstance(status, typing.GpuP2PStatus) @pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="Device attributes not supported on WSL or Windows") @@ -498,7 +499,7 @@ def test_get_nearest_gpus(): # in practice on our CI. for device in system.Device.get_all_devices(): - for near_device in device.get_topology_nearest_gpus(system.GpuTopologyLevel.SINGLE): + for near_device in device.get_topology_nearest_gpus(typing.GpuTopologyLevel.SINGLE): assert isinstance(near_device, system.Device) @@ -520,7 +521,7 @@ def test_get_inforom_version(): assert isinstance(inforom_image_version, str) assert len(inforom_image_version) > 0 - inforom_version = inforom.get_version(system.InforomObject.OEM) + inforom_version = inforom.get_version(typing.InforomObject.OEM) assert isinstance(inforom_version, str) assert len(inforom_version) > 0 @@ -558,7 +559,7 @@ def test_auto_boosted_clocks_enabled(): def test_clock(): for device in system.Device.get_all_devices(): - for clock_type in system.ClockType: + for clock_type in typing.ClockType: clock = device.get_clock(clock_type) assert isinstance(clock, _device.ClockInfo) @@ -609,11 +610,11 @@ def test_clock_event_reasons(): for device in system.Device.get_all_devices(): with unsupported_before(device, None): reasons = device.current_clock_event_reasons - assert all(isinstance(reason, system.ClocksEventReasons) for reason in reasons) + assert all(isinstance(reason, typing.ClocksEventReasons) for reason in reasons) with unsupported_before(device, None): reasons = device.supported_clock_event_reasons - assert all(isinstance(reason, system.ClocksEventReasons) for reason in reasons) + assert all(isinstance(reason, typing.ClocksEventReasons) for reason in reasons) def test_fan(): @@ -651,7 +652,7 @@ def test_fan(): assert min_ <= max_ control_policy = fan_info.control_policy - assert isinstance(control_policy, system.FanControlPolicy) + assert isinstance(control_policy, typing.FanControlPolicy) finally: fan_info.set_default_speed() @@ -669,10 +670,10 @@ def test_cooler(): assert isinstance(cooler_info, _device.CoolerInfo) signal_type = cooler_info.signal_type - assert isinstance(signal_type, (system.CoolerControl, type(None))) + assert isinstance(signal_type, (typing.CoolerControl, type(None))) target = cooler_info.target - assert all(isinstance(t, system.CoolerTarget) for t in target) + assert all(isinstance(t, typing.CoolerTarget) for t in target) def test_temperature(): @@ -687,7 +688,7 @@ def test_temperature(): # By docs, should be supported on KEPLER or newer, but experimentally, # is also unsupported on other hardware. with unsupported_before(device, None): - for threshold in list(system.TemperatureThresholds): + for threshold in list(typing.TemperatureThresholds): t = temperature.get_threshold(threshold) assert isinstance(t, int) assert t >= 0 @@ -698,13 +699,13 @@ def test_temperature(): assert margin >= 0 with unsupported_before(device, None): - thermals = temperature.get_thermal_settings(system.ThermalTarget.ALL) + thermals = temperature.get_thermal_settings(typing.ThermalTarget.ALL) assert isinstance(thermals, _device.ThermalSettings) for i, sensor in enumerate(thermals): assert isinstance(sensor, _device.ThermalSensor) - assert isinstance(sensor.target, system.ThermalTarget) - assert isinstance(sensor.controller, system.ThermalController) + assert isinstance(sensor.target, typing.ThermalTarget) + assert isinstance(sensor.controller, typing.ThermalController) assert isinstance(sensor.default_min_temp, int) assert sensor.default_min_temp >= 0 assert isinstance(sensor.default_max_temp, int) @@ -779,7 +780,7 @@ def test_utilization(): for device in system.Device.get_all_devices(): with unsupported_before(device, None): utilization = device.utilization - assert isinstance(utilization, system.Utilization) + assert isinstance(utilization, _device.Utilization) gpu = utilization.gpu assert isinstance(gpu, int) diff --git a/cuda_core/tests/system/test_system_events.py b/cuda_core/tests/system/test_system_events.py index 7cfe7459109..ce204001a4e 100644 --- a/cuda_core/tests/system/test_system_events.py +++ b/cuda_core/tests/system/test_system_events.py @@ -11,6 +11,7 @@ import pytest from cuda.core import system +from cuda.core.system import typing @pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="System events not supported on WSL or Windows") @@ -23,7 +24,7 @@ def test_register_events(): # Also, some hardware doesn't support any event types. try: - events = system.register_events([system.SystemEventType.UNBIND]) + events = system.register_events([typing.SystemEventType.UNBIND]) except system.UnknownError: pytest.skip("system events may only be registered once per process") diff --git a/cuda_core/tests/test_enum_coverage.py b/cuda_core/tests/test_enum_coverage.py index a02ca8f15f9..e286405b590 100644 --- a/cuda_core/tests/test_enum_coverage.py +++ b/cuda_core/tests/test_enum_coverage.py @@ -103,14 +103,17 @@ if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: # Populated below only when NVML bindings are compatible, so that importing # this module on an incompatible host does not raise ImportError. + import cuda.core.system.typing as system_typing from cuda.bindings import nvml from cuda.core.system import _device, _system_events + _MODULES.append(system_typing) + _CASES.extend( [ ( nvml.DeviceAddressingModeType, - _device.AddressingMode, + system_typing.AddressingMode, _device._ADDRESSING_MODE_MAPPING, # NONE means "no special addressing mode is active"; not a valid target {"DEVICE_ADDRESSING_MODE_NONE"}, @@ -126,7 +129,7 @@ ), ( nvml.GpuP2PStatus, - _device.GpuP2PStatus, + system_typing.GpuP2PStatus, _device._GPU_P2P_STATUS_MAPPING, # Both the typo'd (SUPPORED) and corrected (SUPPORTED) spellings # share the same integer value; the mapping covers both via aliases @@ -135,28 +138,28 @@ ), ( nvml.ClocksEventReasons, - _device.ClocksEventReasons, + system_typing.ClocksEventReasons, _device._CLOCKS_EVENT_REASONS_MAPPING, set(), set(), ), ( nvml.EventType, - _device.EventType, + system_typing.EventType, _device._EVENT_TYPE_MAPPING, set(), set(), ), ( nvml.FanControlPolicy, - _device.FanControlPolicy, + system_typing.FanControlPolicy, _device._FAN_CONTROL_POLICY_MAPPING, set(), set(), ), ( nvml.CoolerControl, - _device.CoolerControl, + system_typing.CoolerControl, _device._COOLER_CONTROL_MAPPING, # NONE means no signal; COUNT is a sentinel {"THERMAL_COOLER_SIGNAL_NONE", "THERMAL_COOLER_SIGNAL_COUNT"}, @@ -164,7 +167,7 @@ ), ( nvml.CoolerTarget, - _device.CoolerTarget, + system_typing.CoolerTarget, _device._COOLER_TARGET_MAPPING, # GPU_RELATED is a composite bitmask (GPU | MEMORY | POWER_SUPPLY); # the wrapper expands it into individual targets instead of mapping @@ -174,14 +177,14 @@ ), ( nvml.ThermalController, - _device.ThermalController, + system_typing.ThermalController, _device._THERMAL_CONTROLLER_MAPPING, {"NONE"}, {"NONE"}, ), ( nvml.ThermalTarget, - _device.ThermalTarget, + system_typing.ThermalTarget, _device._THERMAL_TARGET_MAPPING, # UNKNOWN is a fallback sentinel; handled by .get() {"UNKNOWN"}, @@ -197,35 +200,35 @@ ), ( nvml.SystemEventType, - _system_events.SystemEventType, + system_typing.SystemEventType, _system_events._SYSTEM_EVENT_TYPE_MAPPING, set(), set(), ), ( nvml.AffinityScope, - _device.AffinityScope, + system_typing.AffinityScope, _device._AFFINITY_SCOPE_MAPPING, set(), set(), ), ( nvml.GpuP2PCapsIndex, - _device.GpuP2PCapsIndex, + system_typing.GpuP2PCapsIndex, _device._GPU_P2P_CAPS_INDEX_MAPPING, set(), set(), ), ( nvml.GpuTopologyLevel, - _device.GpuTopologyLevel, + system_typing.GpuTopologyLevel, _device._GPU_TOPOLOGY_LEVEL_MAPPING, set(), set(), ), ( nvml.ClockId, - _device.ClockId, + system_typing.ClockId, _device._CLOCK_ID_MAPPING, # APP_CLOCK_TARGET and APP_CLOCK_DEFAULT are deprecated; COUNT is a sentinel {"APP_CLOCK_TARGET", "APP_CLOCK_DEFAULT", "COUNT"}, @@ -233,7 +236,7 @@ ), ( nvml.ClockType, - _device.ClockType, + system_typing.ClockType, _device._CLOCK_TYPE_MAPPING, # COUNT is a sentinel {"CLOCK_COUNT"}, @@ -241,7 +244,7 @@ ), ( nvml.InforomObject, - _device.InforomObject, + system_typing.InforomObject, _device._INFOROM_OBJECT_MAPPING, # COUNT is a sentinel {"INFOROM_COUNT"}, @@ -249,7 +252,7 @@ ), ( nvml.TemperatureThresholds, - _device.TemperatureThresholds, + system_typing.TemperatureThresholds, _device._TEMPERATURE_THRESHOLD_MAPPING, # COUNT is a sentinel {"TEMPERATURE_THRESHOLD_COUNT"}, From a7ec81ce177ffe60b6ba148a2fe76b423e27c31a Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 6 May 2026 09:58:08 -0400 Subject: [PATCH 073/244] [skip-ci] Add note about minimum cuda_bindings version (#2035) --- cuda_core/docs/source/api.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 582f2140903..99a4dca6e77 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -245,6 +245,9 @@ execution. CUDA system information and NVIDIA Management Library (NVML) ------------------------------------------------------------ +.. note:: + ``cuda.core.system`` support requires ``cuda_bindings`` 12.9.6 or later, or 13.2.0 or later. + Basic functions ``````````````` From fec00bfd79bc93160c6f3c25c57e1d8b847e0590 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Wed, 6 May 2026 06:59:08 -0700 Subject: [PATCH 074/244] Add backfill support to SMResourceOptions and fix topology-sensitive tests (#2031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `backfill` field to SMResourceOptions. When True, sets CU_DEV_SM_RESOURCE_GROUP_BACKFILL on each group's flags, allowing the driver to relax the co-scheduling constraint when assigning SMs to groups. This enables requesting arbitrary aligned SM counts that the driver would otherwise reject due to hardware topology constraints. Fix test_two_groups and related tests: replace _aligned_half (which computed half the SMs rounded to alignment — not always a valid partition on all GPU topologies) with _safe_two_group_count (uses min_partition_size, always valid). Add dedicated test_two_groups_backfill that exercises the aggressive even-split with backfill=True. Fixes #2025. Co-authored-by: Claude Opus 4.6 (1M context) --- cuda_core/cuda/core/_device_resources.pyx | 19 +++++++- cuda_core/tests/test_green_context.py | 56 +++++++++++++---------- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/cuda_core/cuda/core/_device_resources.pyx b/cuda_core/cuda/core/_device_resources.pyx index e4851a73a41..40c0a874d05 100644 --- a/cuda_core/cuda/core/_device_resources.pyx +++ b/cuda_core/cuda/core/_device_resources.pyx @@ -106,11 +106,18 @@ cdef class SMResourceOptions: Preferred co-scheduled SM count; the driver tries to satisfy this but may fall back to ``coscheduled_sm_count``. (Default to ``None``) + backfill : bool or Sequence[bool], optional + If ``True``, allow the driver to relax the co-scheduling + constraint when assigning SMs. This enables requesting + arbitrary aligned SM counts that the driver would otherwise + reject due to hardware topology constraints. + (Default to ``False``) """ count: int | SequenceABC | None = None coscheduled_sm_count: int | SequenceABC | None = None preferred_coscheduled_sm_count: int | SequenceABC | None = None + backfill: bool | SequenceABC = False @dataclass @@ -172,6 +179,12 @@ cdef inline int _resolve_group_count(SMResourceOptions options) except?-1: n_groups, count_is_scalar, ) + _validate_split_field_length( + options.backfill, + "backfill", + n_groups, + count_is_scalar, + ) return n_groups @@ -243,6 +256,7 @@ IF CUDA_CORE_BUILD_MAJOR >= 13: cdef list counts = _broadcast_field(options.count, n_groups) cdef list coscheduled = _broadcast_field(options.coscheduled_sm_count, n_groups) cdef list preferred = _broadcast_field(options.preferred_coscheduled_sm_count, n_groups) + cdef list backfills = _broadcast_field(options.backfill, n_groups) cdef int i for i in range(n_groups): @@ -252,7 +266,10 @@ IF CUDA_CORE_BUILD_MAJOR >= 13: params[i].coscheduledSmCount = (coscheduled[i]) if preferred[i] is not None: params[i].preferredCoscheduledSmCount = (preferred[i]) - params[i].flags = 0 + params[i].flags = ( + cydriver.CUdevSmResourceGroup_flags.CU_DEV_SM_RESOURCE_GROUP_BACKFILL + if backfills[i] else 0 + ) return 0 diff --git a/cuda_core/tests/test_green_context.py b/cuda_core/tests/test_green_context.py index 8eb32f7c1aa..4d89c8d76f7 100644 --- a/cuda_core/tests/test_green_context.py +++ b/cuda_core/tests/test_green_context.py @@ -82,11 +82,16 @@ def fill_kernel(init_cuda): return mod.get_kernel("fill") -def _aligned_half(sm): - """Compute half the SM count, rounded down to min_partition_size alignment.""" +def _safe_two_group_count(sm): + """Return a safe per-group SM count for a 2-group split. + + Uses min_partition_size which is always a valid split size regardless + of hardware topology. Returns None if the device doesn't have enough SMs. + """ min_size = sm.min_partition_size - half = (sm.sm_count // 2 // min_size) * min_size - return half + if sm.sm_count < 2 * min_size: + return None + return min_size @contextlib.contextmanager @@ -238,30 +243,33 @@ def test_discovery_respects_alignment(self, sm_resource): assert groups[0].sm_count % sm_resource.coscheduled_alignment == 0 def test_two_groups(self, sm_resource): - """Two-group split with explicit aligned counts.""" - half = _aligned_half(sm_resource) - if half < sm_resource.min_partition_size: + """Two-group split with min_partition_size (always topology-safe).""" + count = _safe_two_group_count(sm_resource) + if count is None: pytest.skip("Not enough SMs for a 2-group split") - groups, rem = sm_resource.split(SMResourceOptions(count=(half, half))) + groups, rem = sm_resource.split(SMResourceOptions(count=(count, count))) assert len(groups) == 2 - assert groups[0].sm_count > 0 - assert groups[1].sm_count > 0 + assert groups[0].sm_count >= count + assert groups[1].sm_count >= count total = groups[0].sm_count + groups[1].sm_count + rem.sm_count assert total <= sm_resource.sm_count - def test_two_groups_each_meets_request(self, sm_resource): - min_size = sm_resource.min_partition_size - half = _aligned_half(sm_resource) - if half < min_size: - pytest.skip("Not enough SMs for a 2-group split") + def test_two_groups_backfill(self, sm_resource): + """Two-group split with backfill allows larger partitions.""" + align = sm_resource.coscheduled_alignment + if align == 0: + align = sm_resource.min_partition_size + half = (sm_resource.sm_count // 2 // align) * align + if half < sm_resource.min_partition_size: + pytest.skip("Not enough SMs for a 2-group backfill split") - groups, _ = sm_resource.split(SMResourceOptions(count=(min_size, min_size))) + groups, rem = sm_resource.split(SMResourceOptions(count=(half, half), backfill=True)) assert len(groups) == 2 - assert groups[0].sm_count >= min_size - assert groups[1].sm_count >= min_size + assert groups[0].sm_count >= half + assert groups[1].sm_count >= half def test_dry_run_matches_real(self, sm_resource): """Dry-run reports the same SM counts as a real split.""" @@ -352,11 +360,11 @@ def test_green_ctx_sm_resources(self, green_ctx, sm_resource): def test_green_ctx_resources_reflect_partition(self, init_cuda, sm_resource): """Two green contexts should have disjoint SM partitions.""" - half = _aligned_half(sm_resource) - if half < sm_resource.min_partition_size: + count = _safe_two_group_count(sm_resource) + if count is None: pytest.skip("Not enough SMs for a 2-group split") - groups, _ = sm_resource.split(SMResourceOptions(count=(half, half))) + groups, _ = sm_resource.split(SMResourceOptions(count=(count, count))) ctx_a = ctx_b = None try: @@ -425,11 +433,11 @@ def test_launch_and_verify(self, init_cuda, green_ctx, fill_kernel): def test_two_green_contexts_independent(self, init_cuda, sm_resource, fill_kernel): """Two SM groups -> two green contexts -> two independent kernels.""" dev = init_cuda - half = _aligned_half(sm_resource) - if half < sm_resource.min_partition_size: + count = _safe_two_group_count(sm_resource) + if count is None: pytest.skip("Not enough SMs for a 2-group split") - groups, _ = sm_resource.split(SMResourceOptions(count=(half, half))) + groups, _ = sm_resource.split(SMResourceOptions(count=(count, count))) assert len(groups) == 2 ctx_a = ctx_b = None From db4e1c63a6dbfa149be7b6b61028f209fef9f39e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 6 May 2026 07:00:08 -0700 Subject: [PATCH 075/244] Move cuda.core Windows stub-lib generation into build_ext. (#2033) Reuse setuptools' initialized MSVC compiler when linking _tensor_bridge so Windows source builds do not need separate toolchain discovery in build_hooks.py. Co-authored-by: Cursor --- cuda_core/build_hooks.py | 25 +--------- cuda_core/cuda/core/_include/aoti_shim.def | 2 +- cuda_core/cuda/core/_include/aoti_shim.h | 8 ++-- cuda_core/setup.py | 53 ++++++++++++++++++++++ 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index a94120616e0..812c5a76a1d 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -11,7 +11,6 @@ import glob import os import re -import subprocess import sys import tempfile import zipfile @@ -185,28 +184,6 @@ def get_sources(mod_name): # related to free-threading builds. extra_compile_args += ["-DCYTHON_TRACE_NOGIL=1", "-DCYTHON_USE_SYS_MONITORING=0"] - # On Windows, _tensor_bridge.pyx needs a stub import library so the MSVC - # linker can resolve the AOTI symbols (they live in torch_cpu.dll at - # runtime). We generate the .lib from a .def file at build time. - # Note: aoti_torch_get_current_cuda_stream lives in torch_cuda.dll and - # is resolved lazily at runtime (not via the stub lib) — see - # _tensor_bridge.pyx. - _aoti_extra_link_args = [] - if sys.platform == "win32": - _def_file = os.path.join("cuda", "core", "_include", "aoti_shim.def") - _lib_file = os.path.join("build", "aoti_shim.lib") - os.makedirs("build", exist_ok=True) - subprocess.check_call( # noqa: S603 - ["lib", f"/DEF:{_def_file}", f"/OUT:{_lib_file}", "/MACHINE:X64"], # noqa: S607 - stdout=subprocess.DEVNULL, - ) - _aoti_extra_link_args = [_lib_file] - - def get_extra_link_args(mod_name): - if mod_name == "_tensor_bridge" and _aoti_extra_link_args: - return extra_link_args + _aoti_extra_link_args - return extra_link_args - ext_modules = tuple( Extension( f"cuda.core.{mod.replace(os.path.sep, '.')}", @@ -218,7 +195,7 @@ def get_extra_link_args(mod_name): + all_include_dirs, language="c++", extra_compile_args=extra_compile_args, - extra_link_args=get_extra_link_args(mod), + extra_link_args=extra_link_args, ) for mod in module_names() ) diff --git a/cuda_core/cuda/core/_include/aoti_shim.def b/cuda_core/cuda/core/_include/aoti_shim.def index e21097bd25e..3cd1e31aeac 100644 --- a/cuda_core/cuda/core/_include/aoti_shim.def +++ b/cuda_core/cuda/core/_include/aoti_shim.def @@ -4,7 +4,7 @@ ; At runtime the symbols resolve from torch_cpu.dll (loaded by 'import torch'). ; ; IMPORTANT: Keep this export list in sync with the AOTI_SHIM_API declarations -; in aoti_shim.h. build_hooks.py turns this file into the stub import library +; in aoti_shim.h. setup.py turns this file into the stub import library ; that MSVC uses to link _tensor_bridge, so any added/removed/renamed AOTI ; symbol must be updated in both files. LIBRARY torch_cpu.dll diff --git a/cuda_core/cuda/core/_include/aoti_shim.h b/cuda_core/cuda/core/_include/aoti_shim.h index 464d27de46c..bf8894d940a 100644 --- a/cuda_core/cuda/core/_include/aoti_shim.h +++ b/cuda_core/cuda/core/_include/aoti_shim.h @@ -52,10 +52,10 @@ typedef struct AtenTensorOpaque* AtenTensorHandle; /* * IMPORTANT: Keep the AOTI_SHIM_API declaration list below in sync with - * aoti_shim.def. On Windows, build_hooks.py turns that .def file into the - * stub import library that MSVC needs to link _tensor_bridge without making - * PyTorch a build-time dependency. If you add, remove, or rename an - * imported AOTI symbol here, update aoti_shim.def in the same change. + * aoti_shim.def. On Windows, setup.py generates that stub import library + * during build_ext so MSVC can link _tensor_bridge without making PyTorch a + * build-time dependency. If you add, remove, or rename an imported AOTI + * symbol here, update aoti_shim.def in the same change. * * Exception: aoti_torch_get_current_cuda_stream lives in torch_cuda (not * torch_cpu) and is resolved lazily at runtime — see _tensor_bridge.pyx. diff --git a/cuda_core/setup.py b/cuda_core/setup.py index 71d1548755e..bde1fe22fee 100644 --- a/cuda_core/setup.py +++ b/cuda_core/setup.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 import os +from pathlib import Path import build_hooks # our build backend from setuptools import setup @@ -11,11 +12,63 @@ nthreads = int(os.environ.get("CUDA_PYTHON_PARALLEL_LEVEL", os.cpu_count() // 2)) coverage_mode = bool(int(os.environ.get("CUDA_PYTHON_COVERAGE", "0"))) +_ROOT_DIR = Path(__file__).resolve().parent +_AOTI_SHIM_DEF_FILE = _ROOT_DIR / "cuda" / "core" / "_include" / "aoti_shim.def" +_AOTI_SHIM_LIB_FILE = _ROOT_DIR / "build" / "aoti_shim.lib" +_TENSOR_BRIDGE_EXT_NAME = "cuda.core._tensor_bridge" + + +def _ensure_compiler_initialized(compiler, plat_name): + initialize = getattr(compiler, "initialize", None) + if callable(initialize) and not getattr(compiler, "initialized", False): + if plat_name is None: + initialize() + else: + initialize(plat_name) + + +def _build_aoti_shim_lib(compiler): + # Reuse setuptools' initialized MSVC compiler instead of rediscovering + # lib.exe separately in the build backend. + lib_exe = getattr(compiler, "lib", None) + if not lib_exe: + raise RuntimeError("MSVC compiler did not expose lib.exe after initialization.") + + _AOTI_SHIM_LIB_FILE.parent.mkdir(exist_ok=True) + compiler.spawn( + [ + lib_exe, + f"/DEF:{_AOTI_SHIM_DEF_FILE}", + f"/OUT:{_AOTI_SHIM_LIB_FILE}", + "/MACHINE:X64", + ] + ) + return str(_AOTI_SHIM_LIB_FILE) class build_ext(_build_ext): # noqa: N801 + def _configure_windows_tensor_bridge(self): + if os.name != "nt" or getattr(self.compiler, "compiler_type", None) != "msvc": + return + + # _tensor_bridge imports AOTI symbols from torch_cpu.dll, which on + # Windows requires a stub import library for the MSVC linker. + for ext in self.extensions: + if ext.name != _TENSOR_BRIDGE_EXT_NAME: + continue + + _ensure_compiler_initialized(self.compiler, self.plat_name) + shim_lib = _build_aoti_shim_lib(self.compiler) + link_args = list(ext.extra_link_args or []) + if shim_lib not in link_args: + ext.extra_link_args = [*link_args, shim_lib] + return + + raise RuntimeError(f"Failed to find extension {_TENSOR_BRIDGE_EXT_NAME!r} for Windows build.") + def build_extensions(self): self.parallel = nthreads + self._configure_windows_tensor_bridge() super().build_extensions() From 598a966ccb791298e9799f110a07b79ea87e6b43 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 6 May 2026 11:14:17 -0400 Subject: [PATCH 076/244] Fix #2027: Remove StridedMemoryView etc. from public API (#2028) * Fix #2027: Remove StridedMemoryView etc. from public API * Add regression tests * Update cuda_core/tests/test_graphics.py Co-authored-by: Leo Fang * Update cuda_core/tests/test_tensor_map.py Co-authored-by: Leo Fang * Move to utils * Add release note * Fix import sort * Fix renames --------- Co-authored-by: Leo Fang --- cuda_core/cuda/core/__init__.py | 4 ---- cuda_core/docs/source/api.rst | 6 ++--- cuda_core/docs/source/release/1.0.0-notes.rst | 5 ++++ cuda_core/examples/tma_tensor_map.py | 2 +- cuda_core/tests/test_graphics.py | 2 +- cuda_core/tests/test_memory.py | 24 +++++++++++++++++++ cuda_core/tests/test_tensor_map.py | 2 +- 7 files changed, 34 insertions(+), 11 deletions(-) diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index d4042aefcfb..56027d41fe3 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -57,10 +57,6 @@ def _import_versioned_module(): VirtualMemoryResource, VirtualMemoryResourceOptions, ) -from cuda.core._memoryview import ( - StridedMemoryView, - args_viewable_as_strided_memory, -) from cuda.core._module import Kernel, ObjectCode from cuda.core._program import Program, ProgramOptions from cuda.core._stream import ( diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index 99a4dca6e77..41ff5f179ed 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -282,16 +282,14 @@ Types system.Device system.NvlinkInfo -.. module:: cuda.core.utils - Utility functions ----------------- .. autosummary:: :toctree: generated/ - args_viewable_as_strided_memory + utils.args_viewable_as_strided_memory :template: autosummary/cyclass.rst - StridedMemoryView + utils.StridedMemoryView diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst index 7f0ced8c10b..a0f1de1a121 100644 --- a/cuda_core/docs/source/release/1.0.0-notes.rst +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -125,6 +125,11 @@ Breaking changes - :obj:`cuda.core.typing.DevicePointerT` -> :obj:`cuda.core.typing.DevicePointerType` - :obj:`cuda.core.typing.IsStreamT` -> :obj:`cuda.core.typing.IsStreamType` +- :func:`args_viewable_as_strided_memory` and :class:`StridedMemoryView` are now + longer at the top-level in :mod:`cuda.core`. They are available publicly from the + :mod:`cuda.core.utils` module. + (`#2028 `__) + Fixes and enhancements ----------------------- diff --git a/cuda_core/examples/tma_tensor_map.py b/cuda_core/examples/tma_tensor_map.py index 879632ad90d..8c048d4a15d 100644 --- a/cuda_core/examples/tma_tensor_map.py +++ b/cuda_core/examples/tma_tensor_map.py @@ -37,9 +37,9 @@ LaunchConfig, Program, ProgramOptions, - StridedMemoryView, launch, ) +from cuda.core.utils import StridedMemoryView from cuda.pathfinder import get_cuda_path_or_home # --------------------------------------------------------------------------- diff --git a/cuda_core/tests/test_graphics.py b/cuda_core/tests/test_graphics.py index 12184815b02..4358a0c7b38 100644 --- a/cuda_core/tests/test_graphics.py +++ b/cuda_core/tests/test_graphics.py @@ -17,8 +17,8 @@ Buffer, Device, GraphicsResource, - StridedMemoryView, ) +from cuda.core.utils import StridedMemoryView # --------------------------------------------------------------------------- # GL context + buffer helpers diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index 3ae6960fc29..d6ad6ac3320 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -1603,3 +1603,27 @@ def test_memory_resource_alloc_zero_bytes(init_cuda, memory_resource_factory): assert buffer.handle >= 0 assert buffer.size == 0 assert buffer.device_id == mr.device_id + + +def test_strided_memory_view_not_in_top_level_namespace(): + # Regression test for issue #2027: StridedMemoryView and + # args_viewable_as_strided_memory must only be exposed via + # cuda.core.utils, not the top-level cuda.core namespace. + import cuda.core + import cuda.core.utils + + assert not hasattr(cuda.core, "StridedMemoryView") + assert not hasattr(cuda.core, "args_viewable_as_strided_memory") + + assert hasattr(cuda.core.utils, "StridedMemoryView") + assert hasattr(cuda.core.utils, "args_viewable_as_strided_memory") + + +def test_top_level_namespace_excludes_known_leaks(): + # Hardening test: lock the public top-level namespace against + # accidental re-introduction of known-leaked symbols. + import cuda.core + + public = {n for n in dir(cuda.core) if not n.startswith("_")} + leaked = {"StridedMemoryView", "args_viewable_as_strided_memory"} + assert not (public & leaked) diff --git a/cuda_core/tests/test_tensor_map.py b/cuda_core/tests/test_tensor_map.py index 9ca8790d2b8..2596a9daf82 100644 --- a/cuda_core/tests/test_tensor_map.py +++ b/cuda_core/tests/test_tensor_map.py @@ -8,7 +8,6 @@ from cuda.core import ( Device, ManagedMemoryResourceOptions, - StridedMemoryView, TensorMapDescriptor, system, ) @@ -23,6 +22,7 @@ TensorMapSwizzle, _require_view_device, ) +from cuda.core.utils import StridedMemoryView @pytest.fixture From 50c19d02389219719937ac555a020959afb82f0e Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Wed, 6 May 2026 12:33:56 -0700 Subject: [PATCH 077/244] cuda.core: require explicit stream for stream-scheduling APIs (#2020) * cuda.core: require explicit stream for stream-scheduling APIs (#2001) Removes the implicit fallback to default_stream() (or NULL) on APIs that schedule work on a stream. `stream` is now a required keyword-only argument; `Stream_accept(None)` raises TypeError. Affected APIs: - MemoryResource.allocate / deallocate and overrides on DeviceMemoryResource, PinnedMemoryResource, ManagedMemoryResource, LegacyPinnedMemoryResource, GraphMemoryResource. - Device.allocate. - GraphicsResource.map. - KernelOccupancy.max_potential_cluster_size / max_active_clusters. - Graph.launch (stream was previously positional). Stream_accept is promoted to cpdef so the pure-Python legacy/sync resources can call it. Also fixes a latent bug uncovered while doing this: the C++ MR deallocation callback in Buffer's GC path was calling `mr.deallocate(ptr, size, stream)` positionally, which would fail with the new keyword-only signature for every garbage-collected DeviceMemoryResource/GraphMemoryResource buffer. Switched to `stream=stream`. VirtualMemoryResource is exempt because cuMemCreate / cuMemMap are synchronous and not stream-ordered; it now accepts (and validates) an optional stream instead of rejecting any non-None value. Buffer.from_ipc_descriptor is also exempt: stream there only seeds the deallocation stream stored in the handle (no work is scheduled), the same shape as Buffer.close(stream=None). Tests, examples, and the v1.0.0 release note are updated accordingly. Co-authored-by: Cursor * cuda.core: also require explicit stream for Buffer.from_ipc_descriptor (#2001) Buffer.from_ipc_descriptor previously fell back to default_stream() when stream=None. That fallback is exactly the implicit-fallback pattern issue #2001 removes (the chosen stream depends on global state, not the call site), so it does not belong in the same exemption category as Buffer.close(stream=None) / GraphicsResource.unmap(stream=None) which genuinely reuse an existing stream. stream is now keyword-only and required. Internal validation goes through Stream_accept like the other tightened APIs. Tests and the v1.0.0 release note updated accordingly. Co-authored-by: Cursor * cuda.core: align deallocate signatures and revert Graph.launch (#2001) - Make `deallocate` keyword-only on the synchronous resources (`LegacyPinnedMemoryResource`, `_SynchronousMemoryResource`, `VirtualMemoryResource`) so every memory-resource API obeys the kw-only rule, with `stream=None` as the default since these resources do not actually use the stream. - Revert `Graph.launch` to take `stream` positionally. It is the same shape as the kernel `launch(stream, config, kernel, *args)` API (already exempt in the issue) and shouldn't be the odd one out. - Tighten `VirtualMemoryResource.deallocate` docstring to match `allocate`. - Mark unused lambda args in `test_pass_object` as `_stream` to silence ARG005. Co-authored-by: Cursor * cuda.core: tighten test mocks and add Stream_accept(None) test (#2001) Review follow-ups: - Tighten the test-only `MemoryResource` subclasses (`DummyDeviceMemoryResource`, `DummyHostMemoryResource`, `DummyPinnedMemoryResource`, `DummyUnifiedMemoryResource`, `TrackingMR`, `StreamCaptureMR`) to match the new public API: `allocate(self, size, *, stream)` and `deallocate(self, ptr, size, *, stream)` with no default. Previously the mocks accepted `stream=None` positionally, which let tests bypass the new explicit-stream policy. - Update the affected helper functions and call sites in `test_memory.py` to pass `stream=device.default_stream` explicitly. Fix the `super().deallocate(ptr, size, stream)` positional call in `test_mr_deallocate_receives_stream` to use `stream=stream`. - Update `helpers/buffers.py` similarly (`make_scratch_buffer`, `PatternGen`). - Add a direct test for the centralized `Stream_accept(None)` -> `TypeError` behavior in `test_stream.py`. - Tighten the release note for `Buffer.from_ipc_descriptor`: lead with the removal of the silent fallback to the default stream rather than the positional-to-keyword shift. Co-authored-by: Cursor * cuda.core: fix Buffer pickle path broken by kw-only stream (#2001) `Buffer._reduce_helper` (the pickle/unpickle factory) previously called `Buffer.from_ipc_descriptor(mr, ipc_descriptor)` without a stream and relied on the implicit `default_stream()` fallback inside `Buffer_from_ipc_descriptor`. Making `from_ipc_descriptor`'s stream a required keyword-only argument broke this code path, causing every multiprocessing IPC test that pickles a `Buffer` (test_send_buffers, test_memory_ipc, test_event_ipc, test_serialize, test_workerpool, ...) to fail in the child process with: TypeError: from_ipc_descriptor() needs keyword-only argument stream Fix: pass `default_stream()` explicitly from `_reduce_helper`. The parent process's stream isn't portable across processes, so the pickle path cannot thread an explicit stream through. The receiver can still override the deallocation stream via `buffer.close(stream=...)`. The user-facing rule still holds: callers of `Buffer.from_ipc_descriptor` must pass an explicit stream. Co-authored-by: Cursor * cuda.core: relax kw-only TypeError regex for Cython funcs (#2001) Cython-generated functions raise "FUNC() needs keyword-only argument stream" while pure-Python functions raise "FUNC() missing 1 required keyword-only argument: 'stream'" The new tests for `Kernel.occupancy.max_potential_cluster_size`, `Kernel.occupancy.max_active_clusters`, and `GraphicsResource.map` were matching only the CPython phrasing and failed against the Cython forms. Loosen the regex to `keyword-only argument`, which matches both. Co-authored-by: Cursor * cuda.core: review fixes for #2001 (graph_update.py + _legacy.py) - examples/graph_update.py: use the dedicated `stream` created at the top of the example for the pinned allocation, instead of `device.default_stream`. Better model for users (Leo). - _memory/_legacy.py: route the user-supplied `stream` through `Stream_accept` in `LegacyPinnedMemoryResource.deallocate` and `_SynchronousMemoryResource.deallocate` so a non-`Stream` argument raises the clean `TypeError` from `Stream_accept` instead of an `AttributeError` from `.sync()` (matches the validation the matching `allocate` methods already do). Co-authored-by: Cursor * cuda.core: drop unused stream= kwargs from sync MR call sites (#2001) Synchronous memory resources (`LegacyPinnedMemoryResource`, `_SynchronousMemoryResource`, the various test mocks `DummyDeviceMR`, `DummyHostMR`, `DummyPinnedMR`, `DummyUnifiedMR`, `NullMemoryResource`, `TrackingMR`, `StreamCaptureMR`) take a stream argument purely for interface conformance with stream-ordered MRs but never use it. Forcing every caller to manufacture a stream just to discard it adds ceremony and a misleading model. Switch these MRs' allocate/deallocate signatures to keyword-only `stream=None` (validated via `Stream_accept` when provided), and drop the now-unused `stream=...` kwargs from ~35 call sites across examples, tests, and helpers. Also drop the `device` parameter from `buffer_initialization` and `buffer_close` test helpers (no longer needed) and remove leftover Device-setup boilerplate from the NullMemoryResource dlpack-failure tests. The user-facing rule is unchanged for the genuinely stream-ordered APIs (`DeviceMemoryResource`, `PinnedMemoryResource`, `ManagedMemoryResource`, `GraphMemoryResource`, `Device.allocate`, `Buffer.from_ipc_descriptor`, etc.): stream remains required and keyword-only. The release note is updated to reflect the sync-MR exemption (folding `LegacyPinnedMemoryResource` in alongside `VirtualMemoryResource`). Co-authored-by: Cursor * cuda.core: fix C++ teardown leak when buffer has no attached stream (#2001) Issue: the C++ ``shared_ptr`` deleter for a buffer's device-pointer handle invokes ``MemoryResource.deallocate`` via ``_mr_dealloc_callback``. The handle's deallocation stream is set separately via ``set_deallocation_stream``; if it was never set (e.g. buffers minted via ``Buffer.from_handle(ptr, size, mr=mr)`` from DLPack import, IPC import, or third-party adapters), the callback would pass ``stream=None`` to ``mr.deallocate``. After the strict-stream changes for #2001, the stream-ordered MR overrides reject ``stream=None`` via ``Stream_accept`` and raise ``TypeError``. The ``noexcept`` callback catches the exception, prints a warning to stderr, and returns -- silently **leaking** the underlying CUDA allocation (and any associated IPC handles). Fix: when ``h_stream`` is empty in ``_mr_dealloc_callback``, fall back to ``default_stream()`` instead of ``None``. The C++ teardown path is the unique legitimate "no-stream-context" caller (no Python frame from which to obtain a stream), so this is the one place where an implicit default-stream fallback is necessary; everywhere else the policy remains "stream is required and must be passed explicitly". Add ``test_mr_dealloc_callback_falls_back_to_default_stream`` covering the regression: a strict stream-ordered mock MR is used to back a ``Buffer.from_handle`` (no attached stream), and the test asserts that ``deallocate`` is invoked with the default stream rather than failing with ``TypeError`` and leaking. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- cuda_core/cuda/core/_context.pxd | 2 +- cuda_core/cuda/core/_device.pyx | 13 +- cuda_core/cuda/core/_graphics.pyx | 13 +- cuda_core/cuda/core/_layout.pyx | 2 +- cuda_core/cuda/core/_memory/_buffer.pyx | 67 ++++++--- .../core/_memory/_graph_memory_resource.pyx | 14 +- cuda_core/cuda/core/_memory/_ipc.pyx | 8 +- cuda_core/cuda/core/_memory/_legacy.py | 43 ++++-- cuda_core/cuda/core/_memory/_memory_pool.pyx | 29 ++-- .../core/_memory/_virtual_memory_resource.py | 27 +++- cuda_core/cuda/core/_module.pyx | 30 ++-- cuda_core/cuda/core/_stream.pxd | 2 +- cuda_core/cuda/core/_stream.pyx | 11 +- cuda_core/cuda/core/graph/_graph_builder.pyx | 2 +- cuda_core/docs/source/release/1.0.0-notes.rst | 22 +++ cuda_core/examples/memory_ops.py | 2 +- cuda_core/tests/graph/test_device_launch.py | 4 +- .../tests/graph/test_graph_memory_resource.py | 2 +- cuda_core/tests/helpers/buffers.py | 12 +- cuda_core/tests/memory_ipc/test_errors.py | 12 +- .../memory_ipc/test_ipc_duplicate_import.py | 6 +- cuda_core/tests/memory_ipc/test_leaks.py | 12 +- cuda_core/tests/memory_ipc/test_memory_ipc.py | 16 +-- .../tests/memory_ipc/test_peer_access.py | 2 +- .../tests/memory_ipc/test_send_buffers.py | 4 +- cuda_core/tests/memory_ipc/test_serialize.py | 10 +- cuda_core/tests/memory_ipc/test_workerpool.py | 8 +- cuda_core/tests/test_device.py | 4 +- cuda_core/tests/test_graphics.py | 12 +- cuda_core/tests/test_memory.py | 132 +++++++++++++----- cuda_core/tests/test_memory_peer_access.py | 4 +- cuda_core/tests/test_module.py | 12 +- cuda_core/tests/test_object_protocols.py | 6 +- cuda_core/tests/test_stream.py | 9 +- cuda_core/tests/test_tensor_map.py | 78 +++++------ cuda_core/tests/test_utils.py | 14 +- 36 files changed, 396 insertions(+), 250 deletions(-) diff --git a/cuda_core/cuda/core/_context.pxd b/cuda_core/cuda/core/_context.pxd index 92fa5700a06..b0edf5a0674 100644 --- a/cuda_core/cuda/core/_context.pxd +++ b/cuda_core/cuda/core/_context.pxd @@ -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 diff --git a/cuda_core/cuda/core/_device.pyx b/cuda_core/cuda/core/_device.pyx index 233ce6de791..67255506a2d 100644 --- a/cuda_core/cuda/core/_device.pyx +++ b/cuda_core/cuda/core/_device.pyx @@ -1394,14 +1394,12 @@ class Device: cdef Context ctx = self._context return cyEvent._init(cyEvent, self._device_id, ctx._h_context, options, True) - def allocate(self, size, stream: Stream | GraphBuilder | None = None) -> Buffer: + def allocate(self, size, *, stream: Stream | GraphBuilder) -> Buffer: """Allocate device memory from a specified stream. Allocates device memory of `size` bytes on the specified `stream` using the memory resource currently associated with this Device. - Parameter `stream` is optional, using a default stream by default. - Note ---- Device must be initialized. @@ -1410,9 +1408,10 @@ class Device: ---------- size : int Number of bytes to allocate. - stream : :obj:`~_stream.Stream`, optional - The stream establishing the stream ordering semantic. - Default value of `None` uses default stream. + stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder` + Keyword-only. The stream establishing the stream ordering semantic. + Must be passed explicitly; pass ``self.default_stream`` to use + the default stream. Returns ------- @@ -1421,7 +1420,7 @@ class Device: """ self._check_context_initialized() - return self.memory_resource.allocate(size, stream) + return self.memory_resource.allocate(size, stream=stream) def sync(self): """Synchronize the device. diff --git a/cuda_core/cuda/core/_graphics.pyx b/cuda_core/cuda/core/_graphics.pyx index fc053da8cb8..a377589dfc2 100644 --- a/cuda_core/cuda/core/_graphics.pyx +++ b/cuda_core/cuda/core/_graphics.pyx @@ -12,7 +12,7 @@ from cuda.core._resource_handles cimport ( as_intptr, ) from cuda.core._memory._buffer cimport Buffer, Buffer_from_deviceptr_handle -from cuda.core._stream cimport Stream, Stream_accept, default_stream +from cuda.core._stream cimport Stream, Stream_accept from cuda.core._utils.cuda_utils cimport HANDLE_RETURN __all__ = ['GraphicsResource'] @@ -206,7 +206,7 @@ cdef class GraphicsResource: return None return self._mapped_buffer - def map(self, *, stream: Stream | None = None) -> Buffer: + def map(self, *, stream: Stream) -> Buffer: """Map this graphics resource for CUDA access. After mapping, a CUDA device pointer into the underlying graphics @@ -220,9 +220,10 @@ cdef class GraphicsResource: Parameters ---------- - stream : :class:`~cuda.core.Stream`, optional - The CUDA stream on which to perform the mapping. If ``None``, - the current default stream is used. + stream : :class:`~cuda.core.Stream` + Keyword-only. The CUDA stream on which to perform the mapping. + Must be passed explicitly; pass ``device.default_stream`` to use + the default stream. Returns ------- @@ -248,7 +249,7 @@ cdef class GraphicsResource: if self._get_mapped_buffer() is not None: raise RuntimeError("GraphicsResource is already mapped") - s_obj = default_stream() if stream is None else Stream_accept(stream) + s_obj = Stream_accept(stream) raw = as_cu(self._handle) cy_stream = as_cu(s_obj._h_stream) with nogil: diff --git a/cuda_core/cuda/core/_layout.pyx b/cuda_core/cuda/core/_layout.pyx index 3c9392430b6..796a6243fd4 100644 --- a/cuda_core/cuda/core/_layout.pyx +++ b/cuda_core/cuda/core/_layout.pyx @@ -460,7 +460,7 @@ cdef class _StridedLayout: required_size = layout.required_size_in_bytes() # allocate the memory on the device device.set_current() - mem = device.allocate(required_size) + mem = device.allocate(required_size, stream=device.default_stream) # create a view on the newly allocated device memory b_view = StridedMemoryView.from_buffer(mem, layout, a_view.dtype) return b_view diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index dd12ae005d4..5d3bdbb873c 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -24,7 +24,7 @@ from cuda.core._resource_handles cimport ( ) from cuda.core.typing import DevicePointerType -from cuda.core._stream cimport Stream, Stream_accept +from cuda.core._stream cimport Stream, Stream_accept, default_stream from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value import sys @@ -49,12 +49,24 @@ cdef void _mr_dealloc_callback( size_t size, const StreamHandle& h_stream, ) noexcept: - """Called by the C++ deleter to deallocate via MemoryResource.deallocate.""" + """Called by the C++ deleter to deallocate via MemoryResource.deallocate. + + This is the C++ teardown path: there is no Python caller frame from + which to obtain a stream. If the device-pointer handle was created + without ``set_deallocation_stream`` being called (e.g. buffers minted + via ``Buffer.from_handle(ptr, size, mr=mr)`` from DLPack import, + third-party adapters, or other foreign sources), ``h_stream`` is + empty here. Stream-ordered MR ``deallocate`` overrides reject + ``stream=None`` (issue #2001), so without a fallback the destructor + would print a warning and leak the allocation. Fall back to the + legacy/per-thread default stream so the free still happens; this is + the unique exception to the "no implicit default-stream fallback" + policy because the teardown has no other source of truth. + """ + cdef Stream stream try: - stream = None - if h_stream: - stream = Stream._from_handle(Stream, h_stream) - mr.deallocate(int(ptr), size, stream) + stream = Stream._from_handle(Stream, h_stream) if h_stream else default_stream() + mr.deallocate(int(ptr), size, stream=stream) except Exception as exc: print(f"Warning: mr.deallocate() failed during Buffer destruction: {exc}", file=sys.stderr) @@ -119,7 +131,11 @@ cdef class Buffer: @staticmethod def _reduce_helper(mr, ipc_descriptor): - return Buffer.from_ipc_descriptor(mr, ipc_descriptor) + # The parent process's stream is not portable across processes, so the + # pickle path cannot thread an explicit stream through. Seed the + # imported buffer's deallocation with the current context's default + # stream; the receiver can override via buffer.close(stream). + return Buffer.from_ipc_descriptor(mr, ipc_descriptor, stream=default_stream()) def __reduce__(self): # Must not serialize the parent's stream! @@ -158,9 +174,20 @@ cdef class Buffer: @classmethod def from_ipc_descriptor( cls, mr: DeviceMemoryResource | PinnedMemoryResource, ipc_descriptor: IPCBufferDescriptor, - stream: Stream = None + *, stream: Stream ) -> Buffer: - """Import a buffer that was exported from another process.""" + """Import a buffer that was exported from another process. + + Parameters + ---------- + mr : :obj:`~_memory.DeviceMemoryResource` | :obj:`~_memory.PinnedMemoryResource` + The IPC-enabled memory resource matching the exporting process. + ipc_descriptor : :obj:`~_memory.IPCBufferDescriptor` + The descriptor exported from another process. + stream : :obj:`~_stream.Stream` + Keyword-only. The stream used for asynchronous deallocation when + the buffer is closed or garbage collected. + """ return _ipc.Buffer_from_ipc_descriptor(cls, mr, ipc_descriptor, stream) @property @@ -215,7 +242,7 @@ cdef class Buffer: if self._memory_resource is None: raise ValueError("a destination buffer must be provided (this " "buffer does not have a memory_resource)") - dst = self._memory_resource.allocate(src_size, s) + dst = self._memory_resource.allocate(src_size, stream=s) cdef size_t dst_size = dst._size if dst_size != src_size: @@ -490,17 +517,17 @@ cdef class MemoryResource: resource's respective property.) """ - def allocate(self, size_t size, stream: Stream | GraphBuilder | None = None) -> Buffer: + def allocate(self, size_t size, *, stream: Stream | GraphBuilder) -> Buffer: """Allocate a buffer of the requested size. Parameters ---------- size : int The size of the buffer to allocate, in bytes. - stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`, optional - The stream on which to perform the allocation asynchronously. - If None, it is up to each memory resource implementation to decide - and document the behavior. + stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder` + Keyword-only. The stream on which to perform the allocation + asynchronously. Must be passed explicitly; pass + ``device.default_stream`` to use the default stream. Returns ------- @@ -510,7 +537,7 @@ cdef class MemoryResource: """ raise TypeError("MemoryResource.allocate must be implemented by subclasses.") - def deallocate(self, ptr: DevicePointerType, size_t size, stream: Stream | GraphBuilder | None = None): + def deallocate(self, ptr: DevicePointerType, size_t size, *, stream: Stream | GraphBuilder): """Deallocate a buffer previously allocated by this resource. Parameters @@ -519,10 +546,10 @@ cdef class MemoryResource: The pointer or handle to the buffer to deallocate. size : int The size of the buffer to deallocate, in bytes. - stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`, optional - The stream on which to perform the deallocation asynchronously. - If None, it is up to each memory resource implementation to decide - and document the behavior. + stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder` + Keyword-only. The stream on which to perform the deallocation + asynchronously. Must be passed explicitly; pass + ``device.default_stream`` to use the default stream. """ raise TypeError("MemoryResource.deallocate must be implemented by subclasses.") diff --git a/cuda_core/cuda/core/_memory/_graph_memory_resource.pyx b/cuda_core/cuda/core/_memory/_graph_memory_resource.pyx index 2180276ed87..8fdc324dc59 100644 --- a/cuda_core/cuda/core/_memory/_graph_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_graph_memory_resource.pyx @@ -14,7 +14,7 @@ from cuda.core._resource_handles cimport ( as_cu, ) -from cuda.core._stream cimport default_stream, Stream_accept, Stream +from cuda.core._stream cimport Stream_accept, Stream from cuda.core._utils.cuda_utils cimport HANDLE_RETURN from functools import cache @@ -104,19 +104,19 @@ cdef class cyGraphMemoryResource(MemoryResource): def __cinit__(self, int device_id): self._device_id = device_id - def allocate(self, size_t size, stream: Stream | GraphBuilder | None = None) -> Buffer: + def allocate(self, size_t size, *, stream: Stream | GraphBuilder) -> Buffer: """ Allocate a buffer of the requested size. See documentation for :obj:`~_memory.MemoryResource`. """ - stream = Stream_accept(stream) if stream is not None else default_stream() - return GMR_allocate(self, size, stream) + cdef Stream s = Stream_accept(stream) + return GMR_allocate(self, size, s) - def deallocate(self, ptr: "DevicePointerType", size_t size, stream: Stream | GraphBuilder | None = None): + def deallocate(self, ptr: "DevicePointerType", size_t size, *, stream: Stream | GraphBuilder): """ Deallocate a buffer of the requested size. See documentation for :obj:`~_memory.MemoryResource`. """ - stream = Stream_accept(stream) if stream is not None else default_stream() - return GMR_deallocate(ptr, size, stream) + cdef Stream s = Stream_accept(stream) + return GMR_deallocate(ptr, size, s) def close(self): """No operation (provided for compatibility).""" diff --git a/cuda_core/cuda/core/_memory/_ipc.pyx b/cuda_core/cuda/core/_memory/_ipc.pyx index 88a1d9c1695..1c7b25c14fb 100644 --- a/cuda_core/cuda/core/_memory/_ipc.pyx +++ b/cuda_core/cuda/core/_memory/_ipc.pyx @@ -7,7 +7,7 @@ cimport cpython from cuda.bindings cimport cydriver from cuda.core._memory._buffer cimport Buffer, Buffer_from_deviceptr_handle from cuda.core._memory._memory_pool cimport _MemPool -from cuda.core._stream cimport Stream +from cuda.core._stream cimport Stream, Stream_accept from cuda.core._resource_handles cimport ( DevicePtrHandle, create_fd_handle, @@ -19,7 +19,6 @@ from cuda.core._resource_handles cimport ( as_py, ) -from cuda.core._stream cimport default_stream from cuda.core._utils.cuda_utils cimport HANDLE_RETURN from cuda.core._utils.cuda_utils import check_multiprocessing_start_method @@ -171,10 +170,7 @@ cdef Buffer Buffer_from_ipc_descriptor( """Import a buffer that was exported from another process.""" if not mr.is_ipc_enabled: raise RuntimeError("Memory resource is not IPC-enabled") - if stream is None: - # Note: match this behavior to _MemPool.allocate() - stream = default_stream() - cdef Stream s = stream + cdef Stream s = Stream_accept(stream) cdef DevicePtrHandle h_ptr = deviceptr_import_ipc( mr._h_pool, ipc_descriptor.payload_ptr(), diff --git a/cuda_core/cuda/core/_memory/_legacy.py b/cuda_core/cuda/core/_memory/_legacy.py index 036b89abdcc..510974364da 100644 --- a/cuda_core/cuda/core/_memory/_legacy.py +++ b/cuda_core/cuda/core/_memory/_legacy.py @@ -8,6 +8,7 @@ if TYPE_CHECKING: from cuda.core._memory._buffer import DevicePointerType + from cuda.core._stream import Stream from cuda.core._memory._buffer import Buffer, MemoryResource from cuda.core._utils.cuda_utils import ( @@ -27,25 +28,30 @@ class LegacyPinnedMemoryResource(MemoryResource): # TODO: support creating this MR with flags that are later passed to cuMemHostAlloc? - def allocate(self, size, stream=None) -> Buffer: + def allocate(self, size, *, stream: Stream | None = None) -> Buffer: """Allocate a buffer of the requested size. + ``cuMemAllocHost`` is synchronous, so this resource ignores any + supplied stream. The argument is accepted (and validated when + non-``None``) for interface conformance with stream-ordered + memory resources. + Parameters ---------- size : int The size of the buffer to allocate, in bytes. stream : Stream, optional - Currently ignored + Keyword-only. Validated when provided but otherwise unused. Returns ------- Buffer The allocated buffer object, which is accessible on both host and device. """ - if stream is None: - from cuda.core._stream import default_stream + from cuda.core._stream import Stream_accept - stream = default_stream() + if stream is not None: + Stream_accept(stream) if size: err, ptr = driver.cuMemAllocHost(size) raise_if_driver_error(err) @@ -53,7 +59,7 @@ def allocate(self, size, stream=None) -> Buffer: ptr = 0 return Buffer._init(ptr, size, self) - def deallocate(self, ptr: DevicePointerType, size, stream): + def deallocate(self, ptr: DevicePointerType, size, *, stream: Stream | None = None): """Deallocate a buffer previously allocated by this resource. Parameters @@ -62,11 +68,14 @@ def deallocate(self, ptr: DevicePointerType, size, stream): The pointer or handle to the buffer to deallocate. size : int The size of the buffer to deallocate, in bytes. - stream : Stream - The stream on which to perform the deallocation synchronously. + stream : Stream, optional + Keyword-only. If provided, ``stream.sync()`` is called before the + host allocation is freed. ``None`` skips the sync. """ + from cuda.core._stream import Stream_accept + if stream is not None: - stream.sync() + Stream_accept(stream).sync() if size: (err,) = driver.cuMemFreeHost(ptr) @@ -96,11 +105,13 @@ def __init__(self, device_id): self._device_id = Device(device_id).device_id - def allocate(self, size, stream=None) -> Buffer: - if stream is None: - from cuda.core._stream import default_stream + def allocate(self, size, *, stream: Stream | None = None) -> Buffer: + # cuMemAlloc is synchronous; stream is accepted (and validated) + # for interface conformance but not used. + from cuda.core._stream import Stream_accept - stream = default_stream() + if stream is not None: + Stream_accept(stream) if size: err, ptr = driver.cuMemAlloc(size) raise_if_driver_error(err) @@ -108,9 +119,11 @@ def allocate(self, size, stream=None) -> Buffer: ptr = 0 return Buffer._init(ptr, size, self) - def deallocate(self, ptr, size, stream): + def deallocate(self, ptr, size, *, stream: Stream | None = None): + from cuda.core._stream import Stream_accept + if stream is not None: - stream.sync() + Stream_accept(stream).sync() if size: (err,) = driver.cuMemFree(ptr) raise_if_driver_error(err) diff --git a/cuda_core/cuda/core/_memory/_memory_pool.pyx b/cuda_core/cuda/core/_memory/_memory_pool.pyx index 4e0f99d4529..4da5e26ea92 100644 --- a/cuda_core/cuda/core/_memory/_memory_pool.pyx +++ b/cuda_core/cuda/core/_memory/_memory_pool.pyx @@ -11,7 +11,7 @@ from libc.string cimport memset from cuda.bindings cimport cydriver from cuda.core._memory._buffer cimport Buffer, Buffer_from_deviceptr_handle, MemoryResource from cuda.core._memory cimport _ipc -from cuda.core._stream cimport default_stream, Stream_accept, Stream +from cuda.core._stream cimport Stream_accept, Stream from cuda.core._resource_handles cimport ( MemoryPoolHandle, DevicePtrHandle, @@ -122,16 +122,17 @@ cdef class _MemPool(MemoryResource): """ _MP_close(self) - def allocate(self, size_t size, stream: Stream | GraphBuilder | None = None) -> Buffer: + def allocate(self, size_t size, *, stream: Stream | GraphBuilder) -> Buffer: """Allocate a buffer of the requested size. Parameters ---------- size : int The size of the buffer to allocate, in bytes. - stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`, optional - The stream on which to perform the allocation asynchronously. - If None, an internal stream is used. + stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder` + Keyword-only. The stream on which to perform the allocation + asynchronously. Must be passed explicitly; pass + ``device.default_stream`` to use the default stream. Returns ------- @@ -141,10 +142,10 @@ cdef class _MemPool(MemoryResource): """ if self.is_mapped: raise TypeError("Cannot allocate from a mapped IPC-enabled memory resource") - stream = Stream_accept(stream) if stream is not None else default_stream() - return _MP_allocate(self, size, stream) + cdef Stream s = Stream_accept(stream) + return _MP_allocate(self, size, s) - def deallocate(self, ptr: "DevicePointerType", size_t size, stream: Stream | GraphBuilder | None = None): + def deallocate(self, ptr: "DevicePointerType", size_t size, *, stream: Stream | GraphBuilder): """Deallocate a buffer previously allocated by this resource. Parameters @@ -153,13 +154,13 @@ cdef class _MemPool(MemoryResource): The pointer or handle to the buffer to deallocate. size : int The size of the buffer to deallocate, in bytes. - stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`, optional - The stream on which to perform the deallocation asynchronously. - If the buffer is deallocated without an explicit stream, the allocation stream - is used. + stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder` + Keyword-only. The stream on which to perform the deallocation + asynchronously. Must be passed explicitly; pass + ``device.default_stream`` to use the default stream. """ - stream = Stream_accept(stream) if stream is not None else default_stream() - _MP_deallocate(self, ptr, size, stream) + cdef Stream s = Stream_accept(stream) + _MP_deallocate(self, ptr, size, s) @property def attributes(self) -> _MemPoolAttributes: diff --git a/cuda_core/cuda/core/_memory/_virtual_memory_resource.py b/cuda_core/cuda/core/_memory/_virtual_memory_resource.py index a60436a4305..78a35e850fb 100644 --- a/cuda_core/cuda/core/_memory/_virtual_memory_resource.py +++ b/cuda_core/cuda/core/_memory/_virtual_memory_resource.py @@ -474,7 +474,7 @@ def _build_access_descriptors(self, prop: driver.CUmemAllocationProp) -> list: return descs - def allocate(self, size: int, stream: Stream | None = None) -> Buffer: + def allocate(self, size: int, *, stream: Stream | None = None) -> Buffer: """ Allocate a buffer of the given size using CUDA virtual memory. @@ -483,7 +483,8 @@ def allocate(self, size: int, stream: Stream | None = None) -> Buffer: size : int The size in bytes of the buffer to allocate. stream : Stream, optional - CUDA stream to associate with the allocation (not currently supported). + Keyword-only. Unused because virtual memory operations are + synchronous. Returns ------- @@ -492,8 +493,6 @@ def allocate(self, size: int, stream: Stream | None = None) -> Buffer: Raises ------ - NotImplementedError - If a stream is provided or if the location type is not device memory. CUDAError If any CUDA driver API call fails during allocation. @@ -505,7 +504,9 @@ def allocate(self, size: int, stream: Stream | None = None) -> Buffer: specified in the resource's configuration. """ if stream is not None: - raise NotImplementedError("Stream is not supported with VirtualMemoryResource") + from cuda.core._stream import Stream_accept + + Stream_accept(stream) config = self.config # ---- Build allocation properties ---- @@ -558,10 +559,24 @@ def allocate(self, size: int, stream: Stream | None = None) -> Buffer: buf = Buffer.from_handle(ptr=ptr, size=aligned_size, mr=self) return buf - def deallocate(self, ptr: int, size: int, stream: Stream | None = None) -> None: # noqa: ARG002 + def deallocate(self, ptr: int, size: int, *, stream: Stream | None = None) -> None: """ Deallocate memory on the device using CUDA VMM APIs. + + Parameters + ---------- + ptr : int + The pointer to the memory to deallocate. + size : int + The size in bytes of the memory to deallocate. + stream : Stream, optional + Keyword-only. Unused because virtual memory operations are + synchronous. """ + if stream is not None: + from cuda.core._stream import Stream_accept + + Stream_accept(stream) result, handle = driver.cuMemRetainAllocationHandle(ptr) raise_if_driver_error(result) (result,) = driver.cuMemUnmap(ptr, size) diff --git a/cuda_core/cuda/core/_module.pyx b/cuda_core/cuda/core/_module.pyx index fee979b6130..4a8601f8573 100644 --- a/cuda_core/cuda/core/_module.pyx +++ b/cuda_core/cuda/core/_module.pyx @@ -11,7 +11,7 @@ from collections import namedtuple from cuda.core._device import Device from cuda.core._launch_config cimport LaunchConfig from cuda.core._launch_config import LaunchConfig -from cuda.core._stream cimport Stream +from cuda.core._stream cimport Stream, Stream_accept from cuda.core._program import ObjectCodeFormatType from cuda.core._resource_handles cimport ( LibraryHandle, @@ -368,7 +368,7 @@ cdef class KernelOccupancy: )) return dynamic_smem_size - def max_potential_cluster_size(self, config: LaunchConfig, stream: Stream | None = None) -> int: + def max_potential_cluster_size(self, config: LaunchConfig, *, stream: Stream) -> int: """Maximum potential cluster size. The maximum potential cluster size for this kernel and given launch configuration. @@ -377,8 +377,10 @@ cdef class KernelOccupancy: ---------- config: :obj:`~_launch_config.LaunchConfig` Kernel launch configuration. Cluster dimensions in the configuration are ignored. - stream: :obj:`~Stream`, optional - The stream on which this kernel is to be launched. + stream: :obj:`~Stream` + Keyword-only. The stream on which this kernel is to be launched. + Must be passed explicitly; pass ``device.default_stream`` to + use the default stream. Returns ------- @@ -386,17 +388,15 @@ cdef class KernelOccupancy: The maximum cluster size that can be launched for this kernel and launch configuration. """ cdef cydriver.CUlaunchConfig drv_cfg = (config)._to_native_launch_config() - cdef Stream s - if stream is not None: - s = stream - drv_cfg.hStream = as_cu(s._h_stream) + cdef Stream s = Stream_accept(stream) + drv_cfg.hStream = as_cu(s._h_stream) cdef int cluster_size cdef cydriver.CUfunction func = as_cu(self._h_kernel) with nogil: HANDLE_RETURN(cydriver.cuOccupancyMaxPotentialClusterSize(&cluster_size, func, &drv_cfg)) return cluster_size - def max_active_clusters(self, config: LaunchConfig, stream: Stream | None = None) -> int: + def max_active_clusters(self, config: LaunchConfig, *, stream: Stream) -> int: """Maximum number of active clusters on the target device. The maximum number of clusters that could concurrently execute on the target device. @@ -405,8 +405,10 @@ cdef class KernelOccupancy: ---------- config: :obj:`~_launch_config.LaunchConfig` Kernel launch configuration. - stream: :obj:`~Stream`, optional - The stream on which this kernel is to be launched. + stream: :obj:`~Stream` + Keyword-only. The stream on which this kernel is to be launched. + Must be passed explicitly; pass ``device.default_stream`` to + use the default stream. Returns ------- @@ -414,10 +416,8 @@ cdef class KernelOccupancy: The maximum number of clusters that could co-exist on the target device. """ cdef cydriver.CUlaunchConfig drv_cfg = (config)._to_native_launch_config() - cdef Stream s - if stream is not None: - s = stream - drv_cfg.hStream = as_cu(s._h_stream) + cdef Stream s = Stream_accept(stream) + drv_cfg.hStream = as_cu(s._h_stream) cdef int num_clusters cdef cydriver.CUfunction func = as_cu(self._h_kernel) with nogil: diff --git a/cuda_core/cuda/core/_stream.pxd b/cuda_core/cuda/core/_stream.pxd index c47ff1ea289..c9ffb4c80a7 100644 --- a/cuda_core/cuda/core/_stream.pxd +++ b/cuda_core/cuda/core/_stream.pxd @@ -22,4 +22,4 @@ cdef class Stream: cpdef Stream default_stream() -cdef Stream Stream_accept(arg, bint allow_stream_protocol=*) +cpdef Stream Stream_accept(arg, bint allow_stream_protocol=*) diff --git a/cuda_core/cuda/core/_stream.pyx b/cuda_core/cuda/core/_stream.pyx index a93f8906969..f487a0a53e5 100644 --- a/cuda_core/cuda/core/_stream.pyx +++ b/cuda_core/cuda/core/_stream.pyx @@ -515,10 +515,17 @@ cdef cydriver.CUstream _handle_from_stream_protocol(obj) except*: return (info[1]) # Helper for API functions that accept either Stream or GraphBuilder. Performs -# needed checks and returns the relevant stream. -cdef Stream Stream_accept(arg, bint allow_stream_protocol=False): +# needed checks and returns the relevant stream. Rejects None so that callers +# cannot rely on an implicit fallback to the default stream; if the default +# stream is wanted, pass `device.default_stream` explicitly. +cpdef Stream Stream_accept(arg, bint allow_stream_protocol=False): from cuda.core.graph._graph_builder import GraphBuilder + if arg is None: + raise TypeError( + "stream is required and must not be None; " + "pass device.default_stream explicitly to use the default stream." + ) if isinstance(arg, Stream): return (arg) elif isinstance(arg, GraphBuilder): diff --git a/cuda_core/cuda/core/graph/_graph_builder.pyx b/cuda_core/cuda/core/graph/_graph_builder.pyx index 526c95e04ad..b745598abab 100644 --- a/cuda_core/cuda/core/graph/_graph_builder.pyx +++ b/cuda_core/cuda/core/graph/_graph_builder.pyx @@ -868,7 +868,7 @@ class Graph: Parameters ---------- stream : :obj:`~_stream.Stream` - The stream in which to launch the graph + The stream in which to launch the graph. """ handle_return(driver.cuGraphLaunch(self._mnff.graph, stream.handle)) diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst index a0f1de1a121..58553fe59ea 100644 --- a/cuda_core/docs/source/release/1.0.0-notes.rst +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -120,6 +120,28 @@ Breaking changes ``CUgraphConditionalHandle`` value. Previously, ``.handle`` had to be extracted explicitly. +- ``stream`` is now a required keyword-only argument on APIs that schedule + work on a stream + (`#2001 `__). + Pass ``device.default_stream`` (or any :class:`Stream`) explicitly to + retain the previous behavior. Affected APIs: + + - :meth:`MemoryResource.allocate` / :meth:`MemoryResource.deallocate` + and overrides on :class:`DeviceMemoryResource`, + :class:`PinnedMemoryResource`, :class:`ManagedMemoryResource`, + and :class:`graph.GraphMemoryResource`. + - :meth:`Device.allocate`. + - :meth:`GraphicsResource.map`. + - :meth:`KernelOccupancy.max_potential_cluster_size` and + :meth:`KernelOccupancy.max_active_clusters`. + - :meth:`Buffer.from_ipc_descriptor` (no longer falls back to the default + stream when ``stream=None`` is passed). + + Synchronous memory resources are exempt: their allocate/deallocate + methods accept an optional ``stream`` (validated when non-``None``) + but do not use it. This applies to :class:`LegacyPinnedMemoryResource` + and :class:`VirtualMemoryResource`. + - Consistent naming of types annotation helpers (`#2016 `__): - :obj:`cuda.core.typing.DevicePointerT` -> :obj:`cuda.core.typing.DevicePointerType` diff --git a/cuda_core/examples/memory_ops.py b/cuda_core/examples/memory_ops.py index 438c40b333d..549686b466a 100644 --- a/cuda_core/examples/memory_ops.py +++ b/cuda_core/examples/memory_ops.py @@ -82,7 +82,7 @@ def main(): device_array = cp.from_dlpack(device_buffer).view(dtype=dtype) # 2. Pinned Memory (CPU memory, GPU accessible) - pinned_buffer = pinned_mr.allocate(total_size, stream=stream) + pinned_buffer = pinned_mr.allocate(total_size) pinned_array = np.from_dlpack(pinned_buffer).view(dtype=dtype) # Initialize data diff --git a/cuda_core/tests/graph/test_device_launch.py b/cuda_core/tests/graph/test_device_launch.py index 0bd4ba12634..cb143a17328 100644 --- a/cuda_core/tests/graph/test_device_launch.py +++ b/cuda_core/tests/graph/test_device_launch.py @@ -95,7 +95,7 @@ def test_device_launch_basic(init_cuda): # Allocate and initialize memory mr = LegacyPinnedMemoryResource() - buf = mr.allocate(4, stream=stream) + buf = mr.allocate(4) arr = np.from_dlpack(buf).view(np.int32) arr[0] = 0 stream.sync() @@ -145,7 +145,7 @@ def test_device_launch_multiple(init_cuda): # Allocate and initialize memory mr = LegacyPinnedMemoryResource() - buf = mr.allocate(4, stream=stream) + buf = mr.allocate(4) arr = np.from_dlpack(buf).view(np.int32) arr[0] = 0 stream.sync() diff --git a/cuda_core/tests/graph/test_graph_memory_resource.py b/cuda_core/tests/graph/test_graph_memory_resource.py index 13e6745d748..7f71fc95852 100644 --- a/cuda_core/tests/graph/test_graph_memory_resource.py +++ b/cuda_core/tests/graph/test_graph_memory_resource.py @@ -166,7 +166,7 @@ def test_graph_alloc_with_output(mempool_device, mode): # buffer allocated within the graph. The auto_free_on_launch option # is required to properly use the output buffer. gb = device.create_graph_builder().begin_building(mode) - out = gmr.allocate(NBYTES, gb) + out = gmr.allocate(NBYTES, stream=gb) out.copy_from(in_, stream=gb) launch(gb, LaunchConfig(grid=1, block=1), add_one, out, NBYTES) options = GraphCompleteOptions(auto_free_on_launch=True) diff --git a/cuda_core/tests/helpers/buffers.py b/cuda_core/tests/helpers/buffers.py index fbd5428c28b..44f84693089 100644 --- a/cuda_core/tests/helpers/buffers.py +++ b/cuda_core/tests/helpers/buffers.py @@ -19,14 +19,16 @@ class DummyUnifiedMemoryResource(MemoryResource): + # cuMemAllocManaged / cuMemFree are synchronous; stream is accepted + # for interface conformance with stream-ordered MRs but ignored. def __init__(self, device): self.device = device - def allocate(self, size, stream=None) -> Buffer: + def allocate(self, size, *, stream=None) -> Buffer: ptr = handle_return(driver.cuMemAllocManaged(size, driver.CUmemAttach_flags.CU_MEM_ATTACH_GLOBAL.value)) return Buffer.from_handle(ptr=ptr, size=size, mr=self) - def deallocate(self, ptr, size, stream=None): + def deallocate(self, ptr, size, *, stream=None): handle_return(driver.cuMemFree(ptr)) @property @@ -51,12 +53,14 @@ class TrackingMR(MemoryResource): def __init__(self): self.active = {} - def allocate(self, size, stream=None): + # cuMemAlloc / cuMemFree are synchronous; stream is accepted for + # interface conformance but ignored. + def allocate(self, size, *, stream=None): ptr = handle_return(driver.cuMemAlloc(size)) self.active[int(ptr)] = size return Buffer.from_handle(ptr=ptr, size=size, mr=self) - def deallocate(self, ptr, size, stream=None): + def deallocate(self, ptr, size, *, stream=None): handle_return(driver.cuMemFree(ptr)) del self.active[int(ptr)] diff --git a/cuda_core/tests/memory_ipc/test_errors.py b/cuda_core/tests/memory_ipc/test_errors.py index f82164ca37c..d17e63dc90a 100644 --- a/cuda_core/tests/memory_ipc/test_errors.py +++ b/cuda_core/tests/memory_ipc/test_errors.py @@ -70,7 +70,7 @@ def PARENT_ACTION(self, queue): def CHILD_ACTION(self, queue): mr = queue.get(timeout=CHILD_TIMEOUT_SEC) - mr.allocate(NBYTES) + mr.allocate(NBYTES, stream=self.device.default_stream) def ASSERT(self, exc_type, exc_msg): assert exc_type is TypeError @@ -84,12 +84,12 @@ def PARENT_ACTION(self, queue): options = DeviceMemoryResourceOptions(max_size=POOL_SIZE, ipc_enabled=True) mr2 = DeviceMemoryResource(self.device, options=options) self._extra_mrs.append(mr2) - buffer = mr2.allocate(NBYTES) + buffer = mr2.allocate(NBYTES, stream=self.device.default_stream) queue.put([self.mr, buffer.ipc_descriptor]) # Note: mr does not own this buffer def CHILD_ACTION(self, queue): mr, buffer_desc = queue.get(timeout=CHILD_TIMEOUT_SEC) - Buffer.from_ipc_descriptor(mr, buffer_desc) + Buffer.from_ipc_descriptor(mr, buffer_desc, stream=self.device.default_stream) def ASSERT(self, exc_type, exc_msg): assert exc_type is CUDAError @@ -102,12 +102,12 @@ class TestImportBuffer(ChildErrorHarness): def PARENT_ACTION(self, queue): # Note: if the buffer is not attached to something to prolong its life, # CUDA_ERROR_INVALID_CONTEXT is raised from Buffer.__del__ - self.buffer = self.mr.allocate(NBYTES) + self.buffer = self.mr.allocate(NBYTES, stream=self.device.default_stream) queue.put(self.buffer) def CHILD_ACTION(self, queue): buffer = queue.get(timeout=CHILD_TIMEOUT_SEC) - Buffer.from_ipc_descriptor(self.mr, buffer) + Buffer.from_ipc_descriptor(self.mr, buffer, stream=self.device.default_stream) def ASSERT(self, exc_type, exc_msg): assert exc_type is TypeError @@ -124,7 +124,7 @@ def PARENT_ACTION(self, queue): options = DeviceMemoryResourceOptions(max_size=POOL_SIZE, ipc_enabled=True) mr2 = DeviceMemoryResource(self.device, options=options) self._extra_mrs.append(mr2) - self.buffer = mr2.allocate(NBYTES) + self.buffer = mr2.allocate(NBYTES, stream=self.device.default_stream) buffer_s = pickle.dumps(self.buffer) queue.put(buffer_s) # Note: mr2 not sent diff --git a/cuda_core/tests/memory_ipc/test_ipc_duplicate_import.py b/cuda_core/tests/memory_ipc/test_ipc_duplicate_import.py index cab6b44aa3f..f0c4951e8e6 100644 --- a/cuda_core/tests/memory_ipc/test_ipc_duplicate_import.py +++ b/cuda_core/tests/memory_ipc/test_ipc_duplicate_import.py @@ -34,8 +34,8 @@ def child_main(log, queue): buffer_desc2 = queue.get() # Import the same buffer twice - should return same handle due to cache - buffer1 = Buffer.from_ipc_descriptor(mr, buffer_desc1) - buffer2 = Buffer.from_ipc_descriptor(mr, buffer_desc2) + buffer1 = Buffer.from_ipc_descriptor(mr, buffer_desc1, stream=device.default_stream) + buffer2 = Buffer.from_ipc_descriptor(mr, buffer_desc2, stream=device.default_stream) log(f"buffer1.handle = {buffer1.handle}") log(f"buffer2.handle = {buffer2.handle}") @@ -68,7 +68,7 @@ def test_main(self, ipc_device, ipc_memory_resource): mr = ipc_memory_resource log("allocating buffer") - buffer = mr.allocate(NBYTES) + buffer = mr.allocate(NBYTES, stream=ipc_device.default_stream) # Start the child process. log("starting child") diff --git a/cuda_core/tests/memory_ipc/test_leaks.py b/cuda_core/tests/memory_ipc/test_leaks.py index 8debd71c3f9..e2a7e8d096b 100644 --- a/cuda_core/tests/memory_ipc/test_leaks.py +++ b/cuda_core/tests/memory_ipc/test_leaks.py @@ -84,19 +84,19 @@ def __reduce__(self): @pytest.mark.parametrize( "getobject", [ - lambda mr: mr.allocation_handle, - lambda mr: mr, - lambda mr: mr.allocate(NBYTES), - lambda mr: mr.allocate(NBYTES).ipc_descriptor, + lambda mr, _stream: mr.allocation_handle, + lambda mr, _stream: mr, + lambda mr, stream: mr.allocate(NBYTES, stream=stream), + lambda mr, stream: mr.allocate(NBYTES, stream=stream).ipc_descriptor, ], ids=["alloc_handle", "mr", "buffer", "buffer_desc"], ) @pytest.mark.parametrize("launcher", [exec_success, exec_launch_failure, exec_reduce_failure]) -def test_pass_object(ipc_memory_resource, launcher, getobject): +def test_pass_object(ipc_device, ipc_memory_resource, launcher, getobject): """Check for fd leaks when an object is sent as a subprocess argument.""" mr = ipc_memory_resource with CheckFDLeaks(): - obj = getobject(mr) + obj = getobject(mr, ipc_device.default_stream) try: launcher(obj, number=2) finally: diff --git a/cuda_core/tests/memory_ipc/test_memory_ipc.py b/cuda_core/tests/memory_ipc/test_memory_ipc.py index 0996c71d2cc..54159d81130 100644 --- a/cuda_core/tests/memory_ipc/test_memory_ipc.py +++ b/cuda_core/tests/memory_ipc/test_memory_ipc.py @@ -30,7 +30,7 @@ def test_main(self, ipc_device, ipc_memory_resource): process.start() # Allocate and fill memory. - buffer = mr.allocate(NBYTES) + buffer = mr.allocate(NBYTES, stream=device.default_stream) assert not buffer.is_mapped pgen.fill_buffer(buffer, seed=False) @@ -66,10 +66,10 @@ def test_main(self, ipc_device, ipc_memory_resource): q1, q2 = (mp.Queue() for _ in range(2)) # Allocate memory buffers and export them to each child. - buffer1 = mr.allocate(NBYTES) + buffer1 = mr.allocate(NBYTES, stream=device.default_stream) q1.put(buffer1) q2.put(buffer1) - buffer2 = mr.allocate(NBYTES) + buffer2 = mr.allocate(NBYTES, stream=device.default_stream) q1.put(buffer2) q2.put(buffer2) @@ -127,8 +127,8 @@ def test_main(self, ipc_device, ipc_memory_resource): p2.start() # Allocate and share memory. - buffer1 = mr.allocate(NBYTES) - buffer2 = mr.allocate(NBYTES) + buffer1 = mr.allocate(NBYTES, stream=device.default_stream) + buffer2 = mr.allocate(NBYTES, stream=device.default_stream) q1.put(buffer1.ipc_descriptor) q2.put(buffer2.ipc_descriptor) @@ -152,7 +152,7 @@ def child_main(self, device, alloc_handle, seed, queue): device.set_current() mr = DeviceMemoryResource.from_allocation_handle(device, alloc_handle) buffer_descriptor = queue.get(timeout=CHILD_TIMEOUT_SEC) - buffer = Buffer.from_ipc_descriptor(mr, buffer_descriptor) + buffer = Buffer.from_ipc_descriptor(mr, buffer_descriptor, stream=device.default_stream) pgen = PatternGen(device, NBYTES) pgen.fill_buffer(buffer, seed=seed) buffer.close() @@ -177,8 +177,8 @@ def test_main(self, ipc_device, ipc_memory_resource): p2.start() # Allocate and share memory. - buffer1 = mr.allocate(NBYTES) - buffer2 = mr.allocate(NBYTES) + buffer1 = mr.allocate(NBYTES, stream=device.default_stream) + buffer2 = mr.allocate(NBYTES, stream=device.default_stream) q1.put(buffer1) q2.put(buffer2) diff --git a/cuda_core/tests/memory_ipc/test_peer_access.py b/cuda_core/tests/memory_ipc/test_peer_access.py index a3f83986701..de0baef9b8b 100644 --- a/cuda_core/tests/memory_ipc/test_peer_access.py +++ b/cuda_core/tests/memory_ipc/test_peer_access.py @@ -73,7 +73,7 @@ def test_main(self, mempool_device_x2, grant_access_in_parent): assert mr.peer_accessible_by == (0,) else: assert mr.peer_accessible_by == () - buffer = mr.allocate(NBYTES) + buffer = mr.allocate(NBYTES, stream=dev1.default_stream) pgen = PatternGen(dev1, NBYTES) pgen.fill_buffer(buffer, seed=False) diff --git a/cuda_core/tests/memory_ipc/test_send_buffers.py b/cuda_core/tests/memory_ipc/test_send_buffers.py index 6c9f1769142..041a8539da3 100644 --- a/cuda_core/tests/memory_ipc/test_send_buffers.py +++ b/cuda_core/tests/memory_ipc/test_send_buffers.py @@ -28,7 +28,7 @@ def test_main(self, ipc_device, nmrs): try: # Allocate and fill memory. - buffers = [mr.allocate(NBYTES) for mr, _ in zip(cycle(mrs), range(NTASKS))] + buffers = [mr.allocate(NBYTES, stream=device.default_stream) for mr, _ in zip(cycle(mrs), range(NTASKS))] pgen = PatternGen(device, NBYTES) for buffer in buffers: pgen.fill_buffer(buffer, seed=False) @@ -82,7 +82,7 @@ def test_main(self, ipc_device, ipc_memory_resource): # Allocate, fill a buffer. mr = ipc_memory_resource pgen = PatternGen(device, NBYTES) - buffer = mr.allocate(NBYTES) + buffer = mr.allocate(NBYTES, stream=device.default_stream) pgen.fill_buffer(buffer, seed=0) # Set up communication. diff --git a/cuda_core/tests/memory_ipc/test_serialize.py b/cuda_core/tests/memory_ipc/test_serialize.py index 63e6ccf1dfd..78d26387c8a 100644 --- a/cuda_core/tests/memory_ipc/test_serialize.py +++ b/cuda_core/tests/memory_ipc/test_serialize.py @@ -38,10 +38,10 @@ def test_main(self, ipc_device, ipc_memory_resource): mp.reduction.send_handle(parent_conn, alloc_handle.handle, process.pid) # Send a buffer. - buffer1 = mr.allocate(NBYTES) + buffer1 = mr.allocate(NBYTES, stream=device.default_stream) parent_conn.send(buffer1) # directly - buffer2 = mr.allocate(NBYTES) + buffer2 = mr.allocate(NBYTES, stream=device.default_stream) parent_conn.send(buffer2.ipc_descriptor) # by descriptor # Wait for the child process. @@ -68,7 +68,7 @@ def child_main(self, conn): # Receive the buffers. buffer1 = conn.recv() # directly buffer_desc = conn.recv() - buffer2 = Buffer.from_ipc_descriptor(mr, buffer_desc) # by descriptor + buffer2 = Buffer.from_ipc_descriptor(mr, buffer_desc, stream=device.default_stream) # by descriptor # Modify the buffers. pgen = PatternGen(device, NBYTES) @@ -98,7 +98,7 @@ def test_main(self, ipc_device, ipc_memory_resource): assert uuid == mr.uuid # Send a buffer. - buffer = mr.allocate(NBYTES) + buffer = mr.allocate(NBYTES, stream=device.default_stream) pipe[0].put(buffer) # Wait for the child process. @@ -141,7 +141,7 @@ def test_main(self, ipc_device, ipc_memory_resource): device = ipc_device mr = ipc_memory_resource alloc_handle = mr.allocation_handle - buffer = mr.allocate(NBYTES) + buffer = mr.allocate(NBYTES, stream=device.default_stream) buffer_desc = buffer.ipc_descriptor pgen = PatternGen(device, NBYTES) diff --git a/cuda_core/tests/memory_ipc/test_workerpool.py b/cuda_core/tests/memory_ipc/test_workerpool.py index cfaa776ac9e..5d0c7b1a0f5 100644 --- a/cuda_core/tests/memory_ipc/test_workerpool.py +++ b/cuda_core/tests/memory_ipc/test_workerpool.py @@ -35,7 +35,7 @@ def test_main(self, ipc_device, nmrs): mrs = [DeviceMemoryResource(device, options=options) for _ in range(nmrs)] try: - buffers = [mr.allocate(NBYTES) for mr, _ in zip(cycle(mrs), range(NTASKS))] + buffers = [mr.allocate(NBYTES, stream=device.default_stream) for mr, _ in zip(cycle(mrs), range(NTASKS))] with mp.Pool(NWORKERS) as pool: pool.map(self.process_buffer, buffers) @@ -77,7 +77,7 @@ def test_main(self, ipc_device, nmrs): mrs = [DeviceMemoryResource(device, options=options) for _ in range(nmrs)] try: - buffers = [mr.allocate(NBYTES) for mr, _ in zip(cycle(mrs), range(NTASKS))] + buffers = [mr.allocate(NBYTES, stream=device.default_stream) for mr, _ in zip(cycle(mrs), range(NTASKS))] with mp.Pool(NWORKERS, initializer=self.init_worker, initargs=(mrs,)) as pool: pool.starmap( @@ -97,7 +97,7 @@ def process_buffer(self, mr_idx, buffer_desc): mr = self.mrs[mr_idx] device = Device(mr.device_id) device.set_current() - buffer = Buffer.from_ipc_descriptor(mr, buffer_desc) + buffer = Buffer.from_ipc_descriptor(mr, buffer_desc, stream=device.default_stream) pgen = PatternGen(device, NBYTES) pgen.fill_buffer(buffer, seed=True) buffer.close() @@ -127,7 +127,7 @@ def test_main(self, ipc_device, nmrs): mrs = [DeviceMemoryResource(device, options=options) for _ in range(nmrs)] try: - buffers = [mr.allocate(NBYTES) for mr, _ in zip(cycle(mrs), range(NTASKS))] + buffers = [mr.allocate(NBYTES, stream=device.default_stream) for mr, _ in zip(cycle(mrs), range(NTASKS))] with mp.Pool(NWORKERS, initializer=self.init_worker, initargs=(mrs,)) as pool: pool.starmap(self.process_buffer, [(device, pickle.dumps(buffer)) for buffer in buffers]) diff --git a/cuda_core/tests/test_device.py b/cuda_core/tests/test_device.py index 56a97f5185c..26f5c772ea4 100644 --- a/cuda_core/tests/test_device.py +++ b/cuda_core/tests/test_device.py @@ -63,7 +63,7 @@ def test_device_repr(deinit_cuda): def test_device_alloc(deinit_cuda): device = Device() device.set_current() - buffer = device.allocate(1024) + buffer = device.allocate(1024, stream=device.default_stream) device.sync() assert buffer.handle != 0 assert buffer.size == 1024 @@ -73,7 +73,7 @@ def test_device_alloc(deinit_cuda): def test_device_alloc_zero_bytes(deinit_cuda): device = Device() device.set_current() - buffer = device.allocate(0) + buffer = device.allocate(0, stream=device.default_stream) device.sync() assert buffer.handle >= 0 assert buffer.size == 0 diff --git a/cuda_core/tests/test_graphics.py b/cuda_core/tests/test_graphics.py index 4358a0c7b38..6f5877f76b0 100644 --- a/cuda_core/tests/test_graphics.py +++ b/cuda_core/tests/test_graphics.py @@ -337,14 +337,14 @@ def test_map_with_stream(self): assert buf.size > 0 resource.close() - def test_map_with_default_stream(self): + def test_map_requires_explicit_stream(self): with _gl_context_and_buffer(nbytes=4096) as (gl_buf, _): resource = GraphicsResource.from_gl_buffer(gl_buf, flags="write_discard") - with resource.map() as buf: - assert isinstance(buf, Buffer) - assert buf.size > 0 - assert not resource.is_mapped - resource.close() + try: + with pytest.raises(TypeError, match=r"keyword-only argument"): + resource.map() + finally: + resource.close() # --------------------------------------------------------------------------- diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index d6ad6ac3320..1f06162b3d3 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -60,14 +60,16 @@ class DummyDeviceMemoryResource(MemoryResource): + # cuMemAlloc / cuMemFree are synchronous; stream is accepted for + # interface conformance but ignored. def __init__(self, device): self.device = device - def allocate(self, size, stream=None) -> Buffer: + def allocate(self, size, *, stream=None) -> Buffer: ptr = handle_return(driver.cuMemAlloc(size)) return Buffer.from_handle(ptr=ptr, size=size, mr=self) - def deallocate(self, ptr, size, stream=None): + def deallocate(self, ptr, size, *, stream=None): handle_return(driver.cuMemFree(ptr)) @property @@ -84,16 +86,18 @@ def device_id(self) -> int: class DummyHostMemoryResource(MemoryResource): + # Pure-host ctypes allocation; stream is accepted for interface + # conformance but ignored. def __init__(self): pass - def allocate(self, size, stream=None) -> Buffer: + def allocate(self, size, *, stream=None) -> Buffer: # Allocate a ctypes buffer of size `size` ptr = (ctypes.c_byte * size)() self._ptr = ptr return Buffer.from_handle(ptr=ctypes.addressof(ptr), size=size, mr=self) - def deallocate(self, ptr, size, stream=None): + def deallocate(self, ptr, size, *, stream=None): del self._ptr @property @@ -110,14 +114,16 @@ def device_id(self) -> int: class DummyPinnedMemoryResource(MemoryResource): + # cuMemAllocHost / cuMemFreeHost are synchronous; stream is accepted + # for interface conformance but ignored. def __init__(self, device): self.device = device - def allocate(self, size, stream=None) -> Buffer: + def allocate(self, size, *, stream=None) -> Buffer: ptr = handle_return(driver.cuMemAllocHost(size)) return Buffer.from_handle(ptr=ptr, size=size, mr=self) - def deallocate(self, ptr, size, stream=None): + def deallocate(self, ptr, size, *, stream=None): handle_return(driver.cuMemFreeHost(ptr)) @property @@ -381,7 +387,7 @@ def test_buffer_external_device(change_device): dev_id = n - 1 d = Device(dev_id) d.set_current() - buffer_ = d.allocate(size=32) + buffer_ = d.allocate(size=32, stream=d.default_stream) if change_device: # let's switch to a different device if possibe @@ -513,9 +519,9 @@ def test_mr_deallocate_receives_stream(): received = {} class StreamCaptureMR(TrackingMR): - def deallocate(self, ptr, size, stream=None): + def deallocate(self, ptr, size, *, stream=None): received["stream"] = stream - super().deallocate(ptr, size, stream) + super().deallocate(ptr, size, stream=stream) mr = StreamCaptureMR() buf = mr.allocate(1024) @@ -523,6 +529,56 @@ def deallocate(self, ptr, size, stream=None): assert received["stream"].handle == stream.handle +def test_mr_dealloc_callback_falls_back_to_default_stream(): + """When a Buffer's device-pointer handle has no attached deallocation + stream (e.g. buffers minted via :meth:`Buffer.from_handle` from DLPack + import, IPC import, or third-party adapters), the C++ deleter callback + must fall back to the default stream rather than passing ``stream=None`` + to ``mr.deallocate``. Stream-ordered MRs validate the stream and would + otherwise raise ``TypeError`` from inside the ``noexcept`` callback, + which only logs a warning and silently leaks the allocation. See + `#2001 `__. + """ + import gc + + from cuda.core._stream import Stream_accept, default_stream + + device = Device() + device.set_current() + captured = {} + + class StrictCapturingMR(MemoryResource): + # Models a stream-ordered MR: deallocate validates the stream + # the same way DeviceMemoryResource.deallocate does. + @property + def is_device_accessible(self): + return True + + @property + def is_host_accessible(self): + return False + + @property + def device_id(self): + return device.device_id + + def allocate(self, size, *, stream): + raise NotImplementedError # not used; we use from_handle below + + def deallocate(self, ptr, size, *, stream): + captured["stream"] = Stream_accept(stream) + + mr = StrictCapturingMR() + # Buffer.from_handle binds mr but does not attach a deallocation stream. + # ptr=1 is fine because StrictCapturingMR.deallocate does not free. + buf = Buffer.from_handle(1, 1024, mr=mr) + del buf + gc.collect() + + assert "stream" in captured, "deallocate was not invoked (callback raised and leaked)" + assert captured["stream"].handle == default_stream().handle + + def test_memory_resource_and_owner_disallowed(): with pytest.raises(ValueError, match="cannot be both specified together"): a = (ctypes.c_byte * 20)() @@ -627,7 +683,7 @@ def test_managed_memory_resource_buffer_dlpack_device_type(): device.set_current() skip_if_managed_memory_unsupported(device) mr = create_managed_memory_resource_or_skip(ManagedMemoryResourceOptions(preferred_location=device.device_id)) - buf = mr.allocate(1024) + buf = mr.allocate(1024, stream=device.default_stream) assert mr.is_managed assert buf.is_managed @@ -650,7 +706,7 @@ def test_non_managed_resources_report_not_managed(mr_kind): skip_if_pinned_memory_unsupported(device) mr = create_pinned_memory_resource_or_xfail(xfail_device=device) assert mr.is_managed is False - buf = mr.allocate(1024) + buf = mr.allocate(1024, stream=device.default_stream) assert buf.is_managed is False buf.close() @@ -681,7 +737,7 @@ def test_device_memory_resource_initialization(use_device_object): assert not mr.is_ipc_enabled # Test allocation/deallocation works - buffer = mr.allocate(1024) + buffer = mr.allocate(1024, stream=device.default_stream) assert buffer.size == 1024 assert buffer.device_id == device.device_id buffer.close() @@ -699,7 +755,7 @@ def test_pinned_memory_resource_initialization(init_cuda): # Test allocation/deallocation works try: - buffer = mr.allocate(1024) + buffer = mr.allocate(1024, stream=device.default_stream) except CUDAError as exc: msg = str(exc) if "CUDA_ERROR_OUT_OF_MEMORY" in msg: @@ -727,7 +783,7 @@ def test_managed_memory_resource_initialization(init_cuda): assert mr.is_host_accessible # Test allocation/deallocation works - buffer = mr.allocate(1024) + buffer = mr.allocate(1024, stream=device.default_stream) assert buffer.size == 1024 assert buffer.is_host_accessible # But accessible from host assert buffer.memory_resource == mr @@ -952,15 +1008,15 @@ def test_device_memory_resource_with_options(init_cuda): assert not mr.is_ipc_enabled # Test allocation and deallocation - buffer1 = mr.allocate(1024) + buffer1 = mr.allocate(1024, stream=device.default_stream) assert buffer1.handle != 0 assert buffer1.size == 1024 assert buffer1.memory_resource == mr buffer1.close() # Test multiple allocations - buffer1 = mr.allocate(1024) - buffer2 = mr.allocate(2048) + buffer1 = mr.allocate(1024, stream=device.default_stream) + buffer2 = mr.allocate(2048, stream=device.default_stream) assert buffer1.handle != buffer2.handle assert buffer1.size == 1024 assert buffer2.size == 2048 @@ -974,8 +1030,8 @@ def test_device_memory_resource_with_options(init_cuda): buffer.close(stream) # Test memory copying between buffers from same pool - src_buffer = mr.allocate(64) - dst_buffer = mr.allocate(64) + src_buffer = mr.allocate(64, stream=device.default_stream) + dst_buffer = mr.allocate(64, stream=device.default_stream) stream = device.create_stream() src_buffer.copy_to(dst_buffer, stream=stream) device.sync() @@ -998,15 +1054,15 @@ def test_pinned_memory_resource_with_options(init_cuda): assert not mr.is_ipc_enabled # Test allocation and deallocation - buffer1 = mr.allocate(1024) + buffer1 = mr.allocate(1024, stream=device.default_stream) assert buffer1.handle != 0 assert buffer1.size == 1024 assert buffer1.memory_resource == mr buffer1.close() # Test multiple allocations - buffer1 = mr.allocate(1024) - buffer2 = mr.allocate(2048) + buffer1 = mr.allocate(1024, stream=device.default_stream) + buffer2 = mr.allocate(2048, stream=device.default_stream) assert buffer1.handle != buffer2.handle assert buffer1.size == 1024 assert buffer2.size == 2048 @@ -1020,8 +1076,8 @@ def test_pinned_memory_resource_with_options(init_cuda): buffer.close(stream) # Test memory copying between buffers from same pool - src_buffer = mr.allocate(64) - dst_buffer = mr.allocate(64) + src_buffer = mr.allocate(64, stream=device.default_stream) + dst_buffer = mr.allocate(64, stream=device.default_stream) stream = device.create_stream() src_buffer.copy_to(dst_buffer, stream=stream) device.sync() @@ -1043,15 +1099,15 @@ def test_managed_memory_resource_with_options(init_cuda): assert not mr.is_ipc_enabled # Test allocation and deallocation - buffer1 = mr.allocate(1024) + buffer1 = mr.allocate(1024, stream=device.default_stream) assert buffer1.handle != 0 assert buffer1.size == 1024 assert buffer1.memory_resource == mr buffer1.close() # Test multiple allocations - buffer1 = mr.allocate(1024) - buffer2 = mr.allocate(2048) + buffer1 = mr.allocate(1024, stream=device.default_stream) + buffer2 = mr.allocate(2048, stream=device.default_stream) assert buffer1.handle != buffer2.handle assert buffer1.size == 1024 assert buffer2.size == 2048 @@ -1065,8 +1121,8 @@ def test_managed_memory_resource_with_options(init_cuda): buffer.close(stream) # Test memory copying between buffers from same pool - src_buffer = mr.allocate(64) - dst_buffer = mr.allocate(64) + src_buffer = mr.allocate(64, stream=device.default_stream) + dst_buffer = mr.allocate(64, stream=device.default_stream) stream = device.create_stream() src_buffer.copy_to(dst_buffer, stream=stream) device.sync() @@ -1228,7 +1284,7 @@ def test_mempool_ipc_errors(mempool_device): device = mempool_device options = DeviceMemoryResourceOptions(max_size=POOL_SIZE, ipc_enabled=False) mr = DeviceMemoryResource(device, options=options) - buffer = mr.allocate(64) + buffer = mr.allocate(64, stream=device.default_stream) ipc_error_msg = "Memory resource is not IPC-enabled" with pytest.raises(RuntimeError, match=ipc_error_msg): @@ -1239,7 +1295,7 @@ def test_mempool_ipc_errors(mempool_device): with pytest.raises(RuntimeError, match=ipc_error_msg): handle = IPCBufferDescriptor._init(b"", 0) - Buffer.from_ipc_descriptor(mr, handle) + Buffer.from_ipc_descriptor(mr, handle, stream=device.default_stream) buffer.close() @@ -1271,7 +1327,7 @@ def test_pinned_mempool_ipc_basic(): assert alloc_handle is not None # Test buffer allocation - buffer = mr.allocate(1024) + buffer = mr.allocate(1024, stream=device.default_stream) assert buffer.size == 1024 assert buffer.is_device_accessible assert buffer.is_host_accessible @@ -1299,7 +1355,7 @@ def test_pinned_mempool_ipc_errors(): assert mr.device_id == -1 assert mr.numa_id == -1 # Non-IPC uses OS-managed placement - buffer = mr.allocate(64) + buffer = mr.allocate(64, stream=device.default_stream) ipc_error_msg = "Memory resource is not IPC-enabled" with pytest.raises(RuntimeError, match=ipc_error_msg): @@ -1310,7 +1366,7 @@ def test_pinned_mempool_ipc_errors(): with pytest.raises(RuntimeError, match=ipc_error_msg): handle = IPCBufferDescriptor._init(b"", 0) - Buffer.from_ipc_descriptor(mr, handle) + Buffer.from_ipc_descriptor(mr, handle, stream=device.default_stream) buffer.close() mr.close() @@ -1451,7 +1507,7 @@ def test_mempool_attributes(ipc_enabled, memory_resource_factory, property_name, initial_value = value buffer = None try: - buffer = mr.allocate(1024) + buffer = mr.allocate(1024, stream=device.default_stream) new_value = getattr(mr.attributes, property_name) assert new_value >= initial_value, f"{property_name} should increase or stay same after allocation" finally: @@ -1487,8 +1543,8 @@ def test_mempool_attributes_repr(memory_resource_factory): elif MR is ManagedMemoryResource: mr = create_managed_memory_resource_or_skip(options={}) - buffer1 = mr.allocate(64) - buffer2 = mr.allocate(64) + buffer1 = mr.allocate(64, stream=device.default_stream) + buffer2 = mr.allocate(64, stream=device.default_stream) buffer1.close() assert re.match( r".*Attributes\(release_threshold=\d+, reserved_mem_current=\d+, reserved_mem_high=\d+, " @@ -1598,7 +1654,7 @@ def test_memory_resource_alloc_zero_bytes(init_cuda, memory_resource_factory): assert MR is DeviceMemoryResource mr = MR(device) - buffer = mr.allocate(0) + buffer = mr.allocate(0, stream=device.default_stream) device.sync() assert buffer.handle >= 0 assert buffer.size == 0 diff --git a/cuda_core/tests/test_memory_peer_access.py b/cuda_core/tests/test_memory_peer_access.py index 1a790645864..04324ceec81 100644 --- a/cuda_core/tests/test_memory_peer_access.py +++ b/cuda_core/tests/test_memory_peer_access.py @@ -18,7 +18,7 @@ def test_peer_access_basic(mempool_device_x2): stream_on_dev0 = dev0.create_stream() # Use owned pool to ensure clean initial state (no stale peer access). dmr_on_dev1 = DeviceMemoryResource(dev1, DeviceMemoryResourceOptions()) - buf_on_dev1 = dmr_on_dev1.allocate(NBYTES) + buf_on_dev1 = dmr_on_dev1.allocate(NBYTES, stream=dev1.default_stream) # No access at first. assert 0 not in dmr_on_dev1.peer_accessible_by @@ -69,7 +69,7 @@ def test_peer_access_transitions(mempool_device_x3): # Use owned pools (with options) to ensure clean initial state. # Default pools are shared and may have stale peer access from prior tests. dmrs = [DeviceMemoryResource(dev, DeviceMemoryResourceOptions()) for dev in devs] - bufs = [dmr.allocate(NBYTES) for dmr in dmrs] + bufs = [dmr.allocate(NBYTES, stream=dev.default_stream) for dmr, dev in zip(dmrs, devs)] def verify_state(state, pattern_seed): """ diff --git a/cuda_core/tests/test_module.py b/cuda_core/tests/test_module.py index 58f09564971..5c994b6f5e3 100644 --- a/cuda_core/tests/test_module.py +++ b/cuda_core/tests/test_module.py @@ -488,9 +488,8 @@ def test_occupancy_max_active_clusters(get_saxpy_kernel_cubin, cluster): pytest.skip("Device with compute capability 90 or higher is required for cluster support") launch_config = cuda.core.LaunchConfig(grid=128, block=64, cluster=cluster) query_fn = kernel.occupancy.max_active_clusters - max_active_clusters = query_fn(launch_config) - assert isinstance(max_active_clusters, int) - assert max_active_clusters >= 0 + with pytest.raises(TypeError, match=r"keyword-only argument"): + query_fn(launch_config) max_active_clusters = query_fn(launch_config, stream=dev.default_stream) assert isinstance(max_active_clusters, int) assert max_active_clusters >= 0 @@ -503,9 +502,8 @@ def test_occupancy_max_potential_cluster_size(get_saxpy_kernel_cubin): pytest.skip("Device with compute capability 90 or higher is required for cluster support") launch_config = cuda.core.LaunchConfig(grid=128, block=64) query_fn = kernel.occupancy.max_potential_cluster_size - max_potential_cluster_size = query_fn(launch_config) - assert isinstance(max_potential_cluster_size, int) - assert max_potential_cluster_size >= 0 + with pytest.raises(TypeError, match=r"keyword-only argument"): + query_fn(launch_config) max_potential_cluster_size = query_fn(launch_config, stream=dev.default_stream) assert isinstance(max_potential_cluster_size, int) assert max_potential_cluster_size >= 0 @@ -685,7 +683,7 @@ def get_kernel_only(): result = np.from_dlpack(host_buf).view(np.int32) result[:] = 0 - dev_buf = device.memory_resource.allocate(4) + dev_buf = device.memory_resource.allocate(4, stream=device.default_stream) # Launch kernel config = cuda.core.LaunchConfig(grid=1, block=1) diff --git a/cuda_core/tests/test_object_protocols.py b/cuda_core/tests/test_object_protocols.py index 457debc0903..72f7891a711 100644 --- a/cuda_core/tests/test_object_protocols.py +++ b/cuda_core/tests/test_object_protocols.py @@ -68,7 +68,7 @@ def sample_context(sample_device): @pytest.fixture def sample_buffer(sample_device): """A sample Buffer object.""" - return sample_device.allocate(64) + return sample_device.allocate(64, stream=sample_device.default_stream) @pytest.fixture @@ -197,7 +197,7 @@ def sample_context_alt(sample_device_alt): @pytest.fixture def sample_buffer_alt(sample_device): """An alternate Buffer object.""" - return sample_device.allocate(1024) + return sample_device.allocate(1024, stream=sample_device.default_stream) @pytest.fixture @@ -231,7 +231,7 @@ def sample_ipc_buffer_descriptor(ipc_device): """An IPCBufferDescriptor.""" options = DeviceMemoryResourceOptions(max_size=POOL_SIZE, ipc_enabled=True) mr = DeviceMemoryResource(ipc_device, options=options) - buf = mr.allocate(64) + buf = mr.allocate(64, stream=ipc_device.default_stream) return buf.ipc_descriptor diff --git a/cuda_core/tests/test_stream.py b/cuda_core/tests/test_stream.py index 6c843bca004..4e5813ee226 100644 --- a/cuda_core/tests/test_stream.py +++ b/cuda_core/tests/test_stream.py @@ -6,7 +6,7 @@ from cuda.core import Device, Stream, StreamOptions from cuda.core._event import Event -from cuda.core._stream import LEGACY_DEFAULT_STREAM, PER_THREAD_DEFAULT_STREAM +from cuda.core._stream import LEGACY_DEFAULT_STREAM, PER_THREAD_DEFAULT_STREAM, Stream_accept from cuda.core._utils.cuda_utils import driver @@ -15,6 +15,13 @@ def test_stream_init_disabled(): Stream() # Reject at front door. +def test_stream_accept_rejects_none(): + """Stream_accept(None) raises TypeError so APIs cannot silently fall back + to the default stream (issue #2001).""" + with pytest.raises(TypeError, match=r"stream is required and must not be None"): + Stream_accept(None) + + def test_stream_init_with_options(init_cuda): stream = Device().create_stream(options=StreamOptions(nonblocking=True, priority=0)) assert stream.is_nonblocking is True diff --git a/cuda_core/tests/test_tensor_map.py b/cuda_core/tests/test_tensor_map.py index 2596a9daf82..3ec4cb5b020 100644 --- a/cuda_core/tests/test_tensor_map.py +++ b/cuda_core/tests/test_tensor_map.py @@ -112,7 +112,7 @@ def test_cannot_instantiate_directly(self): TensorMapDescriptor() def test_from_tiled_1d(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) # 1024 float32 elements + buf = dev.allocate(1024 * 4, stream=dev.default_stream) # 1024 float32 elements desc = _as_view(buf).as_tensor_map( box_dim=(64,), data_type=TensorMapDataType.FLOAT32, @@ -121,7 +121,7 @@ def test_from_tiled_1d(self, dev, skip_if_no_tma): assert repr(desc) == "TensorMapDescriptor(tiled, rank=1, dtype=FLOAT32, swizzle=NONE)" def test_device_property(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) desc = _as_view(buf).as_tensor_map( box_dim=(64,), data_type=TensorMapDataType.FLOAT32, @@ -129,7 +129,7 @@ def test_device_property(self, dev, skip_if_no_tma): assert desc.device.device_id == dev.device_id def test_from_tiled_2d(self, dev, skip_if_no_tma): - buf = dev.allocate(64 * 64 * 4) # 64x64 float32 + buf = dev.allocate(64 * 64 * 4, stream=dev.default_stream) # 64x64 float32 tensor = _DeviceArray(buf, (64, 64)) desc = _as_view(tensor).as_tensor_map( box_dim=(32, 32), @@ -138,7 +138,7 @@ def test_from_tiled_2d(self, dev, skip_if_no_tma): assert desc is not None def test_strided_memory_view_as_tensor_map(self, dev, skip_if_no_tma): - buf = dev.allocate(64 * 64 * 4) + buf = dev.allocate(64 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (64, 64)) view = StridedMemoryView.from_any_interface(tensor, stream_ptr=-1) desc = view.as_tensor_map( @@ -148,7 +148,7 @@ def test_strided_memory_view_as_tensor_map(self, dev, skip_if_no_tma): assert desc is not None def test_strided_memory_view_as_tensor_map_options(self, dev, skip_if_no_tma): - buf = dev.allocate(64 * 64 * 4) + buf = dev.allocate(64 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (64, 64)) view = StridedMemoryView.from_any_interface(tensor, stream_ptr=-1) desc = view.as_tensor_map( @@ -161,7 +161,7 @@ def test_strided_memory_view_as_tensor_map_options(self, dev, skip_if_no_tma): assert desc is not None def test_strided_memory_view_as_tensor_map_options_dict(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) desc = _as_view(buf).as_tensor_map( options={ "box_dim": (64,), @@ -172,7 +172,7 @@ def test_strided_memory_view_as_tensor_map_options_dict(self, dev, skip_if_no_tm assert desc is not None def test_strided_memory_view_as_tensor_map_rejects_options_with_kwargs(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) with pytest.raises(TypeError, match="Specify either options or the individual tensor map arguments"): _as_view(buf).as_tensor_map( box_dim=(64,), @@ -180,7 +180,7 @@ def test_strided_memory_view_as_tensor_map_rejects_options_with_kwargs(self, dev ) def test_from_tiled_3d(self, dev, skip_if_no_tma): - buf = dev.allocate(16 * 16 * 16 * 4) # 16x16x16 float32 + buf = dev.allocate(16 * 16 * 16 * 4, stream=dev.default_stream) # 16x16x16 float32 tensor = _DeviceArray(buf, (16, 16, 16)) desc = _as_view(tensor).as_tensor_map( box_dim=(8, 8, 8), @@ -192,7 +192,7 @@ def test_from_tiled_5d(self, dev, skip_if_no_tma): # 5D: exercises all 5 c_global_dim / 4 c_global_strides slots shape = (2, 4, 4, 4, 8) n_bytes = 2 * 4 * 4 * 4 * 8 * 4 # float32 - buf = dev.allocate(n_bytes) + buf = dev.allocate(n_bytes, stream=dev.default_stream) tensor = _DeviceArray(buf, shape) desc = _as_view(tensor).as_tensor_map( box_dim=(1, 2, 2, 2, 8), @@ -201,7 +201,7 @@ def test_from_tiled_5d(self, dev, skip_if_no_tma): def test_from_tiled_with_element_strides_buffer(self, dev, skip_if_no_tma): # Use a Buffer input (DLPack path) and explicit element_strides. - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) desc = _as_view(buf).as_tensor_map( box_dim=(64,), element_strides=(2,), @@ -211,7 +211,7 @@ def test_from_tiled_with_element_strides_buffer(self, dev, skip_if_no_tma): def test_from_tiled_with_element_strides_cai(self, dev, skip_if_no_tma): # Use a CAI-style tensor wrapper and explicit element_strides. - buf = dev.allocate(64 * 64 * 4) + buf = dev.allocate(64 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (64, 64)) desc = _as_view(tensor).as_tensor_map( box_dim=(32, 32), @@ -221,7 +221,7 @@ def test_from_tiled_with_element_strides_cai(self, dev, skip_if_no_tma): assert desc is not None def test_from_tiled_with_swizzle(self, dev, skip_if_no_tma): - buf = dev.allocate(64 * 64 * 4) + buf = dev.allocate(64 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (64, 64)) desc = _as_view(tensor).as_tensor_map( box_dim=(32, 32), @@ -231,7 +231,7 @@ def test_from_tiled_with_swizzle(self, dev, skip_if_no_tma): assert desc is not None def test_from_tiled_with_l2_promotion(self, dev, skip_if_no_tma): - buf = dev.allocate(64 * 64 * 4) + buf = dev.allocate(64 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (64, 64)) desc = _as_view(tensor).as_tensor_map( box_dim=(32, 32), @@ -241,7 +241,7 @@ def test_from_tiled_with_l2_promotion(self, dev, skip_if_no_tma): assert desc is not None def test_from_tiled_with_oob_fill(self, dev, skip_if_no_tma): - buf = dev.allocate(64 * 64 * 4) + buf = dev.allocate(64 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (64, 64)) desc = _as_view(tensor).as_tensor_map( box_dim=(32, 32), @@ -255,7 +255,7 @@ class TestTensorMapDescriptorValidation: """Test validation in TensorMapDescriptor factory methods.""" def test_invalid_rank_zero(self, dev, skip_if_no_tma): - buf = dev.allocate(64) + buf = dev.allocate(64, stream=dev.default_stream) tensor = _DeviceArray(buf, ()) # 0-dim tensor with pytest.raises(ValueError, match="rank must be between 1 and 5"): _as_view(tensor).as_tensor_map( @@ -268,7 +268,7 @@ def test_invalid_rank_six(self, dev, skip_if_no_tma): n_elements = 1 for s in shape: n_elements *= s - buf = dev.allocate(n_elements * 4) + buf = dev.allocate(n_elements * 4, stream=dev.default_stream) arr = _DeviceArray(buf, shape) with pytest.raises(ValueError, match="rank must be between 1 and 5"): _as_view(arr).as_tensor_map( @@ -276,7 +276,7 @@ def test_invalid_rank_six(self, dev, skip_if_no_tma): ) def test_box_dim_rank_mismatch(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) with pytest.raises(ValueError, match="box_dim must have 1 elements"): _as_view(buf).as_tensor_map( box_dim=(32, 32), @@ -284,7 +284,7 @@ def test_box_dim_rank_mismatch(self, dev, skip_if_no_tma): ) def test_box_dim_out_of_range(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) with pytest.raises(ValueError, match=r"box_dim\[0\] must be in \[1, 256\]"): _as_view(buf).as_tensor_map( box_dim=(512,), @@ -292,7 +292,7 @@ def test_box_dim_out_of_range(self, dev, skip_if_no_tma): ) def test_element_strides_rank_mismatch(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) with pytest.raises(ValueError, match="element_strides must have 1 elements"): _as_view(buf).as_tensor_map( box_dim=(64,), @@ -301,7 +301,7 @@ def test_element_strides_rank_mismatch(self, dev, skip_if_no_tma): ) def test_invalid_data_type(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) with pytest.raises(TypeError, match="data_type must be"): _as_view(buf).as_tensor_map( box_dim=(64,), @@ -346,18 +346,18 @@ class TestTensorMapReplaceAddress: """Test replace_address functionality.""" def test_replace_address(self, dev, skip_if_no_tma): - buf1 = dev.allocate(1024 * 4) + buf1 = dev.allocate(1024 * 4, stream=dev.default_stream) desc = _as_view(buf1).as_tensor_map( box_dim=(64,), data_type=TensorMapDataType.FLOAT32, ) - buf2 = dev.allocate(1024 * 4) + buf2 = dev.allocate(1024 * 4, stream=dev.default_stream) desc.replace_address(buf2) # No exception means success def test_replace_address_requires_device_accessible(self, dev, skip_if_no_tma): - buf1 = dev.allocate(1024 * 4) + buf1 = dev.allocate(1024 * 4, stream=dev.default_stream) desc = _as_view(buf1).as_tensor_map( box_dim=(64,), data_type=TensorMapDataType.FLOAT32, @@ -375,14 +375,14 @@ def test_replace_address_rejects_tensor_from_other_device(self, dev, skip_if_no_ dev1 = Device(1) dev0.set_current() - buf0 = dev0.allocate(1024 * 4) + buf0 = dev0.allocate(1024 * 4, stream=dev0.default_stream) desc = _as_view(buf0).as_tensor_map( box_dim=(64,), data_type=TensorMapDataType.FLOAT32, ) dev1.set_current() - buf1 = dev1.allocate(1024 * 4) + buf1 = dev1.allocate(1024 * 4, stream=dev1.default_stream) dev0.set_current() with pytest.raises(ValueError, match=r"replace_address expects tensor on device 0, got 1"): @@ -398,13 +398,13 @@ def test_replace_address_accepts_managed_buffer_on_nonzero_device(self, init_cud skip_if_managed_memory_unsupported(dev1) dev1.set_current() - desc = _as_view(dev1.allocate(1024 * 4)).as_tensor_map( + desc = _as_view(dev1.allocate(1024 * 4, stream=dev1.default_stream)).as_tensor_map( box_dim=(64,), data_type=TensorMapDataType.FLOAT32, ) mr = create_managed_memory_resource_or_skip(ManagedMemoryResourceOptions(preferred_location=dev1.device_id)) - managed_buf = mr.allocate(1024 * 4) + managed_buf = mr.allocate(1024 * 4, stream=dev1.default_stream) desc.replace_address(managed_buf) @@ -420,7 +420,7 @@ def test_from_tiled_rejects_tensor_from_other_device(self, init_cuda): dev1 = Device(1) dev1.set_current() - buf1 = dev1.allocate(1024 * 4) + buf1 = dev1.allocate(1024 * 4, stream=dev1.default_stream) dev0.set_current() with pytest.raises( @@ -443,7 +443,7 @@ def test_from_tiled_accepts_managed_buffer_on_nonzero_device(self, init_cuda): dev1.set_current() mr = create_managed_memory_resource_or_skip(ManagedMemoryResourceOptions(preferred_location=dev1.device_id)) - managed_buf = mr.allocate(1024 * 4) + managed_buf = mr.allocate(1024 * 4, stream=dev1.default_stream) desc = _as_view(managed_buf).as_tensor_map( box_dim=(64,), @@ -474,7 +474,7 @@ class TestTensorMapIm2col: def test_from_im2col_3d(self, dev, skip_if_no_tma): # 3D tensor: batch=1, height=32, channels=64 - buf = dev.allocate(1 * 32 * 64 * 4) + buf = dev.allocate(1 * 32 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (1, 32, 64)) desc = TensorMapDescriptor._from_im2col( _as_view(tensor), @@ -487,7 +487,7 @@ def test_from_im2col_3d(self, dev, skip_if_no_tma): assert desc is not None def test_from_im2col_rank_validation(self, dev, skip_if_no_tma): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) with pytest.raises(ValueError, match="Im2col tensor rank must be between 3 and 5"): TensorMapDescriptor._from_im2col( _as_view(buf), @@ -499,7 +499,7 @@ def test_from_im2col_rank_validation(self, dev, skip_if_no_tma): ) def test_from_im2col_corner_rank_mismatch(self, dev, skip_if_no_tma): - buf = dev.allocate(1 * 32 * 64 * 4) + buf = dev.allocate(1 * 32 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (1, 32, 64)) # 3D: n_spatial = 1 with pytest.raises(ValueError, match="pixel_box_lower_corner must have 1 elements"): TensorMapDescriptor._from_im2col( @@ -516,7 +516,7 @@ def test_from_im2col_4d(self, dev, skip_if_no_tma): # Exercises spatial corner reversal with n_spatial=2: # Python [H_lower, W_lower] -> driver [W_lower, H_lower] shape = (1, 8, 8, 64) - buf = dev.allocate(1 * 8 * 8 * 64 * 4) + buf = dev.allocate(1 * 8 * 8 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, shape) desc = TensorMapDescriptor._from_im2col( _as_view(tensor), @@ -532,7 +532,7 @@ def test_from_im2col_5d(self, dev, skip_if_no_tma): # Exercises the full spatial corner reversal: # Python [D, H, W] -> driver [W, H, D] shape = (1, 4, 8, 8, 64) - buf = dev.allocate(1 * 4 * 8 * 8 * 64 * 4) + buf = dev.allocate(1 * 4 * 8 * 8 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, shape) desc = TensorMapDescriptor._from_im2col( _as_view(tensor), @@ -558,7 +558,7 @@ def skip_if_no_im2col_wide(self, dev): # or with driver/GPU combinations that reject im2col-wide descriptor # encoding for otherwise valid inputs. Probe once per test invocation # and skip only for those known unsupported cases. - buf = dev.allocate(1 * 32 * 64 * 4) + buf = dev.allocate(1 * 32 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (1, 32, 64)) try: TensorMapDescriptor._from_im2col_wide( @@ -580,7 +580,7 @@ def skip_if_no_im2col_wide(self, dev): def test_from_im2col_wide_3d(self, dev, skip_if_no_im2col_wide): # 3D tensor: batch=1, width=32, channels=64 - buf = dev.allocate(1 * 32 * 64 * 4) + buf = dev.allocate(1 * 32 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, (1, 32, 64)) desc = TensorMapDescriptor._from_im2col_wide( _as_view(tensor), @@ -596,7 +596,7 @@ def test_from_im2col_wide_4d(self, dev, skip_if_no_im2col_wide): # NHWC layout: N=1, H=8, W=8, C=64 # Wide mode only uses scalar W corners, even with higher rank shape = (1, 8, 8, 64) - buf = dev.allocate(1 * 8 * 8 * 64 * 4) + buf = dev.allocate(1 * 8 * 8 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, shape) desc = TensorMapDescriptor._from_im2col_wide( _as_view(tensor), @@ -611,7 +611,7 @@ def test_from_im2col_wide_5d(self, dev, skip_if_no_im2col_wide): # NDHWC layout: N=1, D=4, H=8, W=8, C=64 # Max rank boundary — verifies all 5 dim/stride slots are filled shape = (1, 4, 8, 8, 64) - buf = dev.allocate(1 * 4 * 8 * 8 * 64 * 4) + buf = dev.allocate(1 * 4 * 8 * 8 * 64 * 4, stream=dev.default_stream) tensor = _DeviceArray(buf, shape) desc = TensorMapDescriptor._from_im2col_wide( _as_view(tensor), @@ -623,7 +623,7 @@ def test_from_im2col_wide_5d(self, dev, skip_if_no_im2col_wide): assert desc is not None def test_from_im2col_wide_rank_validation(self, dev, skip_if_no_im2col_wide): - buf = dev.allocate(1024 * 4) + buf = dev.allocate(1024 * 4, stream=dev.default_stream) with pytest.raises(ValueError, match="Im2col-wide tensor rank must be between 3 and 5"): TensorMapDescriptor._from_im2col_wide( _as_view(buf), diff --git a/cuda_core/tests/test_utils.py b/cuda_core/tests/test_utils.py index 5bcdead92c6..18379cd7a24 100644 --- a/cuda_core/tests/test_utils.py +++ b/cuda_core/tests/test_utils.py @@ -316,7 +316,7 @@ def test_strided_memory_view_dlpack_export_cupy_roundtrip(init_cuda): def test_strided_memory_view_dlpack_export_requires_dtype(init_cuda): - buffer = init_cuda.memory_resource.allocate(16) + buffer = init_cuda.memory_resource.allocate(16, stream=init_cuda.default_stream) view = StridedMemoryView.from_buffer( buffer, shape=(16,), @@ -393,7 +393,7 @@ def test_from_buffer(shape, dtype, stride_order, readonly): layout = _StridedLayout.dense(shape=shape, itemsize=dtype.itemsize, stride_order=stride_order) required_size = layout.required_size_in_bytes() assert required_size == math.prod(shape) * dtype.itemsize - buffer = dev.memory_resource.allocate(required_size) + buffer = dev.memory_resource.allocate(required_size, stream=dev.default_stream) view = StridedMemoryView.from_buffer(buffer, shape=shape, strides=layout.strides, dtype=dtype, is_readonly=readonly) assert view.exporting_obj is buffer assert view._layout == layout @@ -417,7 +417,7 @@ def test_from_buffer_incompatible_dtype_and_itemsize(dtype, itemsize, msg): layout = _StridedLayout.dense((5,), 2) device = Device() device.set_current() - buffer = device.memory_resource.allocate(layout.required_size_in_bytes()) + buffer = device.memory_resource.allocate(layout.required_size_in_bytes(), stream=device.default_stream) with pytest.raises(ValueError, match=msg): StridedMemoryView.from_buffer(buffer, (5,), dtype=dtype, itemsize=itemsize) @@ -427,7 +427,7 @@ def test_from_buffer_sliced(stride_order): layout = _StridedLayout.dense((5, 7), 2, stride_order=stride_order) device = Device() device.set_current() - buffer = device.memory_resource.allocate(layout.required_size_in_bytes()) + buffer = device.memory_resource.allocate(layout.required_size_in_bytes(), stream=device.default_stream) view = StridedMemoryView.from_buffer(buffer, (5, 7), dtype=np.dtype(np.int16)) assert view.shape == (5, 7) assert int(buffer.handle) == view.ptr @@ -445,7 +445,7 @@ def test_from_buffer_too_small(): layout = _StridedLayout.dense((5, 4), 2) d = Device() d.set_current() - buffer = d.memory_resource.allocate(20) + buffer = d.memory_resource.allocate(20, stream=d.default_stream) with pytest.raises(ValueError, match="Expected at least 40 bytes, got 20 bytes."): StridedMemoryView.from_buffer( buffer, @@ -459,7 +459,7 @@ def test_from_buffer_disallowed_negative_offset(): layout = _StridedLayout((5, 4), (-4, 1), 1) d = Device() d.set_current() - buffer = d.memory_resource.allocate(20) + buffer = d.memory_resource.allocate(20, stream=d.default_stream) with pytest.raises(ValueError): StridedMemoryView.from_buffer( buffer, @@ -591,7 +591,7 @@ def test_from_buffer_with_non_power_of_two_itemsize(): layout = _StridedLayout(shape=shape, strides=None, itemsize=dtype.itemsize) required_size = layout.required_size_in_bytes() assert required_size == math.prod(shape) * dtype.itemsize - buffer = dev.memory_resource.allocate(required_size) + buffer = dev.memory_resource.allocate(required_size, stream=dev.default_stream) view = StridedMemoryView.from_buffer(buffer, shape=shape, strides=layout.strides, dtype=dtype, is_readonly=True) assert view.dtype == dtype From 3432b280e6874b910d0a5ec0150bc6930e735513 Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Wed, 6 May 2026 15:15:14 -0700 Subject: [PATCH 078/244] cuda.core: convert peer_accessible_by to a live MutableSet view (#2018) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * cuda.core: convert peer_accessible_by to a live MutableSet view DeviceMemoryResource.peer_accessible_by previously returned a sorted tuple[int, ...] backed by a Python-level cache, which was prone to divergence from driver state across multiple wrappers around the same memory pool. The setter accepted Device | int and emitted a single batched cuMemPoolSetAccess covering the diff against the cache. This commit replaces the property with a live driver-backed view: - Adds PeerAccessibleBySetProxy in _memory/_peer_access_utils.py, a collections.abc.MutableSet whose reads call cuMemPoolGetAccess and whose writes call cuMemPoolSetAccess. Iteration yields Device objects; add, discard, and __contains__ accept either a Device or a device-ordinal int. The proxy is constructed fresh on every property access, so there is nothing to cache or pickle. - Drops the _peer_accessible_by cache field (and its initializations in __cinit__, _DMR_init, and from_allocation_handle), eliminating the owned/non-owned read split. All pools now share the same code path and always query the driver. - All bulk operations on the proxy (update, |=, &=, -=, ^=, clear, pop) issue exactly one cuMemPoolSetAccess call. Peer-access transitions can take seconds per pool because every existing memory mapping is updated, so coalescing into a single driver call lets the toolkit handle the mappings in parallel. The property setter (mr.peer_accessible_by = [...]) preserves its original single-call behavior via the same shared planner path. - Single-element add validates can_access_peer through plan_peer_access_update, matching the existing setter contract. This is a breaking change captured in the v1.0.0 release notes. Callers comparing against tuples must update to set comparisons (mr.peer_accessible_by == {Device(0)}). Existing tests are migrated; new tests for set-interface conformance are intentionally deferred to a follow-up. Co-authored-by: Cursor * cuda.core: move peer-access internals into a single _peer_access_utils.pyx The previous commit left DeviceMemoryResource carrying three pass-through def methods (_query_peer_access_ids, _peer_access_includes, _apply_peer_access_diff) whose only purpose was to give the pure-Python proxy in _peer_access_utils.py a way to call cdef helpers in _device_memory_resource.pyx. These methods served no public role and cluttered the class API. Promote _peer_access_utils.py to a Cython module so the proxy and the driver-touching helpers can live together: - Convert _peer_access_utils.py to _peer_access_utils.pyx. cimports cydriver and DeviceMemoryResource from the .pxd; uses nogil and direct CUmemAccessDesc packing identically to before. - Move _DMR_query_peer_access_ids, _DMR_peer_access_includes, _DMR_apply_peer_access_diff, and _DMR_replace_peer_accessible_by from _device_memory_resource.pyx into the new module as cdef helpers (and a cpdef replace_peer_accessible_by entry point used by the property setter). - Drop the three pass-through def methods from DeviceMemoryResource. The class is left with the property getter and setter only; everything else is module-level in _peer_access_utils. - The proxy now calls the module-level cdef helpers directly instead of routing through methods on mr. No behavior change. The public surface (PeerAccessibleBySetProxy, plan_peer_access_update, normalize_peer_access_targets, PeerAccessPlan) is preserved at the same import paths. Co-authored-by: Cursor * cuda.core: tighten peer-access query loop and 1.0.0 release note Refactor _query_peer_access_ids so the entire driver loop runs inside a single nogil block instead of acquiring/releasing the GIL once per device. The flag query now uses a cached as_cu(mr._h_pool) handle and fills a libcpp.vector[int]; because range(total) ascends, the result is already sorted and the trailing sorted() call is dropped. Also tighten the peer_accessible_by entry in 1.0.0-notes.rst: the breaking-change blurb only needs to state the type/element change, so remove the implementation-flavored details about input acceptance and batched cuMemPoolSetAccess calls. Co-authored-by: Cursor * cuda.core: cover PeerAccessibleBySetProxy interface, batching, and edge cases Existing peer-access tests covered the integration path well (real copies across peers, the full transition matrix, shared-pool consistency) but only touched ``in``, ``==``, and the property setter on the new set proxy. After the v1.0.0 break that surfaced ~25 ``MutableSet`` methods, nothing was pinning the type-coercion contract, the owner-filtering behavior, the ``KeyError``/value-error paths, or the "one ``cuMemPoolSetAccess`` per bulk op" performance invariant. Add the following coverage in ``test_memory_peer_access.py``: - A ``MutableSet`` conformance test using a relaxed ``assert_mutable_set_interface`` mode that admits subjects holding at most one insertable element. CI maxes at two GPUs (one peer), so the multi-element protocol pass cannot run there. The new ``support_multi_insert=False`` path takes one insertable item plus two non-member sentinels and exercises every ``MutableSet`` method (``add``/``discard``/``remove``/``pop``/``clear``/``update``, comparisons, isdisjoint, subset/superset, binary and in-place operators, ``__iter__``/``__len__``/``__repr__``). - ``Device``/``int`` interchangeability on ``add``/``discard``/``__contains__``. - The owner-device filtering contract on every write (silent no-op). - Error paths: ``add(out_of_range)`` and ``add(non_coercible)`` raise while the lenient ``discard``/``__contains__`` paths swallow the same inputs; ``remove(non_member)`` raises ``KeyError``. - "Live driver view" semantics: a proxy obtained before another wrapper modifies the pool reflects the change with no refresh step. - ``__iter__`` ordering is ascending by ``device_id`` and elements are ``Device`` instances; ``__repr__`` includes the class name and tracks live contents; the getter returns the documented proxy type. - A batching spy that monkeypatches the module-level ``_apply_peer_access_diff`` and asserts that every bulk op (``|=``/``&=``/``-=``/``^=``/``update``/``difference_update``/ ``clear``) and the property setter issues at most one driver call, zero when the diff is empty. To make the spy possible, ``_apply_peer_access_diff`` is now a Python-visible ``def`` wrapper around a renamed ``_apply_peer_access_diff_cython`` ``cdef inline``. The proxy and the property setter still call ``_apply_peer_access_diff`` by bare name, which Cython resolves through the module's globals at runtime, so a ``monkeypatch.setattr(_peer_access_utils, "_apply_peer_access_diff", ...)`` intercepts them. The extra Python-level dispatch is negligible next to ``cuMemPoolSetAccess`` itself. Co-authored-by: Cursor * cuda.core: filter empty-delta calls in peer-access batching spy Augmented assignment on the ``peer_accessible_by`` property (``dmr.peer_accessible_by |= {...}``) is two trips through the proxy/setter pair, not one: Python fetches the proxy, the proxy mutates itself in place via ``__ior__``, and Python then assigns the (already-mutated) proxy back through the setter. That trailing setter call computes the diff against current driver state, finds it empty, and short-circuits inside the ``cdef inline`` before issuing any ``cuMemPoolSetAccess`` work — so the *driver-level* contract ("one batched call per bulk op") still holds, but the wrapper is invoked twice, which the spy was counting. Also, the fixture's ``dmr.peer_accessible_by = []`` reset on an already empty pool is itself an empty-delta wrapper call. Filter the recorded calls down to those with non-empty deltas (the ones that translate to real driver work) and switch the bulk-ops test to use a locally bound proxy so augmented assignment goes through ``__ior__`` once with no extra setter invocation. The setter test stays on ``dmr.peer_accessible_by = ...`` because that is the public API contract under test there. Co-authored-by: Cursor * cuda.core: spy on the driver call directly to assert peer-access batching Move the actual ``cuMemPoolSetAccess`` invocation (descriptor-array build + driver call) into a thin Python-visible ``def _set_pool_access`` in ``_peer_access_utils.pyx``. ``_apply_peer_access_diff`` now does only the empty-diff short-circuit and delegates the work to ``_set_pool_access``, which Cython resolves through the module globals at runtime so tests can intercept it via ``monkeypatch.setattr``. Replace the previous internal-wrapper spy with a driver-call spy that counts every real ``cuMemPoolSetAccess`` invocation. Earlier no-op layers (e.g. the augmented-assignment-on-property pattern that writes an already-mutated proxy back through the setter) short-circuit before reaching ``_set_pool_access``, so the recorded count is exactly the number of driver calls. The empty-delta filter and the local-binding workaround in the bulk-ops test are gone; we now also assert that ``dmr.peer_accessible_by |= {...}`` directly on the property is still exactly one driver call. Co-authored-by: Cursor * cuda.core: split MutableSet conformance helpers by capacity Replace the ``support_multi_insert`` flag and ``non_members`` keyword with two purpose-built helpers: - ``assert_mutable_set_interface(subject, items)`` keeps the original signature and contract: at least five distinct insertable items, exercised against a reference set in the standard way. The graph ``AdjacencySetProxy`` test continues to use this unchanged. - ``assert_single_member_mutable_set_interface(subject, member, non_member)`` is a focused pass for proxies whose backing store admits at most one insertable element at a time (here, the peer-access view on a 2-GPU box). It threads a single member and one non-member sentinel through every ``MutableSet`` method. The two helpers share small private utilities (empty-state checks, ``__repr__`` shape) but keep their public surfaces small and linear. A capacity-one proxy is a meaningfully different contract from a general mutable set; naming that explicitly in the API reads better than a flag and avoids forcing call sites to plumb sentinels through. Co-authored-by: Cursor * Address review feedback on peer_accessible_by proxy - Optimize add/discard to check the single target device (1 driver call) instead of scanning all devices via _query_peer_access_ids - Add test for out-of-range int in __contains__ (returns False) - Fix stale comment referencing removed support_multi_insert flag - Add #2018 link to release notes entry Co-authored-by: Cursor --------- Co-authored-by: Cursor --- cuda_core/cuda/core/_layout.pyx | 2 +- .../core/_memory/_device_memory_resource.pxd | 1 - .../core/_memory/_device_memory_resource.pyx | 107 +---- cuda_core/cuda/core/_memory/_ipc.pyx | 2 +- .../cuda/core/_memory/_peer_access_utils.py | 59 --- .../cuda/core/_memory/_peer_access_utils.pyx | 406 ++++++++++++++++++ cuda_core/cuda/core/_stream.pxd | 2 +- cuda_core/docs/source/api_private.rst | 1 + cuda_core/docs/source/release/1.0.0-notes.rst | 5 + cuda_core/tests/helpers/buffers.py | 2 +- .../helpers/collection_interface_testers.py | 204 ++++++++- .../tests/memory_ipc/test_peer_access.py | 18 +- cuda_core/tests/test_memory_peer_access.py | 300 ++++++++++++- cuda_core/tests/test_stream.py | 2 +- 14 files changed, 913 insertions(+), 198 deletions(-) delete mode 100644 cuda_core/cuda/core/_memory/_peer_access_utils.py create mode 100644 cuda_core/cuda/core/_memory/_peer_access_utils.pyx diff --git a/cuda_core/cuda/core/_layout.pyx b/cuda_core/cuda/core/_layout.pyx index 796a6243fd4..3e2580d11d1 100644 --- a/cuda_core/cuda/core/_layout.pyx +++ b/cuda_core/cuda/core/_layout.pyx @@ -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 diff --git a/cuda_core/cuda/core/_memory/_device_memory_resource.pxd b/cuda_core/cuda/core/_memory/_device_memory_resource.pxd index a7f3bfd9585..0b7cd941e18 100644 --- a/cuda_core/cuda/core/_memory/_device_memory_resource.pxd +++ b/cuda_core/cuda/core/_memory/_device_memory_resource.pxd @@ -9,7 +9,6 @@ from cuda.core._memory._ipc cimport IPCDataForMR cdef class DeviceMemoryResource(_MemPool): cdef: int _dev_id - object _peer_accessible_by cpdef DMR_mempool_get_access(DeviceMemoryResource, int) diff --git a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx index c19d7358b00..b7b8b247a92 100644 --- a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx @@ -18,14 +18,12 @@ from cuda.core._utils.cuda_utils cimport ( check_or_create_options, HANDLE_RETURN, ) -from cpython.mem cimport PyMem_Malloc, PyMem_Free - from dataclasses import dataclass import multiprocessing import platform # no-cython-lint import uuid -from cuda.core._memory._peer_access_utils import plan_peer_access_update +from cuda.core._memory._peer_access_utils import PeerAccessibleBySetProxy, replace_peer_accessible_by from cuda.core._utils.cuda_utils import check_multiprocessing_start_method __all__ = ['DeviceMemoryResource', 'DeviceMemoryResourceOptions'] @@ -131,7 +129,6 @@ cdef class DeviceMemoryResource(_MemPool): def __cinit__(self, *args, **kwargs): self._dev_id = cydriver.CU_DEVICE_INVALID - self._peer_accessible_by = None def __init__(self, device_id: Device | int, options=None): _DMR_init(self, device_id, options) @@ -191,7 +188,6 @@ cdef class DeviceMemoryResource(_MemPool): _ipc.MP_from_allocation_handle(cls, alloc_handle)) from .._device import Device mr._dev_id = Device(device_id).device_id - mr._peer_accessible_by = () return mr @property @@ -217,30 +213,23 @@ cdef class DeviceMemoryResource(_MemPool): pool. Access can be modified at any time and affects all allocations from this memory pool. - Returns a tuple of sorted device IDs that currently have peer access to - allocations from this memory pool. - - When setting, accepts a sequence of :obj:`~_device.Device` objects or device IDs. - Setting to an empty sequence revokes all peer access. - - For non-owned pools (the default or current device pool), the state - is always queried from the driver to reflect changes made by other - wrappers or direct driver calls. + Returns a set-like proxy of :obj:`~_device.Device` objects that manages + peer access. Inputs are accepted as either :obj:`~_device.Device` + objects or device-ordinal :class:`int` values. Examples -------- >>> dmr = DeviceMemoryResource(0) - >>> dmr.peer_accessible_by = [1] # Grant access to device 1 - >>> assert dmr.peer_accessible_by == (1,) - >>> dmr.peer_accessible_by = [] # Revoke access + >>> dmr.peer_accessible_by = {1} # grant access to device 1 + >>> assert 1 in dmr.peer_accessible_by + >>> dmr.peer_accessible_by.add(2) # update access to include device 2 + >>> dmr.peer_accessible_by = [] # revoke peer access """ - if not self._mempool_owned: - _DMR_query_peer_access(self) - return self._peer_accessible_by + return PeerAccessibleBySetProxy(self) @peer_accessible_by.setter def peer_accessible_by(self, devices): - _DMR_set_peer_accessible_by(self, devices) + replace_peer_accessible_by(self, devices) @property def is_device_accessible(self) -> bool: @@ -253,81 +242,6 @@ cdef class DeviceMemoryResource(_MemPool): return False -cdef inline _DMR_query_peer_access(DeviceMemoryResource self): - """Query the driver for the actual peer access state of this pool.""" - cdef int total - cdef cydriver.CUmemAccess_flags flags - cdef cydriver.CUmemLocation location - cdef list peers = [] - - with nogil: - HANDLE_RETURN(cydriver.cuDeviceGetCount(&total)) - - location.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE - for dev_id in range(total): - if dev_id == self._dev_id: - continue - location.id = dev_id - with nogil: - HANDLE_RETURN(cydriver.cuMemPoolGetAccess(&flags, as_cu(self._h_pool), &location)) - if flags == cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE: - peers.append(dev_id) - - self._peer_accessible_by = tuple(sorted(peers)) - - -cdef inline _DMR_set_peer_accessible_by(DeviceMemoryResource self, devices): - from .._device import Device - - this_dev = Device(self._dev_id) - cdef object resolve_device_id = lambda dev: Device(dev).device_id - cdef object plan - cdef tuple target_ids - cdef tuple to_add - cdef tuple to_rm - if not self._mempool_owned: - _DMR_query_peer_access(self) - plan = plan_peer_access_update( - owner_device_id=self._dev_id, - current_peer_ids=self._peer_accessible_by, - requested_devices=devices, - resolve_device_id=resolve_device_id, - can_access_peer=this_dev.can_access_peer, - ) - target_ids = plan.target_ids - to_add = plan.to_add - to_rm = plan.to_remove - cdef size_t count = len(to_add) + len(to_rm) - cdef cydriver.CUmemAccessDesc* access_desc = NULL - cdef size_t i = 0 - - if count > 0: - access_desc = PyMem_Malloc(count * sizeof(cydriver.CUmemAccessDesc)) - if access_desc == NULL: - raise MemoryError("Failed to allocate memory for access descriptors") - - try: - for dev_id in to_add: - access_desc[i].flags = cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE - access_desc[i].location.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE - access_desc[i].location.id = dev_id - i += 1 - - for dev_id in to_rm: - access_desc[i].flags = cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_NONE - access_desc[i].location.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE - access_desc[i].location.id = dev_id - i += 1 - - with nogil: - HANDLE_RETURN(cydriver.cuMemPoolSetAccess(as_cu(self._h_pool), access_desc, count)) - finally: - if access_desc != NULL: - PyMem_Free(access_desc) - - self._peer_accessible_by = tuple(target_ids) - - cdef inline _DMR_init(DeviceMemoryResource self, device_id, options): from .._device import Device cdef int dev_id = Device(device_id).device_id @@ -351,7 +265,6 @@ cdef inline _DMR_init(DeviceMemoryResource self, device_id, options): self._mempool_owned = False MP_raise_release_threshold(self) else: - self._peer_accessible_by = () MP_init_create_pool( self, cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE, diff --git a/cuda_core/cuda/core/_memory/_ipc.pyx b/cuda_core/cuda/core/_memory/_ipc.pyx index 1c7b25c14fb..59414fc1b2e 100644 --- a/cuda_core/cuda/core/_memory/_ipc.pyx +++ b/cuda_core/cuda/core/_memory/_ipc.pyx @@ -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 diff --git a/cuda_core/cuda/core/_memory/_peer_access_utils.py b/cuda_core/cuda/core/_memory/_peer_access_utils.py deleted file mode 100644 index e08de69f2c7..00000000000 --- a/cuda_core/cuda/core/_memory/_peer_access_utils.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from collections.abc import Callable, Iterable -from dataclasses import dataclass - - -@dataclass(frozen=True) -class PeerAccessPlan: - """Normalized peer-access target state and the driver updates it requires.""" - - target_ids: tuple[int, ...] - to_add: tuple[int, ...] - to_remove: tuple[int, ...] - - -def normalize_peer_access_targets( - owner_device_id: int, - requested_devices: Iterable[object], - *, - resolve_device_id: Callable[[object], int], -) -> tuple[int, ...]: - """Return sorted, unique peer device IDs, excluding the owner device.""" - - target_ids = {resolve_device_id(device) for device in requested_devices} - target_ids.discard(owner_device_id) - return tuple(sorted(target_ids)) - - -def plan_peer_access_update( - owner_device_id: int, - current_peer_ids: Iterable[int], - requested_devices: Iterable[object], - *, - resolve_device_id: Callable[[object], int], - can_access_peer: Callable[[int], bool], -) -> PeerAccessPlan: - """Compute the peer-access target state and add/remove deltas.""" - - target_ids = normalize_peer_access_targets( - owner_device_id, - requested_devices, - resolve_device_id=resolve_device_id, - ) - bad = tuple(dev_id for dev_id in target_ids if not can_access_peer(dev_id)) - if bad: - bad_ids = ", ".join(str(dev_id) for dev_id in bad) - raise ValueError(f"Device {owner_device_id} cannot access peer(s): {bad_ids}") - - current_ids = set(current_peer_ids) - target_id_set = set(target_ids) - return PeerAccessPlan( - target_ids=target_ids, - to_add=tuple(sorted(target_id_set - current_ids)), - to_remove=tuple(sorted(current_ids - target_id_set)), - ) diff --git a/cuda_core/cuda/core/_memory/_peer_access_utils.pyx b/cuda_core/cuda/core/_memory/_peer_access_utils.pyx new file mode 100644 index 00000000000..8086aaff170 --- /dev/null +++ b/cuda_core/cuda/core/_memory/_peer_access_utils.pyx @@ -0,0 +1,406 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from collections.abc import Callable, Iterable, MutableSet +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from cuda.bindings cimport cydriver +from cuda.core._memory._device_memory_resource cimport DeviceMemoryResource +from cuda.core._resource_handles cimport as_cu +from cuda.core._utils.cuda_utils cimport HANDLE_RETURN +from cpython.mem cimport PyMem_Malloc, PyMem_Free +from libcpp.vector cimport vector + +if TYPE_CHECKING: + from cuda.core._device import Device + + +@dataclass(frozen=True) +class PeerAccessPlan: + """Normalized peer-access target state and the driver updates it requires.""" + + target_ids: tuple[int, ...] + to_add: tuple[int, ...] + to_remove: tuple[int, ...] + + +def normalize_peer_access_targets( + owner_device_id: int, + requested_devices: Iterable[object], + *, + resolve_device_id: Callable[[object], int], +) -> tuple[int, ...]: + """Return sorted, unique peer device IDs, excluding the owner device.""" + + target_ids = {resolve_device_id(device) for device in requested_devices} + target_ids.discard(owner_device_id) + return tuple(sorted(target_ids)) + + +def plan_peer_access_update( + owner_device_id: int, + current_peer_ids: Iterable[int], + requested_devices: Iterable[object], + *, + resolve_device_id: Callable[[object], int], + can_access_peer: Callable[[int], bool], +) -> PeerAccessPlan: + """Compute the peer-access target state and add/remove deltas.""" + + target_ids = normalize_peer_access_targets( + owner_device_id, + requested_devices, + resolve_device_id=resolve_device_id, + ) + bad = tuple(dev_id for dev_id in target_ids if not can_access_peer(dev_id)) + if bad: + bad_ids = ", ".join(str(dev_id) for dev_id in bad) + raise ValueError(f"Device {owner_device_id} cannot access peer(s): {bad_ids}") + + current_ids = set(current_peer_ids) + target_id_set = set(target_ids) + return PeerAccessPlan( + target_ids=target_ids, + to_add=tuple(sorted(target_id_set - current_ids)), + to_remove=tuple(sorted(current_ids - target_id_set)), + ) + + +def _resolve_peer_device_id(value): + """Coerce ``Device | int`` into a device-ordinal int.""" + from cuda.core._device import Device + + return Device(value).device_id + + +# ---- driver-touching helpers (cdef inline, called from .pyx code) ----------- + +cdef inline tuple _query_peer_access_ids(DeviceMemoryResource mr): + """Return the current peer device IDs as a sorted tuple of ints. + + The full driver loop runs inside a single ``nogil`` block. Because + ``range(total)`` ascends, the result is already sorted. + """ + cdef int total + cdef int dev_id + cdef int owner_id = mr._dev_id + cdef cydriver.CUmemAccess_flags flags + cdef cydriver.CUmemLocation location + cdef cydriver.CUmemoryPool h_pool = as_cu(mr._h_pool) + cdef vector[int] peers + cdef size_t i, n + + location.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE + + with nogil: + HANDLE_RETURN(cydriver.cuDeviceGetCount(&total)) + for dev_id in range(total): + if dev_id == owner_id: + continue + location.id = dev_id + HANDLE_RETURN(cydriver.cuMemPoolGetAccess(&flags, h_pool, &location)) + if flags == cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE: + peers.push_back(dev_id) + + n = peers.size() + return tuple(peers[i] for i in range(n)) + + +cdef inline bint _peer_access_includes(DeviceMemoryResource mr, int dev_id): + """Return True if peer access from ``dev_id`` is currently granted.""" + cdef cydriver.CUmemAccess_flags flags + cdef cydriver.CUmemLocation location + + location.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE + location.id = dev_id + with nogil: + HANDLE_RETURN(cydriver.cuMemPoolGetAccess(&flags, as_cu(mr._h_pool), &location)) + return flags == cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE + + +def _set_pool_access(mr, tuple to_add, tuple to_remove): + """Issue one ``cuMemPoolSetAccess`` for the given add/remove deltas. + + The thin Python-callable layer that wraps the actual driver call: building + the ``CUmemAccessDesc`` array and invoking ``cuMemPoolSetAccess`` happens + in here. Tests monkeypatch this on the module to spy on real driver work + without intercepting earlier no-op paths. + + Preconditions: ``len(to_add) + len(to_remove) > 0`` (the caller is + responsible for skipping empty diffs). + """ + cdef DeviceMemoryResource mr_typed = mr + cdef size_t count = len(to_add) + len(to_remove) + cdef cydriver.CUmemAccessDesc* access_desc = NULL + cdef size_t i = 0 + + access_desc = PyMem_Malloc(count * sizeof(cydriver.CUmemAccessDesc)) + if access_desc == NULL: + raise MemoryError("Failed to allocate memory for access descriptors") + + try: + for dev_id in to_add: + access_desc[i].flags = cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE + access_desc[i].location.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE + access_desc[i].location.id = dev_id + i += 1 + for dev_id in to_remove: + access_desc[i].flags = cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_NONE + access_desc[i].location.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE + access_desc[i].location.id = dev_id + i += 1 + + with nogil: + HANDLE_RETURN(cydriver.cuMemPoolSetAccess(as_cu(mr_typed._h_pool), access_desc, count)) + finally: + if access_desc != NULL: + PyMem_Free(access_desc) + + +def _apply_peer_access_diff(mr, to_add, to_remove): + """Apply a peer-access diff in at most one driver call. + + Every write path on :class:`PeerAccessibleBySetProxy` and the + ``peer_accessible_by`` setter routes through this function. Empty diffs + short-circuit here so the driver-level helper :func:`_set_pool_access` is + only invoked when there is actual work for ``cuMemPoolSetAccess`` to do. + """ + add_tuple = tuple(to_add) + remove_tuple = tuple(to_remove) + if not add_tuple and not remove_tuple: + return + _set_pool_access(mr, add_tuple, remove_tuple) + + +cpdef replace_peer_accessible_by(DeviceMemoryResource mr, devices): + """Replace the full peer-access set in a single batched driver call. + + Backs the ``mr.peer_accessible_by = [...]`` setter. Uses the same planner + as the proxy's bulk ops; the only difference is that adds and removes are + derived from the symmetric difference between current driver state and the + requested target set. + """ + from cuda.core._device import Device + + this_dev = Device(mr._dev_id) + plan = plan_peer_access_update( + owner_device_id=mr._dev_id, + current_peer_ids=_query_peer_access_ids(mr), + requested_devices=devices, + resolve_device_id=_resolve_peer_device_id, + can_access_peer=this_dev.can_access_peer, + ) + _apply_peer_access_diff(mr, plan.to_add, plan.to_remove) + + +# ---- Python MutableSet proxy ------------------------------------------------ + +class PeerAccessibleBySetProxy(MutableSet): + """Live driver-backed view of the peer devices granted access to a memory pool. + + Reads (``__contains__``, ``__iter__``, ``len(...)``) call ``cuMemPoolGetAccess``; + writes (``add``, ``discard``, and bulk ops) call ``cuMemPoolSetAccess``. There + is no in-memory mirror, so the view always reflects the current driver state + and stays consistent across multiple wrappers around the same pool. + + Iteration yields :class:`~cuda.core.Device` objects. ``add``, ``discard``, and + ``__contains__`` accept either a :class:`~cuda.core.Device` or a device-ordinal + ``int``; the owner device is silently ignored when supplied. + + All bulk operations (``update``, ``|=``, ``&=``, ``-=``, ``^=``, ``clear``) + issue exactly one ``cuMemPoolSetAccess`` call. This matters: peer-access + transitions can take seconds per pool because every existing memory mapping + is updated, so coalescing into a single driver call lets the toolkit handle + the mappings in parallel. + """ + + __slots__ = ("_mr",) + + def __init__(self, mr): + self._mr = mr + + @classmethod + def _from_iterable(cls, it): + # Binary set operators (&, |, -, ^) collect their result through + # _from_iterable. Returning a plain set lets the user reason about + # the result independently of any pool's driver state. + return set(it) + + # --- abstract MutableSet methods --- + + def __contains__(self, value) -> bool: + try: + dev_id = _resolve_peer_device_id(value) + except (TypeError, ValueError): + return False + cdef DeviceMemoryResource mr = self._mr + if dev_id == mr._dev_id: + return False + return _peer_access_includes(mr, dev_id) + + def __iter__(self): + from cuda.core._device import Device + + return iter(Device(dev_id) for dev_id in _query_peer_access_ids(self._mr)) + + def __len__(self) -> int: + return len(_query_peer_access_ids(self._mr)) + + def add(self, value) -> None: + """Grant peer access from ``value`` to allocations in this pool.""" + dev_id = _resolve_peer_device_id(value) + cdef DeviceMemoryResource mr = self._mr + if dev_id == mr._dev_id: + return + if _peer_access_includes(mr, dev_id): + return + from cuda.core._device import Device + if not Device(mr._dev_id).can_access_peer(dev_id): + raise ValueError(f"Device {mr._dev_id} cannot access peer: {dev_id}") + _apply_peer_access_diff(mr, (dev_id,), ()) + + def discard(self, value) -> None: + """Revoke peer access from ``value`` to allocations in this pool.""" + try: + dev_id = _resolve_peer_device_id(value) + except (TypeError, ValueError): + return + cdef DeviceMemoryResource mr = self._mr + if dev_id == mr._dev_id: + return + if not _peer_access_includes(mr, dev_id): + return + _apply_peer_access_diff(mr, (), (dev_id,)) + + # --- bulk overrides: one driver call per op --- + + def clear(self) -> None: + """Revoke all peer access in a single driver call.""" + self._apply((), _query_peer_access_ids(self._mr)) + + def update(self, *others) -> None: + """Grant peer access to every device in ``others`` in one driver call.""" + to_add = [] + for other in others: + to_add.extend(other) + if to_add: + self._apply(to_add, ()) + + def difference_update(self, *others) -> None: + """Revoke peer access for every device in ``others`` in one driver call.""" + revoke_ids = set() + for other in others: + for value in other: + try: + revoke_ids.add(_resolve_peer_device_id(value)) + except (TypeError, ValueError): + continue + current = set(_query_peer_access_ids(self._mr)) + to_remove = revoke_ids & current + if to_remove: + self._apply((), to_remove) + + def intersection_update(self, *others) -> None: + """Restrict peer access to the intersection in a single driver call.""" + keep_ids = None + for other in others: + ids = set() + for value in other: + try: + ids.add(_resolve_peer_device_id(value)) + except (TypeError, ValueError): + continue + keep_ids = ids if keep_ids is None else keep_ids & ids + if keep_ids is None: + return # ``set.intersection_update()`` with no args is a no-op + current = set(_query_peer_access_ids(self._mr)) + to_remove = current - keep_ids + if to_remove: + self._apply((), to_remove) + + def symmetric_difference_update(self, other) -> None: + """Toggle peer access for every device in ``other`` in one driver call.""" + toggle_ids = set() + for value in other: + try: + toggle_ids.add(_resolve_peer_device_id(value)) + except (TypeError, ValueError): + continue + current = set(_query_peer_access_ids(self._mr)) + to_add = toggle_ids - current + to_remove = toggle_ids & current + if to_add or to_remove: + self._apply(to_add, to_remove) + + def __ior__(self, other): + self.update(other) + return self + + def __iand__(self, other): + self.intersection_update(other) + return self + + def __isub__(self, other): + if other is self: + self.clear() + else: + self.difference_update(other) + return self + + def __ixor__(self, other): + self.symmetric_difference_update(other) + return self + + def __repr__(self) -> str: + return f"PeerAccessibleBySetProxy({set(self)!r})" + + # --- internal: route every write through one batched driver call --- + + def _apply(self, additions, removals) -> None: + """Compute the diff and issue a single ``cuMemPoolSetAccess``. + + ``additions`` and ``removals`` are user-supplied (``Device | int``); + only the owner device is filtered out. Adds are validated through + :meth:`Device.can_access_peer` via :func:`plan_peer_access_update`; + removals bypass that check (revoking is always permitted). + """ + from cuda.core._device import Device + + cdef DeviceMemoryResource mr = self._mr + owner_id = mr._dev_id + owner = Device(owner_id) + current = _query_peer_access_ids(mr) + + # Plan additions through the existing helper (validates can_access_peer). + plan = plan_peer_access_update( + owner_device_id=owner_id, + current_peer_ids=current, + # union of (current set + requested adds) so the planner emits + # exactly the to_add deltas for these additions, no removals. + requested_devices=[*current, *additions], + resolve_device_id=_resolve_peer_device_id, + can_access_peer=owner.can_access_peer, + ) + to_add = plan.to_add + + # Removals: resolve, drop owner and unknowns, intersect with current. + current_set = set(current) + revoke_ids = set() + for value in removals: + try: + dev_id = _resolve_peer_device_id(value) + except (TypeError, ValueError): + continue + if dev_id == owner_id: + continue + if dev_id in current_set: + revoke_ids.add(dev_id) + to_remove = tuple(sorted(revoke_ids)) + + if not to_add and not to_remove: + return + _apply_peer_access_diff(mr, to_add, to_remove) diff --git a/cuda_core/cuda/core/_stream.pxd b/cuda_core/cuda/core/_stream.pxd index c9ffb4c80a7..de16b84bde2 100644 --- a/cuda_core/cuda/core/_stream.pxd +++ b/cuda_core/cuda/core/_stream.pxd @@ -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 diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index 846ed928bf2..4df4f70ce87 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -16,6 +16,7 @@ CUDA runtime .. autosummary:: :toctree: generated/ + _memory._peer_access_utils.PeerAccessibleBySetProxy _module.KernelAttributes _module.KernelOccupancy _module.MaxPotentialBlockSizeOccupancyResult diff --git a/cuda_core/docs/source/release/1.0.0-notes.rst b/cuda_core/docs/source/release/1.0.0-notes.rst index 58553fe59ea..747b033c117 100644 --- a/cuda_core/docs/source/release/1.0.0-notes.rst +++ b/cuda_core/docs/source/release/1.0.0-notes.rst @@ -120,6 +120,11 @@ Breaking changes ``CUgraphConditionalHandle`` value. Previously, ``.handle`` had to be extracted explicitly. +- :attr:`DeviceMemoryResource.peer_accessible_by` now returns a + :class:`collections.abc.MutableSet` of :obj:`~_device.Device` objects instead + of a sorted ``tuple[int, ...]``. The property setter is unchanged. + (`#2018 `__) + - ``stream`` is now a required keyword-only argument on APIs that schedule work on a stream (`#2001 `__). diff --git a/cuda_core/tests/helpers/buffers.py b/cuda_core/tests/helpers/buffers.py index 44f84693089..bbe54c3a000 100644 --- a/cuda_core/tests/helpers/buffers.py +++ b/cuda_core/tests/helpers/buffers.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 import ctypes diff --git a/cuda_core/tests/helpers/collection_interface_testers.py b/cuda_core/tests/helpers/collection_interface_testers.py index 5197e475c18..63fbed8b381 100644 --- a/cuda_core/tests/helpers/collection_interface_testers.py +++ b/cuda_core/tests/helpers/collection_interface_testers.py @@ -1,13 +1,47 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Reusable helpers to verify collections.abc protocol conformance.""" +"""Reusable helpers to verify collections.abc protocol conformance. + +Two helpers are provided for ``MutableSet``-like subjects, picked by the +capacity of the backing store: + +- :func:`assert_mutable_set_interface` is the standard pass; it requires at + least five distinct insertable items so every method (including the + multi-element bulk operators) can be exercised. +- :func:`assert_single_member_mutable_set_interface` is a focused pass for + proxies whose backing store admits at most one insertable element at a time + (for example, a peer-access view on a system with one valid peer device). + It runs every ``MutableSet`` method at least once using a single member and + one non-member sentinel. + +The two helpers are intentionally separate rather than one helper with a +mode flag: a single-member proxy is a substantially different contract +("capacity one, by hardware") and naming it explicitly in the API keeps each +helper's signature small and its assertions linear. +""" from collections.abc import MutableSet, Set import pytest +def _assert_empty(subject): + """Assertions that hold for any empty MutableSet-like subject.""" + assert isinstance(subject, Set) + assert isinstance(subject, MutableSet) + assert len(subject) == 0 + assert subject == set() + assert list(subject) == [] + + +def _assert_repr_nonempty(subject): + """``__repr__`` produces a non-empty string.""" + r = repr(subject) + assert isinstance(r, str) + assert len(r) > 0 + + def assert_mutable_set_interface(subject, items): """Exercise every MutableSet method on *subject* against a reference set. @@ -23,15 +57,7 @@ def assert_mutable_set_interface(subject, items): a, b, c, d, e = items[:5] ref = set() - # -- ABC conformance -- - assert isinstance(subject, Set) - assert isinstance(subject, MutableSet) - - # -- empty state -- - assert len(subject) == 0 - assert subject == ref - assert subject == set() - assert list(subject) == [] + _assert_empty(subject) # -- add -- subject.add(a) @@ -136,7 +162,157 @@ def assert_mutable_set_interface(subject, items): # -- __iter__ -- assert set(subject) == ref - # -- __repr__ -- - r = repr(subject) - assert isinstance(r, str) - assert len(r) > 0 + _assert_repr_nonempty(subject) + + +def assert_single_member_mutable_set_interface(subject, member, non_member): + """Exercise every MutableSet method on a subject with capacity one. + + Use this for proxies whose backing store admits at most one insertable + element at a time (typically because the underlying resource is bounded + by hardware, e.g. a peer-access view on a system with a single valid + peer device). The subject only ever holds ``set()`` or ``{member}``; + *non_member* supplies the right-hand side of comparisons, ``isdisjoint``, + subset/superset, and binary/in-place operators so every ``MutableSet`` + method is exercised at least once. + + Parameters + ---------- + subject : MutableSet + An **empty** mutable-set-like object to test. + member : hashable + A distinct, hashable object valid for insertion into *subject*. + non_member : hashable + A distinct, hashable object that compares correctly under set + semantics but is guaranteed never to be inserted into *subject* + (typically because the backing store rejects it). + """ + assert member != non_member, "member and non_member must be distinct" + a = member + x = non_member + ref = set() + + _assert_empty(subject) + + # -- add -- + subject.add(a) + ref.add(a) + assert subject == ref + assert a in subject + assert x not in subject + assert len(subject) == 1 + + # add duplicate is a no-op + subject.add(a) + assert subject == ref + assert len(subject) == 1 + + # -- comparison with plain set -- + assert subject == {a} + assert subject != {a, x} + assert subject != set() + + # -- isdisjoint -- + assert subject.isdisjoint({x}) + assert not subject.isdisjoint({a, x}) + + # -- subset / superset -- + assert subject <= {a} + assert subject <= {a, x} + assert not (subject <= set()) + assert subject < {a, x} + assert not (subject < {a}) + assert {a, x} >= subject + assert {a, x} > subject + + # -- binary operators (results are plain sets, never insert into subject) -- + assert subject & {a, x} == {a} + assert subject & {x} == set() + assert subject | {x} == {a, x} + assert subject - {a} == set() + assert subject - {x} == {a} + assert subject ^ {x} == {a, x} + assert subject ^ {a} == set() + + # -- discard non-member is a no-op -- + subject.discard(x) + assert subject == ref + + # -- discard member -- + subject.discard(a) + ref.discard(a) + assert subject == ref + + # -- remove member -- + subject.add(a) + ref.add(a) + subject.remove(a) + ref.remove(a) + assert subject == ref + + # -- remove non-member raises -- + with pytest.raises(KeyError): + subject.remove(x) + + # -- pop empty raises -- + with pytest.raises(KeyError): + subject.pop() + + # -- pop populated -- + subject.add(a) + ref.add(a) + popped = subject.pop() + ref.discard(popped) + assert popped == a + assert popped not in subject + assert subject == ref + + # -- in-place union (|=) covers single insert via bulk path -- + subject |= {a} + ref |= {a} + assert subject == ref + + # -- in-place intersection (&=) keeps the lone member -- + subject &= {a, x} + ref &= {a, x} + assert subject == ref + + # -- in-place intersection (&=) drops the lone member -- + subject &= {x} + ref &= {x} + assert subject == ref + + # -- in-place difference (-=) on non-member is a no-op -- + subject |= {a} + ref |= {a} + subject -= {x} + ref -= {x} + assert subject == ref + + # -- in-place difference (-=) on member empties the subject -- + subject -= {a} + ref -= {a} + assert subject == ref + + # -- in-place symmetric difference (^=): toggle in then out -- + subject ^= {a} + ref ^= {a} + assert subject == ref + subject ^= {a} + ref ^= {a} + assert subject == ref + + # -- clear -- + subject.add(a) + ref.add(a) + subject.clear() + ref.clear() + assert subject == ref + assert len(subject) == 0 + + # -- __iter__ on populated subject -- + subject.add(a) + ref.add(a) + assert set(subject) == ref + + _assert_repr_nonempty(subject) diff --git a/cuda_core/tests/memory_ipc/test_peer_access.py b/cuda_core/tests/memory_ipc/test_peer_access.py index de0baef9b8b..993aa7344ec 100644 --- a/cuda_core/tests/memory_ipc/test_peer_access.py +++ b/cuda_core/tests/memory_ipc/test_peer_access.py @@ -29,7 +29,7 @@ def test_main(self, mempool_device_x2): options = DeviceMemoryResourceOptions(max_size=POOL_SIZE, ipc_enabled=True) mr = DeviceMemoryResource(dev1, options=options) mr.peer_accessible_by = [dev0] - assert mr.peer_accessible_by == (0,) + assert mr.peer_accessible_by == {dev0} # Spawn child process process = mp.Process(target=self.child_main, args=(mr,)) @@ -38,18 +38,18 @@ def test_main(self, mempool_device_x2): assert process.exitcode == 0 # Verify parent's MR still has peer access set (independent state) - assert mr.peer_accessible_by == (0,) + assert mr.peer_accessible_by == {dev0} mr.close() def child_main(self, mr): Device(1).set_current() assert mr.is_mapped is True assert mr.device_id == 1 - assert mr.peer_accessible_by == () + assert mr.peer_accessible_by == set() mr.peer_accessible_by = [0] - assert mr.peer_accessible_by == (0,) + assert mr.peer_accessible_by == {Device(0)} mr.peer_accessible_by = [] - assert mr.peer_accessible_by == () + assert mr.peer_accessible_by == set() mr.close() @@ -70,9 +70,9 @@ def test_main(self, mempool_device_x2, grant_access_in_parent): mr = DeviceMemoryResource(dev1, options=options) if grant_access_in_parent: mr.peer_accessible_by = [dev0] - assert mr.peer_accessible_by == (0,) + assert mr.peer_accessible_by == {dev0} else: - assert mr.peer_accessible_by == () + assert mr.peer_accessible_by == set() buffer = mr.allocate(NBYTES, stream=dev1.default_stream) pgen = PatternGen(dev1, NBYTES) pgen.fill_buffer(buffer, seed=False) @@ -108,14 +108,14 @@ def child_main(self, mr, buffer): # Test 3: Set peer access and verify buffer becomes accessible dev1.set_current() mr.peer_accessible_by = [0] - assert mr.peer_accessible_by == (0,) + assert mr.peer_accessible_by == {dev0} dev0.set_current() PatternGen(dev0, NBYTES).verify_buffer(buffer, seed=False) # Test 4: Revoke peer access and verify buffer becomes inaccessible dev1.set_current() mr.peer_accessible_by = [] - assert mr.peer_accessible_by == () + assert mr.peer_accessible_by == set() dev0.set_current() with pytest.raises(CUDAError, match="CUDA_ERROR_INVALID_VALUE"): PatternGen(dev0, NBYTES).verify_buffer(buffer, seed=False) diff --git a/cuda_core/tests/test_memory_peer_access.py b/cuda_core/tests/test_memory_peer_access.py index 04324ceec81..71beb459143 100644 --- a/cuda_core/tests/test_memory_peer_access.py +++ b/cuda_core/tests/test_memory_peer_access.py @@ -1,10 +1,13 @@ -# 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 import pytest from helpers.buffers import PatternGen, compare_buffer_to_constant, make_scratch_buffer +from helpers.collection_interface_testers import assert_single_member_mutable_set_interface -from cuda.core import DeviceMemoryResource, DeviceMemoryResourceOptions +from cuda.core import Device, DeviceMemoryResource, DeviceMemoryResourceOptions, system +from cuda.core._memory import _peer_access_utils +from cuda.core._memory._peer_access_utils import PeerAccessibleBySetProxy from cuda.core._utils.cuda_utils import CUDAError NBYTES = 1024 @@ -100,18 +103,18 @@ def verify_state(state, pattern_seed): transitions = [(s0, s1) for s0 in states for s1 in states if s0 != s1] for init_state, final_state in transitions: dmrs[0].peer_accessible_by = init_state - assert dmrs[0].peer_accessible_by == init_state + assert dmrs[0].peer_accessible_by == {Device(i) for i in init_state} verify_state(init_state, pattern_seed) pattern_seed += 1 dmrs[0].peer_accessible_by = final_state - assert dmrs[0].peer_accessible_by == final_state + assert dmrs[0].peer_accessible_by == {Device(i) for i in final_state} verify_state(final_state, pattern_seed) pattern_seed += 1 def test_peer_access_shared_pool_queries_driver(mempool_device_x2): - """Non-owned pools always query the driver for peer access state.""" + """All pools always query the driver, so wrappers see consistent state.""" dev0, dev1 = mempool_device_x2 # Grant peer access via one wrapper; a second wrapper must see it. @@ -122,18 +125,289 @@ def test_peer_access_shared_pool_queries_driver(mempool_device_x2): # Revoke via dmr2; dmr1 must reflect the change immediately. dmr2.peer_accessible_by = [] - assert dmr1.peer_accessible_by == () + assert dmr1.peer_accessible_by == set() # Re-grant via dmr1. A fresh wrapper that has never read the # property must still query the driver before computing diffs # in the setter, so setting [] must discover and revoke the access. dmr1.peer_accessible_by = [dev1] dmr3 = DeviceMemoryResource(dev0) - assert dmr1.peer_accessible_by == (dev1.device_id,) - assert dmr2.peer_accessible_by == (dev1.device_id,) - assert dmr3.peer_accessible_by == (dev1.device_id,) + assert dmr1.peer_accessible_by == {dev1} + assert dmr2.peer_accessible_by == {dev1} + assert dmr3.peer_accessible_by == {dev1} dmr3.peer_accessible_by = [] - assert DeviceMemoryResource(dev0).peer_accessible_by == () - assert dmr1.peer_accessible_by == () - assert dmr2.peer_accessible_by == () - assert dmr3.peer_accessible_by == () + assert DeviceMemoryResource(dev0).peer_accessible_by == set() + assert dmr1.peer_accessible_by == set() + assert dmr2.peer_accessible_by == set() + assert dmr3.peer_accessible_by == set() + + +# --------------------------------------------------------------------------- +# Set-proxy interface coverage +# +# These tests exercise the ``PeerAccessibleBySetProxy`` surface added in +# v1.0.0. They run against ``mempool_device_x2`` because every CI machine has +# at most 2 GPUs, which means at most one valid peer device. The +# ``assert_single_member_mutable_set_interface`` helper threads that single +# insertable element through the full ``MutableSet`` protocol. +# --------------------------------------------------------------------------- + + +@pytest.fixture +def isolated_dmr_x2(mempool_device_x2): + """Owned-pool DMR on dev0 + the lone peer device (dev1). + + The owned pool guarantees a clean, empty initial peer-access state so the + proxy tests are not polluted by other tests sharing a default pool. + """ + dev0, dev1 = mempool_device_x2 + dmr = DeviceMemoryResource(dev0, DeviceMemoryResourceOptions()) + dmr.peer_accessible_by = [] + return dmr, dev0, dev1 + + +def test_peer_accessible_by_mutable_set_interface(isolated_dmr_x2): + """Run the MutableSet protocol against a single-peer driver-backed view. + + On a 2-GPU box the proxy can only ever hold ``{dev1}`` because there is a + single valid peer. The capacity-one helper exercises every ``MutableSet`` + method using ``dev1`` as the lone insertable element and ``dev0`` (the + owner, which the proxy refuses to insert) as the non-member sentinel. + """ + dmr, dev0, dev1 = isolated_dmr_x2 + assert_single_member_mutable_set_interface( + dmr.peer_accessible_by, + member=dev1, + non_member=dev0, + ) + + +def test_peer_accessible_by_accepts_int_and_device(isolated_dmr_x2): + """``add``/``discard``/``__contains__`` accept ``Device`` and ``int`` interchangeably.""" + dmr, dev0, dev1 = isolated_dmr_x2 + + dmr.peer_accessible_by.add(dev1.device_id) + assert dmr.peer_accessible_by == {dev1} + assert dev1 in dmr.peer_accessible_by + assert dev1.device_id in dmr.peer_accessible_by + + dmr.peer_accessible_by.discard(dev1) + assert dmr.peer_accessible_by == set() + + dmr.peer_accessible_by.add(dev1) + assert dmr.peer_accessible_by == {dev1} + dmr.peer_accessible_by.discard(dev1.device_id) + assert dmr.peer_accessible_by == set() + + +def test_peer_accessible_by_silently_ignores_owner(isolated_dmr_x2): + """The owner device is silently filtered on every write; ``__contains__`` returns False.""" + dmr, dev0, dev1 = isolated_dmr_x2 + + # add/discard on owner is a no-op (no error, no state change) + dmr.peer_accessible_by.add(dev0) + dmr.peer_accessible_by.add(dev0.device_id) + assert dmr.peer_accessible_by == set() + dmr.peer_accessible_by.discard(dev0) + dmr.peer_accessible_by.discard(dev0.device_id) + assert dmr.peer_accessible_by == set() + + # __contains__ on owner is False (matches set semantics, never raises) + assert dev0 not in dmr.peer_accessible_by + assert dev0.device_id not in dmr.peer_accessible_by + + # Owner mixed into bulk ops is filtered, the peer is still added/removed + dmr.peer_accessible_by |= {dev0, dev1} + assert dmr.peer_accessible_by == {dev1} + dmr.peer_accessible_by -= {dev0, dev1} + assert dmr.peer_accessible_by == set() + + +def test_peer_accessible_by_rejects_invalid_inputs(isolated_dmr_x2): + """``add`` raises on out-of-range/unsupported inputs; lenient methods do not.""" + dmr, dev0, dev1 = isolated_dmr_x2 + bad_id = system.get_num_devices() # one past the last valid device ordinal + + # add: validates strictly, propagates errors from Device(bad_id) + with pytest.raises((ValueError, CUDAError)): + dmr.peer_accessible_by.add(bad_id) + # Non-coercible inputs surface whatever Device(value) raises (TypeError or + # ValueError depending on Cython's int coercion path). + with pytest.raises((TypeError, ValueError)): + dmr.peer_accessible_by.add("not-a-device") + + # discard: silently ignores non-coercible values (matches set.discard) + dmr.peer_accessible_by.discard("not-a-device") + assert dmr.peer_accessible_by == set() + + # __contains__: returns False on non-coercible values, never raises + assert "not-a-device" not in dmr.peer_accessible_by + + # __contains__: out-of-range int returns False, never raises + assert bad_id not in dmr.peer_accessible_by + + # remove on a non-member raises KeyError (inherited from MutableSet) + with pytest.raises(KeyError): + dmr.peer_accessible_by.remove(dev1) + + +def test_peer_accessible_by_no_cache_across_proxies(mempool_device_x2): + """Updates via one wrapper are immediately visible through any other proxy.""" + dev0, dev1 = mempool_device_x2 + dmr_a = DeviceMemoryResource(dev0) + dmr_b = DeviceMemoryResource(dev0) + dmr_a.peer_accessible_by = [] + + proxy = dmr_a.peer_accessible_by # acquired before the change below + dmr_b.peer_accessible_by.add(dev1) + # The proxy must reflect the new driver state, not a snapshot. + assert dev1 in proxy + assert proxy == {dev1} + + dmr_b.peer_accessible_by.clear() + assert proxy == set() + + +def test_peer_accessible_by_iteration_order_is_sorted(mempool_device_x2): + """``__iter__`` yields peers in ascending device-ordinal order.""" + dev0, dev1 = mempool_device_x2 + dmr = DeviceMemoryResource(dev0, DeviceMemoryResourceOptions()) + dmr.peer_accessible_by = [dev1] + devices = list(dmr.peer_accessible_by) + ids = [d.device_id for d in devices] + assert ids == sorted(ids) + assert all(isinstance(d, Device) for d in devices) + + +def test_peer_accessible_by_repr(isolated_dmr_x2): + """``repr`` includes the class name and reflects the live contents.""" + dmr, dev0, dev1 = isolated_dmr_x2 + empty_repr = repr(dmr.peer_accessible_by) + assert "PeerAccessibleBySetProxy" in empty_repr + assert "set()" in empty_repr + + dmr.peer_accessible_by.add(dev1) + populated_repr = repr(dmr.peer_accessible_by) + assert "PeerAccessibleBySetProxy" in populated_repr + # Don't pin the exact device repr; just confirm content changed. + assert populated_repr != empty_repr + + +def test_peer_accessible_by_returns_proxy_type(isolated_dmr_x2): + """The getter returns the documented proxy type (anchors the public contract).""" + dmr, dev0, dev1 = isolated_dmr_x2 + assert isinstance(dmr.peer_accessible_by, PeerAccessibleBySetProxy) + + +# --------------------------------------------------------------------------- +# Batching contract: every bulk op must issue at most one cuMemPoolSetAccess +# +# Spying via ``monkeypatch.setattr`` on the module-level +# ``_apply_peer_access_diff`` works because the proxy and the property setter +# call it by bare name, which Cython resolves through the module's globals at +# runtime (the wrapper is a plain ``def``, not a ``cdef inline``). +# --------------------------------------------------------------------------- + + +class _DriverCallSpy: + """Records every actual ``cuMemPoolSetAccess`` invocation. + + Spies on :func:`cuda.core._memory._peer_access_utils._set_pool_access` — + the thin Python-visible wrapper that builds the descriptor array and + issues the single driver call. Earlier no-op layers (e.g. the + augmented-assignment-on-property quirk that reassigns an already-mutated + proxy back through the setter) short-circuit before reaching here, so the + recorded count is exactly the number of real ``cuMemPoolSetAccess`` calls. + """ + + def __init__(self, real): + self._real = real + self.calls = [] + + def __call__(self, mr, to_add, to_remove): + self.calls.append((tuple(to_add), tuple(to_remove))) + self._real(mr, to_add, to_remove) + + +@pytest.fixture +def driver_spy(monkeypatch): + spy = _DriverCallSpy(_peer_access_utils._set_pool_access) + monkeypatch.setattr(_peer_access_utils, "_set_pool_access", spy) + return spy + + +def test_peer_accessible_by_setter_batches_one_call(driver_spy, isolated_dmr_x2): + """``mr.peer_accessible_by = [...]`` issues exactly one driver call (or zero on no-op).""" + dmr, dev0, dev1 = isolated_dmr_x2 + dmr.peer_accessible_by = [dev1] + assert len(driver_spy.calls) == 1 + assert dev1.device_id in driver_spy.calls[-1][0] + + # Reassigning the same set is a no-op (zero driver calls). + driver_spy.calls.clear() + dmr.peer_accessible_by = [dev1] + assert driver_spy.calls == [] + + # Revoking everything is a single call (one removal). + dmr.peer_accessible_by = [] + assert len(driver_spy.calls) == 1 + assert dev1.device_id in driver_spy.calls[-1][1] + + +def test_peer_accessible_by_bulk_ops_batch_one_call(driver_spy, isolated_dmr_x2): + """Every bulk op issues exactly one ``cuMemPoolSetAccess`` (or zero on no-op). + + Covers ``|=``, ``&=``, ``^=``, ``update``, ``difference_update``, and + ``clear``. Both operand styles are exercised: a locally bound proxy + (``proxy |= {...}``) and augmented assignment directly on the property + (``dmr.peer_accessible_by |= {...}``). The latter trips Python's + augmented-assignment-on-property pattern (fetch proxy, mutate, write + back through setter) but the trailing setter call discovers an empty + diff and short-circuits before reaching the driver, so the count is + still one. + """ + dmr, dev0, dev1 = isolated_dmr_x2 + proxy = dmr.peer_accessible_by + + proxy |= {dev1} + assert len(driver_spy.calls) == 1 + driver_spy.calls.clear() + + # &= keeping the lone member: no driver call (no diff). + proxy &= {dev1} + assert driver_spy.calls == [] + + # &= dropping the lone member: one removal. + proxy &= {dev0} + assert len(driver_spy.calls) == 1 + driver_spy.calls.clear() + + # ^= toggling the lone peer in then out: two ops, one call each. + proxy ^= {dev1} + assert len(driver_spy.calls) == 1 + proxy ^= {dev1} + assert len(driver_spy.calls) == 2 + driver_spy.calls.clear() + + # update() with the peer already absent: one add. + proxy.update([dev1]) + assert len(driver_spy.calls) == 1 + driver_spy.calls.clear() + + # clear() with one member: one removal. + proxy.clear() + assert len(driver_spy.calls) == 1 + driver_spy.calls.clear() + + # Already-empty bulk ops are no-ops (nothing to add or remove). + proxy.clear() + proxy.difference_update([dev1]) + proxy -= {dev1} + assert driver_spy.calls == [] + + # Augmented assignment directly on the property is also one driver call: + # the proxy mutates the pool via __ior__, the setter writes back an + # already-up-to-date proxy, and the empty-diff short-circuit prevents a + # second driver call. + dmr.peer_accessible_by |= {dev1} + assert len(driver_spy.calls) == 1 diff --git a/cuda_core/tests/test_stream.py b/cuda_core/tests/test_stream.py index 4e5813ee226..49e372c9d53 100644 --- a/cuda_core/tests/test_stream.py +++ b/cuda_core/tests/test_stream.py @@ -1,4 +1,4 @@ -# 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 import pytest From 2a2186c809ce4f9f4419f9edaf27dad38a719616 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Wed, 6 May 2026 17:25:16 -0500 Subject: [PATCH 079/244] Add persistent program cache for Program.compile (#1912) * feat(core.utils): persistent program cache Add a bytes-in / bytes-out cache abstraction and two backends for caching compiled CUDA programs across process boundaries. * ``ProgramCacheResource`` -- abstract base. Concrete backends store raw binary bytes keyed by ``bytes`` or ``str``; reads return the same payload. ``__setitem__`` accepts ``bytes``, ``bytearray``, ``memoryview``, or any :class:`~cuda.core.ObjectCode` (path-backed too -- the file is read at write time so the cached entry holds the binary content, not a path that could move). Provides default ``get``, ``update`` (mapping or pairs), ``close``, and context manager. ``__contains__`` is intentionally NOT abstract: the racy ``if key in cache; data = cache[key]`` idiom is steered toward ``cache.get(key)`` instead. * ``InMemoryProgramCache`` -- single-process LRU on ``collections.OrderedDict`` with ``threading.RLock`` and a size-only cap. Reads promote via ``move_to_end``. * ``FileStreamProgramCache`` -- directory of atomic per-entry files. Writes stage to ``tmp/`` then ``os.replace`` into ``entries/<2-char>/``; concurrent readers never see a torn file. Each entry is the raw compiled binary (no pickle, no framing) so files are directly consumable by external NVIDIA tools (``cuobjdump``, ``nvdisasm``, ``cuda-gdb``). Eviction is true LRU via ``st_atime`` (the read path calls ``os.utime`` to bypass ``relatime`` / ``NtfsDisableLastAccessUpdate`` / ``noatime``). Stat-guarded prunes refuse to unlink entries another process replaced mid-eviction. ``tmp/`` is recreated on every write so an external wipe doesn't crash later writes. Default cache directory comes from ``platformdirs.user_cache_path("cuda-python", appauthor=False, opinion=False) / "program-cache"``. * Windows sharing-violation handling -- ``os.replace``, ``path.stat() + read_bytes()``, and ``path.unlink`` all retry on winerror 5/32/33 with a bounded backoff (~185 ms). The ``_is_windows_sharing_violation`` predicate filters EACCES only when ``winerror`` is absent so non-sharing winerrors propagate as the real config errors they are. Off-Windows ``PermissionError`` always propagates. * ``make_program_cache_key`` -- escape hatch for callers whose compile inputs require an ``extra_digest`` (header / PCH content fingerprints, NVVM libdevice). Builds a 32-byte blake2b digest via a backend-strategy pattern: a ``_KeyBackend`` ABC with per-code-type subclasses (``_NvrtcBackend``, ``_LinkerBackend``, ``_NvvmBackend``) owns each backend's validation, code coercion, option fingerprinting, name-expression handling, version probe, and extra-payload hashing. The orchestrator dispatches via ``_BACKENDS_BY_CODE_TYPE[code_type]`` and assembles the digest in fixed order. Backend gates match ``Program.compile``: rejects inputs the real compile would reject (side-effect options, external-content options without an ``extra_digest``, driver-linker-unsupported options, NVRTC ``options.name`` with a directory component). NVVM ``extra_sources`` is hashed in caller-provided order because NVVM module linking is order-dependent in the general case (overlapping symbols, weak definitions); canonicalising would silently change behavior for order-dependent inputs. Adds ``platformdirs >=3.0`` to ``cuda_core/pyproject.toml`` and the matching pixi manifests. Tests cover the abstract contract, key-construction matrix (deterministic, supported-target gates, backend-probe taint, gate canonicalization, side-effect / external-content / dir-component guards, schema version mixing), single-process CRUD and LRU, atomic-write race coverage, atime LRU promotion, stat-guarded prune / atime touch / clear / size-cap, default-dir resolution via platformdirs, the ``_is_windows_sharing_violation`` predicate's truth table including the regression case (non-sharing winerror plus EACCES propagates), tmp-dir recreation after external wipe, multiprocess concurrent writers / reader-vs-writer torn-file safety / size-cap eviction race. * feat(core): Program.compile(cache=...) convenience wrapper Adds a ``cache=`` keyword to :meth:`cuda.core.Program.compile` that threads the persistent cache machinery into the high-level compile path. With ``cache=None`` (the default) the call is byte-identical to the un-cached path -- no key derivation, no extra import, no behavior change. When a cache is provided, the wrapper derives a key via :func:`~cuda.core.utils.make_program_cache_key` from the program's source, options, and target type; checks the cache; on hit, returns a fresh ``ObjectCode._init(hit_bytes, target_type, name=self._options.name)``; on miss, runs the underlying compile and stores ``cache[key] = compiled`` (the cache extracts ``bytes(obj.code)``). Two compile-time guards close obvious footguns: * ``name_expressions`` plus ``cache=`` raises ``ValueError``. NVRTC populates ``ObjectCode.symbol_mapping`` from name-expression mangling at compile time, and that mapping isn't carried in the binary the cache stores. Without this guard the first call (miss) would return an ObjectCode with mappings populated, while every subsequent call (hit) would return one without -- silently breaking later ``get_kernel(name_expression)`` lookups that work on the uncached path. Compiles that need name_expressions should run without ``cache=``, or look up mangled symbols by hand from the cached ``ObjectCode``. * Inputs whose compilation effect isn't captured by the key (``include_path``, ``pre_include``, ``pch``, ``use_pch``, ``pch_dir``, NVVM ``use_libdevice=True``, NVRTC ``options.name`` with a directory component, side-effect options like ``create_pch`` / ``time`` / ``fdevice_time_trace``) propagate the ``ValueError`` from ``make_program_cache_key`` -- those callers should use ``make_program_cache_key`` directly with an ``extra_digest`` covering the external content. Cache hits also mirror the uncached path's NVRTC-PTX loadability warning: when ``self._backend == "NVRTC"``, ``target_type == "ptx"``, and ``_can_load_generated_ptx()`` returns False, a ``RuntimeWarning`` is emitted before returning the cached bytes. Loadability is a property of the active driver, not of how the bytes were produced, so the warning applies equally to cached PTX. Supporting refactors: * Unify ``Program``'s source retention into a single ``_code`` field (was split between ``_code`` for NVVM and a separate ``_source`` for c++/ptx). ``_code`` is now always bytes; the cache wrapper decodes back to ``str`` for c++/ptx before passing to ``make_program_cache_key`` (which only accepts bytes for NVVM). * Move the actual compile call into a module-level ``_program_compile_uncached`` so tests can monkeypatch the seam without going through NVRTC. ``Program`` is a ``cdef class``, so its methods cannot be reassigned from Python -- the seam has to live outside the class. * The unified ``_code`` field also exposed a pre-existing bug on the NVVM path: the C pointer was being recomputed from the caller's original ``code`` argument rather than from ``self._code``, which crashed for ``bytearray`` inputs that the field's bytes coercion handled cleanly. Fixed; regression test added in ``test_program.py``. Tests in ``test_program_compile_cache.py`` cover both halves of the contract: the wrapper-level miss/hit/error paths against a recording stub (verifying it's duck-typed and doesn't require subclassing ``ProgramCacheResource``), the rejection paths (name_expressions, extra_digest-required options, side-effect options, NVRTC ``options.name`` with a directory component), the PTX loadability warning on cache hit (positive: warns when the driver can't load the cached PTX; negative: stays quiet otherwise), and a real NVRTC end-to-end roundtrip using ``FileStreamProgramCache`` across reopen so the bytes match across processes. * refactor(core.utils): import program cache eagerly, fix ruff findings Drop the module-level __getattr__/__dir__ shim that lazily exposed the program-cache classes. Import them eagerly alongside the memoryview helpers. Remove the two tests that pinned the lazy-import behaviour. Also pick up ruff's auto-fixes (import-block blank lines, long-line reformat) and rename the unused classmethod argument to satisfy ARG005. * docs(core): nest Program caches under CUDA compilation toolchain Move the Program caches autosummary out of the trailing cuda.core.utils block and into a subsection of CUDA compilation toolchain so the cache classes sit next to Program/Linker. The subsection switches the current module to cuda.core.utils for the autosummary and switches back afterwards. make_program_cache_key moves with the cache classes. * docs(core.utils): nudge cache callers toward close() The default close() on ProgramCacheResource is a no-op because the in-tree backends happen not to hold long-lived state. Future backends will -- file handles, sockets, db connections -- so the docstring now spells that out and tells callers to use the context manager or call close() explicitly so their code is portable across backends. * docs(core.utils): teach cache examples with public ObjectCode API Two docstring examples (in ProgramCacheResource and make_program_cache_key) reached into the private cuda.core._module path and used the private ObjectCode._init constructor. Switch to the public from cuda.core import ObjectCode plus ObjectCode.from_cubin(...) so users learning the cache API don't get steered at private surface. * feat(core): require Program.compile(cache=...) to be a ProgramCacheResource cache= used to accept any object with the get/__setitem__ duck-typed shape. Tighten the contract: the wrapper now isinstance-checks against ProgramCacheResource and raises TypeError up front when callers pass a plain dict-like. Subclasses get the get/update/close/__enter__/__exit__ defaults from the ABC and a portable interface across backends. Update the recording test cache to subclass the ABC and add a regression test that a duck-typed cache is rejected with TypeError. * test(core): mock driver_version to keep _can_load_generated_ptx as cpdef The PTX cache-hit warning tests used to monkeypatch _can_load_generated_ptx itself, which forced the helper to a plain def so Cython would not early-bind the in-module call past the patch. Restore it to cpdef bint ... except? -1 and instead pin driver_version() in the test to (0, 0, 0) (warning expected) or (999, 0, 0) (no warning) so the cpdef body computes the desired result. Cython compiles each global lookup inside the helper as a fresh module-globals fetch, so swapping driver_version on the module object is enough to steer the comparison. * docs(core): note cache hits do not write to logs * fix(core.utils): cache _LinkerBackend driver decision per key call _LinkerBackend.validate, option_fingerprint, and hash_version_probe each re-probed _decide_nvjitlink_or_driver(), so a flapping probe could mint a key whose option fingerprint and version probe disagreed on which linker is in use. Cache the decision (and any probe exception) on a per-instance basis, instantiate _BACKENDS_BY_CODE_TYPE entries fresh per make_program_cache_key call so the cache lives exactly one call, and thread the decision into _linker_backend_and_version() instead of letting it probe a third time. Tests that monkeypatched _linker_backend_and_version now accept the extra use_driver argument (or *args/**kwargs in the failure-path test). * fix(core): lowercase target_type in Program.compile and cache key code_type was already normalised at Program init, but target_type was checked case-sensitively against {"ptx", "cubin", "ltoir"} in Program_compile (so compile(target_type="PTX") used to raise) and the cache key path inherited the same asymmetry from make_program_cache_key. Lowercase target_type at the top of Program.compile and at the entry to make_program_cache_key so callers who pass "PTX" get the same dispatch and the same cache key as "ptx". * refactor(core.utils): collapse _LINKER_RELEVANT_FIELDS into _LINKER_FIELD_GATES The names tuple and the gates dict had to be kept in sync by hand: a field added to one but forgotten in the other would silently slip out of the PTX fingerprint. Drop the tuple and iterate the dict directly, so the dict is the single source of truth for which ProgramOptions fields perturb a PTX cache key. * fix(core.utils): contextual error when path-backed ObjectCode vanishes _extract_bytes used to let a bare FileNotFoundError bubble up from Path(code).read_bytes(), so cache[key] = obj failures pointed only at the missing path with no hint that the cache was reading a path-backed ObjectCode. Wrap the FileNotFoundError with a message that names both the cache operation and the missing file so debugging the case stays self-explanatory. * docs(core): clarify cache= vs ProgramOptions.no_cache independence * test(core.utils): tighten multiprocess cache assertions The reader test only checked that every read returned non-None; a half-written file with non-empty bytes would pass. Carry the seeded payload into the worker, count exact-byte mismatches, and require zero. The eviction-race test wrote a final uncontested cache[key] = payload + b"final" after the churner exited, so the post-race endswith assertion would pass even with a broken stat-guard. Drop the final write and assert the entry survives carrying a rewriter payload prefix -- if the stat-guarded eviction path is broken, the in-race write is the one that vanishes. * test(core.utils): add InMemoryProgramCache thread test and FileStream overwrite test InMemoryProgramCache claims thread-safety via the RLock that wraps every method but had no concurrent-thread coverage. Add a stress test with 4 writers + 4 readers x 200 ops against a size-capped cache that verifies no exceptions, no deadlocks (RLock reentrance through __setitem__ -> _evict_to_caps -> popitem), and that internal accounting (_total_bytes, len(_entries), len(cache)) stays consistent under contention. FileStreamProgramCache had no overwrite test analogous to test_inmemory_cache_overwrite_replaces_value_and_updates_size. Add one that writes a key twice and asserts the second value reads back, len stays at 1, and exactly one entry file lives on disk -- so a leaked entry from a botched os.replace would surface here. * refactor(core.utils): drop dead empty-key branch in _path_for_key * perf(core.utils): drop redundant stat in FileStreamProgramCache.__len__ * feat(core.utils): reject max_size_bytes=0 in cache backends max_size_bytes=0 used to slip past the >=0 guard but turned the cache into a black hole: every write was immediately evicted on its own size-cap pass. There is no legitimate use for that, so tighten the guard to >0 (or None for unbounded) and update both backends and the matching tests. * docs(core.utils): note ptxas_options order is intentionally significant * docs(core.utils): document the _keys.py <-> _program.pyx import contract * docs(core.utils): note options.name double-hash on PTX is intentional * docs(core.utils): document burst-write over-eviction trade-off * docs(core): explain why Program.compile cache decode is provably safe * refactor(core.utils): extract _stat_key for the four stat-guarded paths The (st_ino, st_size, st_mtime_ns) triple was open-coded in _touch_atime (fd-based and path-based fallbacks), _prune_if_stat_unchanged, and _enforce_size_cap. Centralise the fingerprint as _stat_key(st) so all four readers compare the same fields and the invariant has one place to read. * refactor(core.utils): collapse Windows sharing-retry loops Replace, stat-and-read, and unlink each carried their own copy of the _REPLACE_RETRY_DELAYS / sleep / try-op / PermissionError loop. Centralise the loop as _with_sharing_retry(op, on_exhausted=...) and let each caller plug in its own success-on-success and exhausted-budget behaviour. Net behaviour is unchanged (exhaustion semantics for each public helper are preserved via the on_exhausted callback). * fix(test): walk sharded entries dir in FileStream overwrite assertion FileStreamProgramCache shards cache files into entries//, so the overwrite test's iterdir filter on entries/ saw only the digest-prefix subdir (no is_file() match) and reported 0 entries. Switch to rglob so the assertion counts actual entry files. CI from the previous push caught this. * fix(test): collapse nested with into a single contextmanager group (SIM117) * fix(core.utils): keep path-backed ObjectCode error message Windows-friendly `{code!r}` doubles every backslash on Windows, so the error string holds `'C:\\Users\\...\\file'` while `str(src)` only has single backslashes. Naive `str(path) in str(exc)` checks (and the `test_filestream_cache_path_backed_object_code_missing_file_message` assertion) failed on win-64 as a result. Drop the `!r` so the path appears verbatim; the message still quotes it via the surrounding sentence punctuation. * perf(core.utils): track FileStream cache size incrementally Avoid the per-write directory walk in `_enforce_size_cap` by maintaining a running byte total. The tracker is seeded once from `_compute_total_size` at open time, updated on `__setitem__` (net delta from the old entry's size), `__delitem__` (subtract the unlinked file's size), and `clear` (re-derive from the post-clear state). When a write doesn't push the running total above `max_size_bytes`, eviction stays a no-op -- writes become O(1) instead of O(n) in the cache size. Cross-process drift (other writers/deleters working on the same root) self-corrects: any time `_enforce_size_cap` actually runs its scan, it reseeds `_tracked_size_bytes` from the observed disk total, so overestimates trigger one extra scan and then settle. Skipped entirely when `max_size_bytes is None` (no cap, no need for a tracker). Mutations are guarded by `_size_lock` so multi-threaded writers in the same process don't interleave the read-modify-write on the int. Also switch `_iter_entry_paths` from `Path.iterdir` to `os.scandir`: the dirent type cache lets `is_dir`/`is_file` answer without a separate `stat` syscall on filesystems that report it (ext4, NTFS, ...). Behaviourally equivalent (the cache layout never creates symlinks, and we pass `follow_symlinks=False` to match `pathlib`'s previous no-symlink-confusion default for our paths). Tests cover three guarantees: (1) writes that stay under the cap don't call `_enforce_size_cap`, (2) writes that cross the cap do call it, and (3) external deletion behind the cache's back is reconciled by the next eviction pass. * test(core.utils): drop SUPPORTED_TARGETS cross-check The test parsed `_program.pyx` to extract `SUPPORTED_TARGETS` and compare against `_SUPPORTED_TARGETS_BY_CODE_TYPE` in the cache. Source parsing for a duplication check is not worth the maintenance cost -- a reviewer eyeballing both definitions catches drift just as well, and the test was already broken once by upstream's StrEnum migration. * refactor(core.utils): idiomatic with-as on os.scandir, yield from inner `os.scandir` returns an iterator that holds an OS directory handle (POSIX dirent fd, Windows FindFirstFileW); the context manager closes that handle deterministically. Using `with os.scandir(...) as outer` is the conventional spelling -- splitting the call from the `with` to land the FileNotFoundError catch on the call alone added one binding for no behavioural difference. Wrap each `with` in its own `try/except FileNotFoundError` instead. The inner loop is also a plain filter-and-yield over the DirEntry iterator, which is exactly what `yield from ` expresses. * refactor(core.utils): extract _iter_tmp_entries and _sum_tmp_sizes The same scandir-then-stat-for-size loop appeared in three places (`_compute_total_size`, `_sweep_stale_tmp_files`, `_enforce_size_cap`). `_iter_tmp_entries` covers the scandir + is_file filter + context-managed cleanup that all three need. `_sum_tmp_sizes` covers the size accumulation that both `_compute_total_size` and `_enforce_size_cap` need. Net result: each callsite now reads as one obvious line instead of seven, and a future change to the temp-walk discipline (e.g. switching to a different scan strategy) lands in one place. * fix(core.utils): clamp tracked size at zero on __delitem__ `__delitem__` could walk `_tracked_size_bytes` negative under a race with `_enforce_size_cap`'s reseed: if the eviction scan runs AFTER this delete unlinks (so its reseed value excludes the deleted entry) but BEFORE this delete's subtract, the subtract undercounts by `size`. Repeated under contention, the tracker crosses zero -- and once negative, the `tracker > cap` check that gates eviction never fires again, so the cache grows without bound and there is no self-healing path (the only reseed point is the function that no longer runs). Clamp `tracker = max(0, tracker - size)` so the tracker can't enter the permanently-broken state. Worst case after the race is undercounting reality, which the next eviction's reseed corrects; that's the same self-healing path the existing tests already exercise. Reported by leofang in PR review. --- cuda_core/cuda/core/_program.pxd | 3 +- cuda_core/cuda/core/_program.pyx | 151 +- cuda_core/cuda/core/utils.py | 8 - cuda_core/cuda/core/utils/__init__.py | 23 + .../core/utils/_program_cache/__init__.py | 36 + .../cuda/core/utils/_program_cache/_abc.py | 204 ++ .../core/utils/_program_cache/_file_stream.py | 751 ++++++ .../core/utils/_program_cache/_in_memory.py | 127 + .../cuda/core/utils/_program_cache/_keys.py | 813 ++++++ cuda_core/docs/source/api.rst | 20 + cuda_core/tests/test_program.py | 17 + cuda_core/tests/test_program_cache.py | 2346 +++++++++++++++++ .../tests/test_program_cache_multiprocess.py | 192 ++ cuda_core/tests/test_program_compile_cache.py | 392 +++ 14 files changed, 5067 insertions(+), 16 deletions(-) delete mode 100644 cuda_core/cuda/core/utils.py create mode 100644 cuda_core/cuda/core/utils/__init__.py create mode 100644 cuda_core/cuda/core/utils/_program_cache/__init__.py create mode 100644 cuda_core/cuda/core/utils/_program_cache/_abc.py create mode 100644 cuda_core/cuda/core/utils/_program_cache/_file_stream.py create mode 100644 cuda_core/cuda/core/utils/_program_cache/_in_memory.py create mode 100644 cuda_core/cuda/core/utils/_program_cache/_keys.py create mode 100644 cuda_core/tests/test_program_cache.py create mode 100644 cuda_core/tests/test_program_cache_multiprocess.py create mode 100644 cuda_core/tests/test_program_compile_cache.py diff --git a/cuda_core/cuda/core/_program.pxd b/cuda_core/cuda/core/_program.pxd index b2feeba860d..cea430c3f20 100644 --- a/cuda_core/cuda/core/_program.pxd +++ b/cuda_core/cuda/core/_program.pxd @@ -17,5 +17,6 @@ cdef class Program: object _compile_lock # Per-instance lock for compile-time mutation bint _use_libdevice # Flag for libdevice loading bint _libdevice_added - bytes _nvrtc_code # Source code for NVRTC retry (PCH auto-resize) + bytes _code # Source code as bytes: used for key derivation and NVRTC PCH retry + str _code_type # Normalised code_type ("c++", "ptx", "nvvm") str _pch_status # PCH creation outcome after compile diff --git a/cuda_core/cuda/core/_program.pyx b/cuda_core/cuda/core/_program.pyx index 12d52198b2a..eda4c68ce91 100644 --- a/cuda_core/cuda/core/_program.pyx +++ b/cuda_core/cuda/core/_program.pyx @@ -85,7 +85,12 @@ cdef class Program: self._h_nvvm.reset() def compile( - self, target_type: ObjectCodeFormatType | str, name_expressions: tuple | list = (), logs = None + self, + target_type: ObjectCodeFormatType | str, + name_expressions: tuple | list = (), + logs=None, + *, + cache: "ProgramCacheResource | None" = None, ) -> ObjectCode: """Compile the program to the specified target type. @@ -98,13 +103,126 @@ cdef class Program: Used for template instantiation and similar cases. logs : object, optional Object with a ``write`` method to receive compilation logs. + On a cache hit no compilation runs and ``logs`` receives + nothing -- callers that rely on log output to confirm a + compile happened should compile without ``cache=``. + cache : :class:`~cuda.core.utils.ProgramCacheResource`, optional + If provided, the compiled binary is looked up in ``cache`` via a + key derived from the program's code, options, and ``target_type``. + On a hit the cached bytes are wrapped in a fresh + :class:`~cuda.core.ObjectCode` (with the same ``target_type`` + and ``ProgramOptions.name``) and returned without re-compiling; + on a miss the compile output is stored as raw bytes (the cache + extracts ``bytes(object_code.code)``). Passing a non-empty + ``name_expressions`` together with ``cache=`` raises + ``ValueError``: NVRTC populates + ``ObjectCode.symbol_mapping`` at compile time and that mapping + is not carried in the binary the cache stores, so cache hits + would silently miss ``get_kernel(name_expression)`` lookups. + Options that require an ``extra_digest`` (``include_path``, + ``pre_include``, ``pch``, ``use_pch``, ``pch_dir``, NVVM + ``use_libdevice=True``, or NVRTC ``options.name`` with a + directory component) raise ``ValueError`` via + :func:`~cuda.core.utils.make_program_cache_key`; for those + compiles, use the manual ``make_program_cache_key(...)`` + pattern directly. + + ``cache=`` is independent of ``ProgramOptions.no_cache``: the + former controls this program-level cache (compiled-output + reuse across calls), while ``no_cache`` is forwarded to the + Linker to disable its in-process JIT cache for cuLink/nvJitLink. + Setting ``options.no_cache=True`` does not bypass ``cache=``, + and vice-versa. Returns ------- :class:`~cuda.core.ObjectCode` The compiled object code. """ - return Program_compile(self, str(target_type), name_expressions, logs) + # Mirror Program_init's code_type normalization so callers can pass + # ``ObjectCodeFormatType.PTX`` or ``"PTX"`` and get the same routing + # / cache key as the lowercase string. ``Program_compile_nvrtc`` + # keys on lowercase ``target_type`` and ``make_program_cache_key`` + # lowercases too. + target_type = str(target_type).lower() + + if cache is None: + return _program_compile_uncached(self, target_type, name_expressions, logs) + + # Deferred import to avoid a circular import between _program and + # cuda.core.utils._program_cache (the cache module already imports + # ProgramOptions from this module). Import from the leaf module so + # tests that monkeypatch make_program_cache_key via that path + # intercept reliably. + from cuda.core.utils._program_cache import ( + ProgramCacheResource, + make_program_cache_key, + ) + + if not isinstance(cache, ProgramCacheResource): + raise TypeError( + "cache must be an instance of " + "cuda.core.utils.ProgramCacheResource; got " + f"{type(cache).__name__}" + ) + + # ``name_expressions`` is incompatible with the cache: NVRTC + # populates ``ObjectCode.symbol_mapping`` from name-expression + # mangling at compile time, and that mapping isn't carried in + # the binary bytes the cache stores. Without this guard the + # first call (cache miss) would return an ObjectCode with + # symbol_mapping populated, while every subsequent call (hit) + # would return one without -- silently breaking later + # ``get_kernel(name_expression)`` lookups that work on the + # uncached path. Fail loud here instead. + if name_expressions: + raise ValueError( + "Program.compile(cache=...) does not support name_expressions: " + "ObjectCode.symbol_mapping is populated by NVRTC at compile " + "time and is not preserved across a cache round-trip, so cache " + "hits would silently break get_kernel(name_expression) lookups " + "that the uncached path supports. Compile without cache= when " + "name_expressions are needed, or look up mangled symbols by " + "hand from the cached ObjectCode." + ) + + # ``self._code`` is always stored as bytes (see ``Program_init``), + # but ``make_program_cache_key`` only accepts bytes when + # ``code_type == "nvvm"`` -- c++/ptx must be ``str``. The bytes + # came from ``code.encode()`` on a ``str`` Program_init validated + # via ``assert_type(code, str)``, so this round-trip is always + # safe; no try/except needed. + code_for_key = self._code if self._code_type == "nvvm" else self._code.decode("utf-8") + + key = make_program_cache_key( + code=code_for_key, + code_type=self._code_type, + options=self._options, + target_type=target_type, + ) + hit_bytes = cache.get(key) + if hit_bytes is not None: + # The uncached NVRTC path warns when the active driver can't + # load freshly-generated PTX; that loadability is a property + # of the driver, not of how the bytes were produced, so the + # warning applies equally to cached PTX. Mirror it here so a + # cache hit doesn't silently hide an incompatibility that the + # uncached call would have surfaced. + if ( + self._backend == "NVRTC" + and target_type == "ptx" + and not _can_load_generated_ptx() + ): + warn( + "The CUDA driver version is older than the backend version. " + "The generated ptx will not be loadable by the current driver.", + stacklevel=2, + category=RuntimeWarning, + ) + return ObjectCode._init(hit_bytes, target_type, name=self._options.name) + compiled = _program_compile_uncached(self, target_type, name_expressions, logs) + cache[key] = compiled + return compiled @property def pch_status(self) -> PCHStatusType | None: @@ -505,6 +623,19 @@ class ProgramOptions: # Private Classes and Helper Functions # ============================================================================= + +def _program_compile_uncached(program, target_type, name_expressions, logs): + """Run ``Program_compile`` without the cache wrapper. + + Module-level Python function so tests can monkeypatch it from + ``cuda.core._program`` to avoid invoking NVRTC when exercising the cache + wrapper in :meth:`Program.compile`. ``Program`` itself is a ``cdef class`` + and its methods cannot be reassigned from Python, so the seam must live + outside the class. + """ + return Program_compile(program, target_type, name_expressions, logs) + + # Module-level state for NVVM lazy loading _nvvm_module = None _nvvm_import_attempted = False @@ -620,6 +751,7 @@ cdef inline int Program_init(Program self, object code, str code_type, object op self._options = options = check_or_create_options(ProgramOptions, options, "Program options") code_type = code_type.lower() + self._code_type = code_type self._compile_lock = threading.Lock() self._use_libdevice = False self._libdevice_added = False @@ -640,7 +772,7 @@ cdef inline int Program_init(Program self, object code, str code_type, object op HANDLE_RETURN_NVRTC(NULL, cynvrtc.nvrtcCreateProgram( &nvrtc_prog, code_ptr, name_ptr, 0, NULL, NULL)) self._h_nvrtc = create_nvrtc_program_handle(nvrtc_prog) - self._nvrtc_code = code_bytes + self._code = code_bytes self._backend = str(CompilerBackendType.NVRTC) self._linker = None @@ -648,8 +780,10 @@ cdef inline int Program_init(Program self, object code, str code_type, object op assert_type(code, str) if options.extra_sources is not None: raise ValueError("extra_sources is not supported by the PTX backend.") + code_bytes = code.encode() + self._code = code_bytes self._linker = Linker( - ObjectCode._init(code.encode(), code_type), options=_translate_program_options(options) + ObjectCode._init(code_bytes, code_type), options=_translate_program_options(options) ) self._backend = str(self._linker.backend) @@ -659,10 +793,13 @@ cdef inline int Program_init(Program self, object code, str code_type, object op code = code.encode("utf-8") elif not isinstance(code, (bytes, bytearray)): raise TypeError("NVVM IR code must be provided as str, bytes, or bytearray") + self._code = bytes(code) # Coerce bytearray -> bytes so retention type is stable - code_ptr = (code) + # Use self._code (strictly bytes) for the C pointer so a bytearray + # input doesn't trip the `code` cast at runtime. + code_ptr = self._code name_ptr = options._name - code_len = len(code) + code_len = len(self._code) with nogil: HANDLE_RETURN_NVVM(NULL, cynvvm.nvvmCreateProgram(&nvvm_prog)) @@ -828,7 +965,7 @@ cdef object Program_compile_nvrtc(Program self, str target_type, object name_exp HANDLE_RETURN_NVRTC(NULL, cynvrtc.nvrtcSetPCHHeapSize(required)) cdef cynvrtc.nvrtcProgram retry_prog - cdef const char* code_ptr = self._nvrtc_code + cdef const char* code_ptr = self._code cdef const char* name_ptr = self._options._name with nogil: HANDLE_RETURN_NVRTC(NULL, cynvrtc.nvrtcCreateProgram( diff --git a/cuda_core/cuda/core/utils.py b/cuda_core/cuda/core/utils.py deleted file mode 100644 index f15d9242778..00000000000 --- a/cuda_core/cuda/core/utils.py +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -from cuda.core._memoryview import ( - StridedMemoryView, # noqa: F401 - args_viewable_as_strided_memory, # noqa: F401 -) diff --git a/cuda_core/cuda/core/utils/__init__.py b/cuda_core/cuda/core/utils/__init__.py new file mode 100644 index 00000000000..45b900600bb --- /dev/null +++ b/cuda_core/cuda/core/utils/__init__.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from cuda.core._memoryview import ( + StridedMemoryView, + args_viewable_as_strided_memory, +) +from cuda.core.utils._program_cache import ( + FileStreamProgramCache, + InMemoryProgramCache, + ProgramCacheResource, + make_program_cache_key, +) + +__all__ = [ + "FileStreamProgramCache", + "InMemoryProgramCache", + "ProgramCacheResource", + "StridedMemoryView", + "args_viewable_as_strided_memory", + "make_program_cache_key", +] diff --git a/cuda_core/cuda/core/utils/_program_cache/__init__.py b/cuda_core/cuda/core/utils/_program_cache/__init__.py new file mode 100644 index 00000000000..a233ad31ad5 --- /dev/null +++ b/cuda_core/cuda/core/utils/_program_cache/__init__.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Persistent program cache for cuda.core. + +Public surface: + +* :class:`ProgramCacheResource` -- bytes-in / bytes-out ABC. +* :class:`InMemoryProgramCache` -- thread-safe LRU dict-backed cache. +* :class:`FileStreamProgramCache` -- atomic, multi-process directory cache. +* :func:`make_program_cache_key` -- key derivation for arbitrary + ``Program`` configurations. + +The package is split into submodules by concern. Tests that need to +monkeypatch internals (Windows flag, version probes, helpers, ...) +should reach into the owning submodule (e.g. +``_program_cache._file_stream._IS_WINDOWS``, +``_program_cache._keys._linker_backend_and_version``) rather than the +package object: the symbols re-exported here are only convenience +aliases and don't intercept calls within the submodules. +""" + +from __future__ import annotations + +from ._abc import ProgramCacheResource +from ._file_stream import FileStreamProgramCache +from ._in_memory import InMemoryProgramCache +from ._keys import make_program_cache_key + +__all__ = [ + "FileStreamProgramCache", + "InMemoryProgramCache", + "ProgramCacheResource", + "make_program_cache_key", +] diff --git a/cuda_core/cuda/core/utils/_program_cache/_abc.py b/cuda_core/cuda/core/utils/_program_cache/_abc.py new file mode 100644 index 00000000000..82233540eb3 --- /dev/null +++ b/cuda_core/cuda/core/utils/_program_cache/_abc.py @@ -0,0 +1,204 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Abstract base class and value-coercion helpers shared by every cache backend.""" + +from __future__ import annotations + +import abc +import collections.abc +from pathlib import Path + +from cuda.core._module import ObjectCode + + +def _extract_bytes(value: object) -> bytes: + """Return the raw binary bytes to store on disk. + + Accepts ``bytes``, ``bytearray``, ``memoryview``, or any + :class:`ObjectCode`. Path-backed ``ObjectCode`` (created via + ``ObjectCode.from_cubin('/path')`` etc.) is read from the filesystem + at write time so the cached entry is the binary content itself, not + a path that could later be moved or modified. + """ + if isinstance(value, (bytes, bytearray, memoryview)): + return bytes(value) + if isinstance(value, ObjectCode): + code = value.code + if isinstance(code, str): + try: + return Path(code).read_bytes() + except FileNotFoundError: + # Avoid ``{code!r}``: on Windows it doubles every backslash in + # the path, which breaks naive ``str(path) in str(exc)`` checks + # callers (and tests) write against the raw filesystem string. + raise FileNotFoundError( + f"cannot store path-backed ObjectCode in cache: {code} no longer exists" + ) from None + return bytes(code) + raise TypeError(f"cache values must be bytes-like or ObjectCode, got {type(value).__name__}") + + +def _as_key_bytes(key: object) -> bytes: + if isinstance(key, (bytes, bytearray)): + return bytes(key) + if isinstance(key, str): + return key.encode("utf-8") + raise TypeError(f"cache keys must be bytes or str, got {type(key).__name__}") + + +class ProgramCacheResource(abc.ABC): + """Abstract base class for compiled-program caches. + + Concrete implementations store and retrieve **raw binary bytes** keyed + by ``bytes`` or ``str``. A ``str`` key is encoded as UTF-8 before + being used, so ``"k"`` and ``b"k"`` refer to the same entry. A typical + key is produced by :func:`make_program_cache_key`, which returns + ``bytes``. + + The values written are the compiled program bytes themselves -- + cubin, PTX, LTO-IR, etc. Reads return raw bytes so cache files + remain consumable by external NVIDIA tools (``cuobjdump``, + ``nvdisasm``, ``cuda-gdb``, ...). + + Most callers don't interact with this object directly. The + recommended usage is :meth:`cuda.core.Program.compile`'s ``cache=`` + keyword, which derives the key, returns a fresh + :class:`~cuda.core.ObjectCode` on hit, and stores the compile + result on miss:: + + with FileStreamProgramCache() as cache: + obj = program.compile("cubin", cache=cache) + + The escape hatch -- only needed when the compile inputs require an + ``extra_digest`` (header / PCH content fingerprints, NVVM + libdevice) -- is to call :func:`make_program_cache_key` yourself + and use the cache as a plain ``bytes`` mapping:: + + from cuda.core import ObjectCode + + key = make_program_cache_key( + code=source, + code_type="c++", + options=options, + target_type="cubin", + extra_digest=header_fingerprint(), + ) + data = cache.get(key) + if data is None: + obj = program.compile("cubin") + cache[key] = obj # extracts bytes(obj.code) + else: + obj = ObjectCode.from_cubin(data) + + The cache layer does no payload validation; bytes go in and come + back out unchanged. Symbol-mapping metadata that + :class:`~cuda.core.ObjectCode` carries when produced with NVRTC + name expressions is **not** preserved across a cache round-trip -- + the binary alone is stored. Callers that need ``symbol_mapping`` + for ``get_kernel(name_expression)`` should compile fresh, or look + the mangled symbol up by hand. + + .. note:: **Concurrent-access idiom.** + + Use :meth:`get` (or ``data = cache[key]`` inside a ``try / + except KeyError``) for lookups. There is intentionally no + ``__contains__``: the obvious ``if key in cache: data = + cache[key]`` idiom is racy across processes (another writer + can ``os.replace`` over the entry, or eviction can unlink + it, between the check and the read), and exposing + ``__contains__`` invites that pattern. ``get`` answers + both questions in one filesystem-level operation, so a + successful return always carries the bytes. + """ + + @abc.abstractmethod + def __getitem__(self, key: bytes | str) -> bytes: + """Retrieve the cached binary bytes. + + Raises + ------ + KeyError + If ``key`` is not in the cache. + """ + + @abc.abstractmethod + def __setitem__(self, key: bytes | str, value: bytes | bytearray | memoryview | ObjectCode) -> None: + """Store ``value`` under ``key``. + + Path-backed :class:`~cuda.core.ObjectCode` is read from disk at + write time so the cached entry holds the bytes, not a path. + """ + + @abc.abstractmethod + def __delitem__(self, key: bytes | str) -> None: + """Remove the entry associated with ``key``. + + Raises + ------ + KeyError + If ``key`` is not in the cache. + """ + + @abc.abstractmethod + def __len__(self) -> int: + """Return the number of entries currently in the cache. + + Implementations that store entries on disk by hashed key may + count orphaned files (entries from a previous + ``_KEY_SCHEMA_VERSION`` that are still on disk but no longer + reachable by post-bump lookups) until eviction reaps them. + Callers that need an exact count of live entries should not + rely on ``__len__`` across schema bumps. + """ + + @abc.abstractmethod + def clear(self) -> None: + """Remove every entry from the cache.""" + + def get(self, key: bytes | str, default: bytes | None = None) -> bytes | None: + """Return ``self[key]`` or ``default`` if absent.""" + try: + return self[key] + except KeyError: + return default + + def update( + self, + items: ( + collections.abc.Mapping[bytes | str, bytes | bytearray | memoryview | ObjectCode] + | collections.abc.Iterable[tuple[bytes | str, bytes | bytearray | memoryview | ObjectCode]] + ), + /, + ) -> None: + """Bulk ``__setitem__``. + + Accepts a mapping or an iterable of ``(key, value)`` pairs. Each + write goes through ``__setitem__`` so backend-specific value + coercion (e.g. extracting bytes from an :class:`~cuda.core.ObjectCode`) + and size-cap enforcement run on every entry. Not transactional -- + a failure mid-iteration leaves earlier writes committed. + """ + if isinstance(items, collections.abc.Mapping): + items = items.items() + for key, value in items: + self[key] = value + + def close(self) -> None: # noqa: B027 + """Release backend resources. + + The default implementation does nothing. Subclasses that hold + long-lived state (open file handles, database connections, + network sockets, ...) should override this to release them. + + Callers should use the context-manager form (``with cache:``) + or call :meth:`close` explicitly when finished, so code stays + portable across backends that do hold resources. + """ + + def __enter__(self) -> ProgramCacheResource: + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.close() diff --git a/cuda_core/cuda/core/utils/_program_cache/_file_stream.py b/cuda_core/cuda/core/utils/_program_cache/_file_stream.py new file mode 100644 index 00000000000..eccf494c99b --- /dev/null +++ b/cuda_core/cuda/core/utils/_program_cache/_file_stream.py @@ -0,0 +1,751 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""On-disk bytes-in / bytes-out program cache. + +Atomic writes via :func:`os.replace`. Concurrent readers see either the +old entry or the new one, never a partial file. Each entry is the raw +compiled binary so files are directly consumable by external NVIDIA +tools (``cuobjdump``, ``nvdisasm``, ``cuda-gdb``). +""" + +from __future__ import annotations + +import contextlib +import errno +import hashlib +import os +import tempfile +import threading +import time +from pathlib import Path +from typing import Iterable + +from cuda.core._module import ObjectCode + +from ._abc import ProgramCacheResource, _as_key_bytes, _extract_bytes + +_ENTRIES_SUBDIR = "entries" +_TMP_SUBDIR = "tmp" +# Temp files older than this are assumed to belong to a crashed writer and +# are eligible for cleanup. Picked large enough that no real ``os.replace`` +# write should still be in flight (writes are bounded by mkstemp + write + +# fsync + replace, all fast on healthy disks). +_TMP_STALE_AGE_SECONDS = 3600 + + +_SHARING_VIOLATION_WINERRORS = (5, 32, 33) # ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION, ERROR_LOCK_VIOLATION +_REPLACE_RETRY_DELAYS = (0.0, 0.005, 0.010, 0.020, 0.050, 0.100) # ~185ms budget + + +# Exposed as a module-level flag so tests can toggle it without monkeypatching +# ``os.name`` itself (pathlib reads ``os.name`` at instantiation time). +_IS_WINDOWS = os.name == "nt" + + +def _stat_key(st: os.stat_result) -> tuple: + """Stat fingerprint used by every stat-guarded path. + + ``(st_ino, st_size, st_mtime_ns)`` is the smallest triple that + distinguishes "same file" from "file replaced under us": ``st_ino`` + catches replacement, ``st_size`` and ``st_mtime_ns`` catch a write + that happens to land on the same inode (e.g. truncate-and-write in + place). Centralised so all four readers compare the same fields. + """ + return (st.st_ino, st.st_size, st.st_mtime_ns) + + +def _default_cache_dir() -> Path: + """OS-conventional default location for the file-stream cache. + + Resolves to the user-cache root for the calling user, with a + ``program-cache`` leaf so future tooling can place sibling caches + under the same ``cuda-python`` vendor directory: + + * Linux: ``$XDG_CACHE_HOME/cuda-python/program-cache`` + (default ``~/.cache/cuda-python/program-cache`` per the XDG Base + Directory spec). + * Windows: ``%LOCALAPPDATA%\\cuda-python\\program-cache`` + (Windows uses local AppData -- caches don't roam; falls back to + ``~/AppData/Local`` if the env var is unset). + + CUDA does not support macOS, so no macOS branch is provided. + """ + if _IS_WINDOWS: + local_app_data = os.environ.get("LOCALAPPDATA") + root = Path(local_app_data) if local_app_data else Path.home() / "AppData" / "Local" + else: + xdg = os.environ.get("XDG_CACHE_HOME") + root = Path(xdg) if xdg else Path.home() / ".cache" + return root / "cuda-python" / "program-cache" + + +def _with_sharing_retry(op, *args, on_exhausted=None, **kwargs): + """Run ``op(*args, **kwargs)`` retrying transient Windows sharing + violations under the bounded ``_REPLACE_RETRY_DELAYS`` budget. + + On Windows, ``os.replace``/``read_bytes``/``unlink`` can surface + winerror 5/32/33 (or bare EACCES via ``_is_windows_sharing_violation``) + while another process briefly holds the file open without share-delete + rights. The retry hides that contention. Other ``PermissionError``s + (real ACLs, unexpected winerror) propagate immediately. + + Successful returns and any non-``PermissionError`` exceptions + (including ``FileNotFoundError``) bubble up unchanged. After the + budget is exhausted, the helper either calls ``on_exhausted(last_exc)`` + if provided, or re-raises the last sharing-violation exception. + """ + last_exc: PermissionError | None = None + for delay in _REPLACE_RETRY_DELAYS: + if delay: + time.sleep(delay) + try: + return op(*args, **kwargs) + except PermissionError as exc: + if not _is_windows_sharing_violation(exc): + raise + last_exc = exc + if on_exhausted is not None: + return on_exhausted(last_exc) + assert last_exc is not None # at least one iteration ran and caught a PermissionError + raise last_exc + + +def _replace_with_sharing_retry(tmp_path: Path, target: Path) -> bool: + """Atomic rename with Windows-specific retry on sharing/lock violations. + + Returns True on success. Returns False only after the retry budget is + exhausted on Windows with a genuine sharing violation -- the caller then + treats the cache write as dropped. Any other ``PermissionError`` (ACLs, + read-only dir, unexpected winerror, or any POSIX failure) propagates. + + ``ERROR_ACCESS_DENIED`` (winerror 5) is treated as a sharing violation + because Windows surfaces it when a file is held open without + ``FILE_SHARE_WRITE`` (Python's default for ``open(p, "wb")``) or while + a previous unlink is in ``PENDING_DELETE`` -- both are transient. + """ + + def _do_replace() -> bool: + os.replace(tmp_path, target) + return True + + return _with_sharing_retry(_do_replace, on_exhausted=lambda _exc: False) + + +def _stat_and_read_with_sharing_retry(path: Path) -> tuple[os.stat_result, bytes]: + """Snapshot stat and read bytes, retrying briefly on Windows transient + sharing-violation ``PermissionError``. + + Reads race the rewriter's ``os.replace``: on Windows, the destination + can be momentarily inaccessible (winerror 5/32/33) while the rename + completes. Mirroring ``_replace_with_sharing_retry``'s budget keeps + transient contention from being mistaken for a real read failure. + + Raises ``FileNotFoundError`` on miss or after exhausting the Windows + sharing-retry budget. Non-Windows ``PermissionError`` propagates. + + On Windows, EACCES (errno 13) is treated as transient too: ``io.open`` + sometimes surfaces a pending-delete or share-mode mismatch as bare + EACCES with no ``winerror`` attribute, indistinguishable here from + a true sharing violation. Real ACL problems on a path the cache owns + would surface consistently; the bounded retry budget keeps the cost + of treating them as transient negligible. + """ + + def _do_stat_and_read() -> tuple[os.stat_result, bytes]: + return path.stat(), path.read_bytes() + + def _exhausted(last_exc): + raise FileNotFoundError(path) from last_exc + + return _with_sharing_retry(_do_stat_and_read, on_exhausted=_exhausted) + + +_UTIME_SUPPORTS_FD = os.utime in os.supports_fd + + +def _touch_atime(path: Path, st_before: os.stat_result) -> None: + """Bump ``path``'s atime to "now", preserving its mtime, iff the + file's stat still matches ``st_before``. + + Eviction sorts by ``st_atime`` so reads must reliably refresh atime + regardless of OS or filesystem default behavior: + + * Linux ``relatime`` (default) only updates atime when the existing + atime is older than mtime, which would skew LRU once an entry has + been read once. + * NTFS on Windows Vista+ disables atime updates by default + (``NtfsDisableLastAccessUpdate``) and most modern installations + keep that off, so a bare read never bumps atime. + * ``noatime``-mounted filesystems disable updates entirely. + + Calling ``os.utime`` with explicit times bypasses all of the above + and writes atime directly. The stat-guard is critical: if another + process ``os.replace``-d a fresh entry into ``path`` between the + read and this touch, blindly applying ``st_before.st_mtime_ns`` + would roll the new entry's mtime back to the old value and confuse + the eviction stat-guard (which checks ``(ino, size, mtime_ns)``) + into deleting a freshly-committed file. + + Where ``os.utime`` supports file descriptors (Linux, macOS), the + fstat-then-utime pair runs against the same open fd: even if another + writer replaces the path between our ``os.open`` and the ``fstat``, + the fd still refers to the file we opened, so the comparison and the + utime both target the same inode. This closes the residual TOCTOU + window that a path-based stat + path-based utime would have. + + On Windows, ``os.utime`` is path-only; the fallback re-stats the + path and accepts a small TOCTOU window between the second stat and + the utime. That window is microseconds and the worst-case outcome + is the racing writer's mtime being rolled back by a few hundred + nanoseconds -- the eviction stat-guard would then refuse to evict + the slightly-stale entry, costing one cache miss (recompile) but + not a corrupt eviction. + + Best-effort: any ``OSError`` (read-only mount, restrictive ACLs, + ...) is swallowed -- size enforcement still bounds the cache, but + eviction degrades toward FIFO. + """ + new_atime_ns = time.time_ns() + if _UTIME_SUPPORTS_FD: + try: + fd = os.open(path, os.O_RDONLY) + except OSError: + return + try: + try: + st_now = os.fstat(fd) + except OSError: + return + if _stat_key(st_now) != _stat_key(st_before): + return + with contextlib.suppress(OSError): + os.utime(fd, ns=(new_atime_ns, st_before.st_mtime_ns)) + finally: + os.close(fd) + return + + # Path-based fallback (Windows). Best-effort -- residual TOCTOU window + # documented above. + try: + st_now = path.stat() + except OSError: + return + if _stat_key(st_now) != _stat_key(st_before): + return + with contextlib.suppress(OSError): + os.utime(path, ns=(new_atime_ns, st_before.st_mtime_ns)) + + +def _is_windows_sharing_violation(exc: BaseException) -> bool: + """Return True if ``exc`` is a Windows sharing/lock violation that + :func:`_unlink_with_sharing_retry` would have retried. + + Used by best-effort callers to filter out the exhausted-retry case + while letting other ``PermissionError`` instances (POSIX ACL + issues, Windows non-sharing winerrors) propagate -- those are real + configuration problems, not transient contention. + + The ``EACCES`` fallback only fires when ``winerror`` is absent: a + bare ``EACCES`` (no winerror attached) is the way ``io.open`` + surfaces a pending-delete or share-mode mismatch on Windows. When + ``winerror`` IS set but is NOT in the sharing set, the OS told us + exactly what failed and it isn't a sharing violation -- treating it + as transient would silently swallow real errors like a corrupt + ACL. + """ + if not _IS_WINDOWS: + return False + if not isinstance(exc, PermissionError): + return False + winerror = getattr(exc, "winerror", None) + if winerror in _SHARING_VIOLATION_WINERRORS: + return True + return winerror is None and exc.errno == errno.EACCES + + +def _unlink_with_sharing_retry(path: Path) -> None: + """Unlink with Windows-specific retry on sharing/lock violations. + + On Windows, ``Path.unlink`` raises ``PermissionError`` (winerror 5, + 32, or 33; sometimes bare ``EACCES``) when another process holds + the file open without ``FILE_SHARE_DELETE``. Python's default + ``open(p, "rb")`` does not pass that flag, so a reader from another + process briefly blocks our unlink while it reads. Retry with the + same backoff budget as :func:`_replace_with_sharing_retry` so + transient contention is not turned into a propagated error. + + Raises ``FileNotFoundError`` if the file is absent; the last + ``PermissionError`` if the Windows retry budget is exhausted; and + propagates any non-sharing ``PermissionError`` (or any non-Windows + ``PermissionError``) immediately. Best-effort callers should use + :func:`_is_windows_sharing_violation` to filter the exhausted-retry + case and re-raise any other ``PermissionError``. + """ + _with_sharing_retry(path.unlink) + + +def _prune_if_stat_unchanged(path: Path, st_before: os.stat_result) -> None: + """Unlink ``path`` iff its stat still matches ``st_before``. + + Guards against a cross-process race: a reader that sees a corrupt + record can have it atomically replaced (via ``os.replace``) by a + writer before the reader decides to prune. Comparing + ``(ino, size, mtime_ns)`` before and after rules out that case -- + any mismatch means someone else wrote a new file and we must not + delete their work. The residual TOCTOU window between stat and + unlink is narrow; worst case, a very-recently-written entry is + removed and the next read recompiles. + + Best-effort: a Windows sharing violation that survives the retry + budget leaves the file in place. The caller is in an eviction or + cleanup pass, so re-trying on the next pass is the right outcome. + """ + try: + st_now = path.stat() + except FileNotFoundError: + return + if _stat_key(st_before) != _stat_key(st_now): + return + try: + _unlink_with_sharing_retry(path) + except FileNotFoundError: + pass + except PermissionError as exc: + # Swallow only the exhausted-Windows-sharing case. POSIX ACL + # errors and Windows non-sharing winerrors are real configuration + # problems and must surface, not be silently lost during a prune. + if not _is_windows_sharing_violation(exc): + raise + + +class FileStreamProgramCache(ProgramCacheResource): + """Persistent program cache backed by a directory of atomic files. + + Designed for multi-process use: writes stage a temporary file and then + :func:`os.replace` it into place, so concurrent readers never observe a + partially-written entry. Each entry on disk is the raw compiled binary + -- cubin / PTX / LTO-IR -- with no header, framing, or pickle wrapper, + so the files are directly consumable by external NVIDIA tools + (``cuobjdump``, ``nvdisasm``, ``cuda-gdb``). + + Eviction is by least-recently-*read* time: every successful read bumps + the entry's ``atime``, and the size enforcer evicts oldest atime + first. + + .. note:: **Best-effort writes.** + + On Windows, ``os.replace`` raises ``PermissionError`` (winerror + 32 / 33) when another process holds the target file open. This + backend retries with bounded backoff (~185 ms) and, if still + failing, drops the cache write silently and returns success-shaped + control flow. The next call will see no entry and recompile. POSIX + and other ``PermissionError`` codes propagate. + + .. note:: **Atomic for readers, not crash-durable.** + + Each entry's temp file is ``fsync``-ed before ``os.replace``, but + the containing directory is **not** ``fsync``-ed. A host crash + between write and the next directory commit may lose recently + added entries; surviving entries remain consistent. + + .. note:: **Cross-version sharing.** + + The cache is safe to share across ``cuda.core`` patch releases: + every key produced by :func:`make_program_cache_key` encodes the + relevant backend/compiler/runtime fingerprints for its + compilation path (NVRTC entries pin the NVRTC version, NVVM + entries pin the libNVVM library and IR versions, PTX/linker + entries pin the chosen linker backend and its version -- and, + when the cuLink/driver backend is selected, the driver version + too; nvJitLink-backed PTX entries are deliberately + driver-version independent). Bumping ``_KEY_SCHEMA_VERSION`` + (mixed into the digest by ``make_program_cache_key``) produces + new keys that don't collide with old entries: post-bump + lookups miss the old on-disk paths, and the orphaned files + are reaped on the next size-cap eviction pass. Entries are + stored verbatim as the compiled binary, so cross-patch sharing + only requires that the compiler-pinning surface above stays + stable -- there is no Python-pickle compatibility involved. + + Parameters + ---------- + path: + Directory that owns the cache. Created if missing. If omitted, + the OS-conventional user cache directory is used: + ``$XDG_CACHE_HOME/cuda-python/program-cache`` (Linux, defaulting + to ``~/.cache/cuda-python/program-cache``) or + ``%LOCALAPPDATA%\\cuda-python\\program-cache`` (Windows). + max_size_bytes: + Optional soft cap on total on-disk size. Enforced opportunistically + on writes; concurrent writers may briefly exceed it. Eviction is by + least-recently-read time (oldest ``st_atime`` first). + """ + + def __init__( + self, + path: str | os.PathLike | None = None, + *, + max_size_bytes: int | None = None, + ) -> None: + if max_size_bytes is not None and max_size_bytes <= 0: + raise ValueError("max_size_bytes must be positive or None (0 would evict every write)") + self._root = Path(path) if path is not None else _default_cache_dir() + self._entries = self._root / _ENTRIES_SUBDIR + self._tmp = self._root / _TMP_SUBDIR + self._max_size_bytes = max_size_bytes + self._root.mkdir(parents=True, exist_ok=True) + self._entries.mkdir(exist_ok=True) + self._tmp.mkdir(exist_ok=True) + # Opportunistic startup sweep of orphaned temp files left by any + # crashed writers. Age-based so concurrent in-flight writes from + # other processes are preserved. + self._sweep_stale_tmp_files() + # Incremental size tracker. Without it every ``__setitem__`` would + # walk ``entries/`` + ``tmp/`` to compute the total -- O(n) per + # write. With it: writes update the tracker by the net delta in O(1) + # and only walk on eviction (which already needs the scan to sort + # entries by atime). The tracker is seeded by one full scan at open + # time and refreshed on every eviction pass; cross-process drift + # (other writers/deleters) self-corrects the next time eviction + # fires. The lock guards mutations so multi-threaded writers in + # the same process don't interleave the read-modify-write on the + # int. Skipped entirely when ``max_size_bytes is None`` -- without + # a cap the tracker is dead weight. + self._size_lock = threading.Lock() + self._tracked_size_bytes = self._compute_total_size() if max_size_bytes is not None else 0 + + # -- key-to-path helpers ------------------------------------------------- + + def _path_for_key(self, key: object) -> Path: + k = _as_key_bytes(key) + # Hash the key to a fixed-length identifier so arbitrary-length user + # keys never exceed per-component filename limits (typically 255 on + # ext4 / NTFS). With a 256-bit blake2b digest, the cache relies on + # cryptographic collision resistance for key uniqueness -- two + # distinct keys hashing to the same path is astronomically unlikely + # (~2^-128 with the 32-byte digest in use here). + digest = hashlib.blake2b(k, digest_size=32).hexdigest() + return self._entries / digest[:2] / digest[2:] + + # -- mapping API --------------------------------------------------------- + + def __getitem__(self, key: object) -> bytes: + path = self._path_for_key(key) + try: + # The helper retries on Windows transient sharing-violation + # PermissionErrors so a racing rewriter doesn't turn a hit + # into a spurious propagated error. + st, data = _stat_and_read_with_sharing_retry(path) + except FileNotFoundError: + raise KeyError(key) from None + # Bump atime to "now" so eviction (which sorts by st_atime) treats + # this read as the entry's most recent use. Best-effort: filesystems + # mounted ``noatime`` or with restrictive ACLs may refuse, in which + # case the cap still bounds size but eviction degrades toward FIFO + # rather than true LRU. + _touch_atime(path, st) + return data + + def __setitem__(self, key: object, value: bytes | bytearray | memoryview | ObjectCode) -> None: + data = _extract_bytes(value) + target = self._path_for_key(key) + target.parent.mkdir(parents=True, exist_ok=True) + # Re-create ``tmp/`` if something deleted it after ``__init__`` + # (operators clearing the cache by hand, ``rm -rf cache_dir/tmp``, + # another process's overzealous wipe). Cheap and idempotent; + # without it, every subsequent write would crash with + # FileNotFoundError even though we could trivially recover. + self._tmp.mkdir(parents=True, exist_ok=True) + + # Stat the existing entry (if any) BEFORE the replace so we can + # update the tracker by the net delta. A racing writer that lands + # an ``os.replace`` between this stat and our own makes ``old_size`` + # slightly off; the next ``_enforce_size_cap`` reconciles by + # re-scanning. Skipped when ``max_size_bytes is None`` (no tracker). + old_size = 0 + if self._max_size_bytes is not None: + try: + old_size = target.stat().st_size + except FileNotFoundError: + old_size = 0 + + fd, tmp_name = tempfile.mkstemp(prefix="entry-", dir=self._tmp) + tmp_path = Path(tmp_name) + try: + with os.fdopen(fd, "wb") as fh: + fh.write(data) + fh.flush() + os.fsync(fh.fileno()) + # Retry os.replace under Windows sharing/lock violations; only + # give up (and drop the cache write) after a bounded backoff, so + # transient contention is not turned into a silent miss. + # Non-sharing PermissionErrors and all POSIX PermissionErrors + # propagate immediately (real config problem). + if not _replace_with_sharing_retry(tmp_path, target): + with contextlib.suppress(FileNotFoundError): + tmp_path.unlink() + return + except BaseException: + with contextlib.suppress(FileNotFoundError): + tmp_path.unlink() + raise + + if self._max_size_bytes is None: + return + + # O(1) tracker update. Only run the scan-heavy ``_enforce_size_cap`` + # when this write actually pushes the running total above the cap. + new_size = len(data) + with self._size_lock: + self._tracked_size_bytes += new_size - old_size + over_cap = self._tracked_size_bytes > self._max_size_bytes + if over_cap: + self._enforce_size_cap() + + def __delitem__(self, key: object) -> None: + path = self._path_for_key(key) + # Stat before unlink so we can decrement the tracker by the actual + # on-disk size. Best-effort: if the file vanishes between stat and + # unlink (concurrent eviction), we treat the delete as a miss -- + # matching the behaviour callers expect (KeyError) and leaving the + # tracker untouched (the racing eviction already accounted for it). + size = 0 + if self._max_size_bytes is not None: + try: + size = path.stat().st_size + except FileNotFoundError: + raise KeyError(key) from None + try: + _unlink_with_sharing_retry(path) + except FileNotFoundError: + raise KeyError(key) from None + if self._max_size_bytes is not None: + with self._size_lock: + # Clamp at zero. A racing ``_enforce_size_cap`` can re-seed the + # tracker between our stat and our subtract; if its scan ran + # AFTER we unlinked, its reseed value didn't include ``size``, + # so subtracting ``size`` again here would undercount reality + # by ``size``. Repeated under contention, an unclamped subtract + # walks the tracker negative -- and once negative, the + # ``tracker > cap`` check that gates ``_enforce_size_cap`` + # never fires, so eviction dies silently and there is no + # self-healing path (the only reseed point is the function + # that no longer runs). Clamping leaves us at worst + # undercounting (the next reseed corrects it) instead of + # entering the permanently-broken negative state. + self._tracked_size_bytes = max(0, self._tracked_size_bytes - size) + + def __len__(self) -> int: + """Return the number of files currently in ``entries/``. + + This is a count of on-disk files, not of keys reachable through + ``make_program_cache_key``. After a ``_KEY_SCHEMA_VERSION`` bump + old entries become unreachable by lookup but remain on disk + until eviction reaps them; ``__len__`` keeps counting them + until then. The same is true for entries written by callers + using arbitrary user keys -- the backend has no way to tell a + live entry from an orphan without knowing the caller's keying + scheme. + """ + # ``_iter_entry_paths`` already filters with ``entry.is_file()``, + # so don't stat each path a second time here. + return sum(1 for _ in self._iter_entry_paths()) + + def clear(self) -> None: + # Snapshot stat alongside path so we can refuse to unlink an entry + # that was concurrently replaced by another process between the + # snapshot scan and the unlink. Same stat-guard contract as + # ``_prune_if_stat_unchanged`` and ``_enforce_size_cap``. + snapshot = [] + for path in self._iter_entry_paths(): + try: + snapshot.append((path, path.stat())) + except FileNotFoundError: + continue + for path, st_before in snapshot: + _prune_if_stat_unchanged(path, st_before) + # Sweep ONLY stale temp files. Deleting a young temp would race with + # another process between ``mkstemp`` and ``os.replace`` and turn its + # write into ``FileNotFoundError`` instead of a successful commit. + self._sweep_stale_tmp_files() + # Remove empty subdirs (best-effort; concurrent writers may re-create). + if self._entries.exists(): + for sub in sorted(self._entries.iterdir(), reverse=True): + if sub.is_dir(): + with contextlib.suppress(OSError): + sub.rmdir() + # The directory is now (almost) empty -- but a concurrent writer may + # have landed a fresh entry between the snapshot and the unlink, and + # young temp files were intentionally preserved. Re-derive the + # tracker from the post-clear state instead of zeroing blindly. + if self._max_size_bytes is not None: + actual = self._compute_total_size() + with self._size_lock: + self._tracked_size_bytes = actual + + # -- internals ----------------------------------------------------------- + + def _iter_entry_paths(self) -> Iterable[Path]: + # ``os.scandir`` returns ``DirEntry`` objects whose ``is_dir`` / + # ``is_file`` methods consult the cached dirent type from the + # ``readdir`` result on filesystems that report it (ext4, NTFS, ...), + # avoiding a per-entry ``stat`` syscall. ``Path.iterdir`` also wraps + # ``scandir`` but discards the cached type, forcing a separate + # ``stat`` for every ``Path.is_dir`` / ``Path.is_file``. The ``with`` + # blocks release the underlying directory handle deterministically + # when the consumer stops early -- otherwise a leaked handle blocks + # deletes/renames on Windows until GC. + try: + with os.scandir(self._entries) as outer: + for sub in outer: + if not sub.is_dir(follow_symlinks=False): + continue + try: + with os.scandir(sub.path) as inner: + yield from (Path(entry.path) for entry in inner if entry.is_file(follow_symlinks=False)) + except FileNotFoundError: + continue + except FileNotFoundError: + return + + def _compute_total_size(self) -> int: + """Walk ``entries/`` + ``tmp/`` and return the on-disk byte total. + + Used to seed the tracker at open time and to refresh it after every + eviction pass. Best-effort: files that vanish under us during the + walk (concurrent eviction by this or another process) are skipped. + Tracked total may briefly differ from this scan's result under + cross-process contention; the next eviction will reconcile. + """ + total = 0 + for path in self._iter_entry_paths(): + try: + total += path.stat().st_size + except FileNotFoundError: + continue + return total + self._sum_tmp_sizes() + + def _iter_tmp_entries(self) -> Iterable[os.DirEntry]: + # Mirror ``_iter_entry_paths``: scandir + cached d_type for the + # file/dir filter + deterministic handle close on early exit. + # Yields ``DirEntry`` (not Path) so callers can use ``entry.stat`` + # / ``entry.path`` directly without an extra wrap. + try: + with os.scandir(self._tmp) as it: + yield from (entry for entry in it if entry.is_file(follow_symlinks=False)) + except FileNotFoundError: + return + + def _sum_tmp_sizes(self) -> int: + """Sum sizes of every file in ``tmp/``, skipping vanished entries. + + Both ``_compute_total_size`` (open-time seed) and + ``_enforce_size_cap`` (eviction reconciliation) need this -- + temp files occupy disk too, so undercounting them would let + bursts of in-flight writes silently exceed ``max_size_bytes``. + """ + total = 0 + for entry in self._iter_tmp_entries(): + try: + total += entry.stat(follow_symlinks=False).st_size + except FileNotFoundError: + continue + return total + + def _sweep_stale_tmp_files(self) -> None: + """Remove temp files left behind by crashed writers. + + Age threshold is conservative (``_TMP_STALE_AGE_SECONDS``) so an + in-flight write from another process is not interrupted. Best + effort: a missing file or a permission failure is ignored. + """ + cutoff = time.time() - _TMP_STALE_AGE_SECONDS + for entry in self._iter_tmp_entries(): + try: + if entry.stat(follow_symlinks=False).st_mtime < cutoff: + os.unlink(entry.path) + except (FileNotFoundError, PermissionError): + continue + + def _enforce_size_cap(self) -> None: + if self._max_size_bytes is None: + return + # Sweep stale temp files first so a long-dead writer's leftovers + # don't drag the apparent size up and force needless eviction. + self._sweep_stale_tmp_files() + entries = [] + total = 0 + # Count both committed entries AND surviving temp files: temp files + # occupy disk too, even if they're young. Without this the soft cap + # silently undercounts in-flight writes. + # + # Trade-off under burst concurrency: many young temp files (each + # below the stale-sweep threshold) can push ``total`` above + # ``max_size_bytes`` with only committed entries left to evict. + # That can over-evict committed entries during the burst; once + # the burst subsides and the temps land via ``os.replace`` (or + # are reaped by a later sweep), the cap re-stabilises. This is + # consistent with the documented soft-cap contract -- callers + # that need a hard bound should leave the cap None and prune + # externally. + for path in self._iter_entry_paths(): + try: + st = path.stat() + except FileNotFoundError: + continue + # Carry the full stat so eviction can guard against a concurrent + # os.replace that swapped a fresh entry into this path between + # snapshot and unlink. Eviction below sorts by ``st_atime`` so + # entries that callers actually read recently survive + # write-only churn (true LRU instead of FIFO). + entries.append((st.st_atime, st.st_size, path, st)) + total += st.st_size + total += self._sum_tmp_sizes() + if total <= self._max_size_bytes: + # Re-seed the tracker from the scan: catches drift from + # cross-process writers/deleters that the per-write delta + # accounting wouldn't have observed. Reaching here means the + # tracker was over-cap but the disk truth is under-cap, so + # this assignment is the cheapest reconciliation point we get. + with self._size_lock: + self._tracked_size_bytes = total + return + entries.sort(key=lambda e: e[0]) # oldest atime first + for _atime, size, path, st_before in entries: + if total <= self._max_size_bytes: + break + # _prune_if_stat_unchanged refuses if a writer replaced the file + # between snapshot and now, so eviction can't silently delete a + # freshly-committed entry from another process. + try: + stat_now = path.stat() + except FileNotFoundError: + total -= size + continue + if _stat_key(stat_now) != _stat_key(st_before): + # File was replaced -- don't unlink, but update ``total`` to + # reflect the replacement's actual size or the cap check + # below could declare us done while still over the limit. + total += stat_now.st_size - size + continue + # Tolerate Windows sharing violations during eviction: another + # process may briefly hold the file open for a read. Skip this + # entry; a later eviction pass will retry. Same outcome as if + # the stat-guard above had triggered. Other PermissionErrors + # (POSIX ACL, Windows non-sharing winerrors) are real config + # problems -- surface them rather than silently exceed the cap. + try: + _unlink_with_sharing_retry(path) + total -= size + except FileNotFoundError: + pass + except PermissionError as exc: + if not _is_windows_sharing_violation(exc): + raise + # Reconcile: after the eviction pass, ``total`` reflects what we + # believe the disk now holds. Re-seed the tracker so the next write + # accumulates from a fresh baseline. + with self._size_lock: + self._tracked_size_bytes = total diff --git a/cuda_core/cuda/core/utils/_program_cache/_in_memory.py b/cuda_core/cuda/core/utils/_program_cache/_in_memory.py new file mode 100644 index 00000000000..6a25e2afbc6 --- /dev/null +++ b/cuda_core/cuda/core/utils/_program_cache/_in_memory.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""In-memory bytes-in / bytes-out program cache.""" + +from __future__ import annotations + +import collections +import threading + +from cuda.core._module import ObjectCode + +from ._abc import ProgramCacheResource, _as_key_bytes, _extract_bytes + + +class InMemoryProgramCache(ProgramCacheResource): + """In-memory program cache with LRU eviction. + + Suitable for single-process workflows that want to avoid disk I/O -- + a typical application compiles its kernels once per process and + looks them up many times. Entries live only for the lifetime of + the process; use :class:`FileStreamProgramCache` when the cache + should persist across runs. + + Like :class:`FileStreamProgramCache`, this backend is bytes-in / + bytes-out: ``__setitem__`` accepts ``bytes``, ``bytearray``, + ``memoryview``, or any :class:`~cuda.core.ObjectCode` (path-backed + too -- the file is read at write time so the cached entry holds the + binary content, not a path). ``__getitem__`` returns ``bytes``. + + Parameters + ---------- + max_size_bytes: + Optional cap on the sum of stored payload sizes. When exceeded, + LRU eviction runs until the total fits. ``None`` means + unbounded. The size-only bound mirrors + :class:`FileStreamProgramCache`. + + Notes + ----- + Recency is updated on :meth:`__getitem__`; ``get`` is the + recommended lookup since the cache deliberately omits + ``__contains__`` (the ``if key in cache: ...`` idiom is racy + across processes; see :class:`ProgramCacheResource`). + + Thread safety: a :class:`threading.RLock` serialises every method, + so the cache can be shared across threads without external + locking. + """ + + def __init__( + self, + *, + max_size_bytes: int | None = None, + ) -> None: + if max_size_bytes is not None and max_size_bytes <= 0: + raise ValueError("max_size_bytes must be positive or None (0 would evict every write)") + self._max_size_bytes = max_size_bytes + # Key insertion order encodes LRU order: oldest first, newest last. + # Each value is ``(payload_bytes, payload_size)``; caching the size + # avoids recomputing ``len(data)`` on every eviction pass. + self._entries: collections.OrderedDict[bytes, tuple[bytes, int]] = collections.OrderedDict() + self._total_bytes = 0 + # Reentrant so helper methods that also take the lock can nest + # without deadlocking. + self._lock = threading.RLock() + + def __getitem__(self, key: object) -> bytes: + k = _as_key_bytes(key) + with self._lock: + try: + data, _size = self._entries[k] + except KeyError: + raise KeyError(key) from None + # Touch LRU: a real read promotes the entry to "most recent" + # so eviction prefers genuinely cold entries. + self._entries.move_to_end(k) + return data + + def __setitem__(self, key: object, value: bytes | bytearray | memoryview | ObjectCode) -> None: + data = _extract_bytes(value) + size = len(data) + k = _as_key_bytes(key) + with self._lock: + existing = self._entries.pop(k, None) + if existing is not None: + self._total_bytes -= existing[1] + self._entries[k] = (data, size) + self._total_bytes += size + self._evict_to_caps() + + def __delitem__(self, key: object) -> None: + k = _as_key_bytes(key) + with self._lock: + try: + _data, size = self._entries.pop(k) + except KeyError: + raise KeyError(key) from None + self._total_bytes -= size + + def __len__(self) -> int: + with self._lock: + return len(self._entries) + + def clear(self) -> None: + with self._lock: + self._entries.clear() + self._total_bytes = 0 + + # -- eviction ------------------------------------------------------------ + + def _evict_to_caps(self) -> None: + """Evict oldest entries until the size cap is satisfied. + + Called from ``__setitem__`` after an insert/update. Pops from + the front of the OrderedDict (oldest first). If the + just-inserted entry on its own exceeds ``max_size_bytes``, the + loop will evict it too -- mirroring + :class:`FileStreamProgramCache` (a write that cannot fit does + not survive its own size-cap pass). + """ + if self._max_size_bytes is None: + return + while self._entries and self._total_bytes > self._max_size_bytes: + _k, (_data, size) = self._entries.popitem(last=False) + self._total_bytes -= size diff --git a/cuda_core/cuda/core/utils/_program_cache/_keys.py b/cuda_core/cuda/core/utils/_program_cache/_keys.py new file mode 100644 index 00000000000..dda07039e32 --- /dev/null +++ b/cuda_core/cuda/core/utils/_program_cache/_keys.py @@ -0,0 +1,813 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Cache-key construction. + +A backend-strategy hierarchy (:class:`_KeyBackend`) owns the per-code-type +guard / fingerprint / version-probe logic; :func:`make_program_cache_key` +dispatches to the right backend and assembles the digest. +""" + +from __future__ import annotations + +import abc +import collections.abc +import hashlib +from typing import Sequence + +# Mutual-dependency contract: this module imports ProgramOptions from +# cuda.core._program at module level, and cuda.core._program imports +# ProgramCacheResource / make_program_cache_key from cuda.core.utils +# only via deferred imports inside ``Program.compile``. Adding a +# top-level ``from cuda.core.utils import ...`` to _program.pyx would +# turn this into a real import cycle -- keep the import in _program.pyx +# deferred (or import the symbols from the leaf submodule directly). +from cuda.core._program import ProgramOptions +from cuda.core._utils.cuda_utils import ( + driver as _driver, +) +from cuda.core._utils.cuda_utils import ( + handle_return as _handle_return, +) +from cuda.core._utils.cuda_utils import ( + nvrtc as _nvrtc, +) + +# Bump when the key schema changes in a way that invalidates existing caches. +_KEY_SCHEMA_VERSION = 1 + +_VALID_CODE_TYPES = frozenset({"c++", "ptx", "nvvm"}) +_VALID_TARGET_TYPES = frozenset({"ptx", "cubin", "ltoir"}) + +# code_type -> allowed target_type set, mirroring Program.compile's +# SUPPORTED_TARGETS matrix in _program.pyx. +_SUPPORTED_TARGETS_BY_CODE_TYPE = { + "c++": frozenset({"ptx", "cubin", "ltoir"}), + "ptx": frozenset({"cubin", "ptx"}), + "nvvm": frozenset({"ptx", "ltoir"}), +} + + +# Map each ProgramOptions field that reaches the Linker via +# _translate_program_options (see cuda_core/cuda/core/_program.pyx) to the +# gate the Linker uses to turn it into a flag (see +# ``_prepare_nvjitlink_options`` and ``_prepare_driver_options`` in +# _linker.pyx). All other fields on ProgramOptions are NVRTC-only and must +# NOT perturb a PTX cache key: a PTX compile with a shared ProgramOptions +# that happens to set include_path/pch/frandom_seed would otherwise miss the +# cache unnecessarily. Collapsing inputs through these gates means +# semantically-equivalent configurations (``debug=False`` vs ``None``, +# ``time=True`` vs ``time="path"``) hash to the same cache key instead of +# forcing spurious misses. Single source of truth: every reader iterates +# this dict, so adding a field here is enough -- there is no parallel +# field-name list to keep in sync. +def _gate_presence(v): + return v is not None + + +def _gate_truthy(v): + return bool(v) + + +def _gate_is_true(v): + return v is True + + +def _gate_tristate_bool(v): + return None if v is None else bool(v) + + +def _gate_identity(v): + return v + + +def _gate_ptxas_options(v): + # ``_prepare_nvjitlink_options`` emits one ``-Xptxas=`` per element, and + # treats ``str`` as a single-element sequence. Canonicalize to a tuple so + # ``"-v"`` / ``["-v"]`` / ``("-v",)`` all hash the same. An empty sequence + # emits no flags, so collapse it to ``None`` too. + # + # Order is preserved on purpose: ptxas accepts ordering-sensitive flag + # pairs (e.g. ``-O2`` after ``-O3`` lowers the active level), so + # ``["-v", "-O2"]`` and ``["-O2", "-v"]`` are not guaranteed to produce + # identical bytes. We accept the spurious miss when callers reorder + # flags; treating order as semantic keeps the cache safe in the + # ordering-sensitive case. + if v is None: + return None + if isinstance(v, str): + return ("-Xptxas=" + v,) + if isinstance(v, collections.abc.Sequence): + if len(v) == 0: + return None + return tuple(f"-Xptxas={s}" for s in v) + return v + + +_LINKER_FIELD_GATES = { + "name": _gate_identity, + "arch": _gate_identity, + "max_register_count": _gate_identity, + "time": _gate_presence, # linker emits ``-time`` iff value is not None + "link_time_optimization": _gate_truthy, + "debug": _gate_truthy, + "lineinfo": _gate_truthy, + "ftz": _gate_tristate_bool, + "prec_div": _gate_tristate_bool, + "prec_sqrt": _gate_tristate_bool, + "fma": _gate_tristate_bool, + "split_compile": _gate_identity, + "ptxas_options": _gate_ptxas_options, + "no_cache": _gate_is_true, +} + + +# LinkerOptions fields the ``cuLink`` driver backend silently ignores +# (emits only a DeprecationWarning; no actual flag reaches the compiler). +# When the driver backend is active, collapse them to a single sentinel in +# the fingerprint so nvJitLink<->driver parity of ``ObjectCode`` doesn't +# cause cache misses from otherwise-equivalent configurations. +_DRIVER_IGNORED_LINKER_FIELDS = frozenset({"ftz", "prec_div", "prec_sqrt", "fma"}) + + +def _linker_option_fingerprint(options: ProgramOptions, *, use_driver_linker: bool | None) -> list[bytes]: + """Backend-aware fingerprint of ProgramOptions fields consumed by the Linker. + + Each field passes through the gate the Linker itself uses so equivalent + inputs (e.g. ``debug=False`` / ``None``) hash to the same bytes. When + the driver (cuLink) linker backend is in use, fields it silently + ignores collapse to one sentinel so those options don't perturb the + key on driver-backed hosts either. ``use_driver_linker=None`` means we + couldn't probe the backend; we don't collapse driver-ignored fields in + that case, to stay conservative. + """ + parts = [] + driver_ignored = use_driver_linker is True + for name, gate in _LINKER_FIELD_GATES.items(): + if driver_ignored and name in _DRIVER_IGNORED_LINKER_FIELDS: + parts.append(f"{name}=".encode()) + continue + gated = gate(getattr(options, name, None)) + parts.append(f"{name}={gated!r}".encode()) + return parts + + +# ProgramOptions fields that map to LinkerOptions fields the cuLink (driver) +# backend rejects outright (see _prepare_driver_options in _linker.pyx). +# ``split_compile_extended`` exists on LinkerOptions but is not exposed via +# ProgramOptions / _translate_program_options, so it cannot reach the driver +# linker from the cache path and is omitted here. +_DRIVER_LINKER_UNSUPPORTED_FIELDS = ("time", "ptxas_options", "split_compile") + + +def _driver_version() -> int: + return int(_handle_return(_driver.cuDriverGetVersion())) + + +def _nvrtc_version() -> tuple[int, int]: + major, minor = _handle_return(_nvrtc.nvrtcVersion()) + return int(major), int(minor) + + +def _linker_backend_and_version(use_driver: bool) -> tuple[str, str]: + """Return ``(backend, version)`` for the linker used on PTX inputs. + + ``use_driver`` is the result of ``_decide_nvjitlink_or_driver()`` and + must be passed in so a single ``make_program_cache_key`` call shares + one probe across :meth:`_LinkerBackend.validate`, + :meth:`option_fingerprint`, and :meth:`hash_version_probe` (otherwise + a transient probe flap could write inconsistent fields into the same + key). + + Raises any underlying probe exception. ``make_program_cache_key`` catches + and mixes the exception's class name into the digest, so the same probe + failure produces the same key across processes -- the cache stays + persistent in broken environments, while never sharing a key with a + working probe (``_probe_failed`` label vs. ``driver``/``nvrtc``/...). + + nvJitLink version lookup goes through ``sys.modules`` first so we hit the + same module ``_decide_nvjitlink_or_driver()`` already loaded. That keeps + fingerprinting aligned with whichever ``cuda.bindings.nvjitlink`` import + path the linker actually uses. + """ + import sys + + if use_driver: + return ("driver", str(_driver_version())) + nvjitlink = sys.modules.get("cuda.bindings.nvjitlink") + if nvjitlink is None: + from cuda.bindings import nvjitlink + + return ("nvJitLink", str(nvjitlink.version())) + + +def _nvvm_fingerprint() -> str: + """Stable identifier for the loaded NVVM toolchain. + + Combines the libNVVM library version (``module.version()``) with the IR + version reported by ``module.ir_version()``. The library version is the + primary invalidation lever: a libNVVM patch upgrade can change codegen + while keeping the same IR major/minor, so keying only on the IR pair + would silently reuse stale entries. Paired with cuda-core, the IR pair + adds defence in depth without making the key any less stable. + + Both calls go through ``_get_nvvm_module()`` so this fingerprint follows + the same availability / cuda-bindings-version gate that real NVVM + compilation does -- if NVVM is unusable at compile time, the probe + fails the same way and ``_probe`` mixes the failure label into the key. + """ + from cuda.core._program import _get_nvvm_module + + module = _get_nvvm_module() + lib_major, lib_minor = module.version() + major, minor, debug_major, debug_minor = module.ir_version() + return f"lib={lib_major}.{lib_minor};ir={major}.{minor}.{debug_major}.{debug_minor}" + + +# ProgramOptions fields that reference external files whose *contents* the +# cache key cannot observe without reading the filesystem. Callers that set +# any of these must supply an ``extra_digest`` covering the dependency surface +# (e.g. a hash over all reachable headers / PCH bytes). +_EXTERNAL_CONTENT_OPTIONS = ( + "include_path", + "pre_include", + "pch", + "use_pch", + "pch_dir", +) + +# ProgramOptions fields whose compilation effect is not captured in the +# returned ``ObjectCode`` -- they produce a filesystem artifact as a side +# effect. A cache hit skips compilation, so that artifact would never be +# written. Reject these outright: the persistent cache is for pure ObjectCode +# reuse, not for replaying compile-time side effects. +# * create_pch -- writes a PCH file (NVRTC). +# * time -- writes NVRTC timing info to a file. +# * fdevice_time_trace -- writes a device-compilation time trace file (NVRTC). +# These are all NVRTC-specific; the Linker's ``-time`` logs to the info log +# (not a file) and NVVM explicitly rejects all three at compile time. The +# side-effect guard is therefore gated on ``backend == "nvrtc"`` below. +_SIDE_EFFECT_OPTIONS = ("create_pch", "time", "fdevice_time_trace") + + +# ProgramOptions fields gated by plain truthiness in ``_program.pyx`` (the +# compiler writes the flag only when the value is truthy). +_BOOLEAN_OPTION_FIELDS = frozenset({"pch"}) + +# Fields whose compiler emission requires ``isinstance(value, str)`` or a +# non-empty sequence; anything else (``False``, ``int``, ``None``, ``[]``) +# is silently ignored at compile time. +_STR_OR_SEQUENCE_OPTION_FIELDS = frozenset({"include_path", "pre_include"}) + + +def _option_is_set(options: ProgramOptions, name: str) -> bool: + """Match how ``_program.pyx`` gates option emission, per field shape. + + - Boolean flags (``pch``): truthy only. + - str-or-sequence fields (``include_path``, ``pre_include``): ``str`` + (including empty) or a non-empty ``collections.abc.Sequence`` (list, + tuple, range, user subclass, ...); everything else (``False``, ``int``, + empty sequence, ``None``) is ignored by the compiler and must not + trigger a cache-time guard. + - Path/string-shaped fields (``create_pch``, ``time``, + ``fdevice_time_trace``, ``use_pch``, ``pch_dir``): ``is not None`` -- + the compiler emits ``--flag=`` for any non-None value, so + ``False`` / ``""`` / ``0`` must still count as set. + """ + value = getattr(options, name, None) + if value is None: + return False + if name in _BOOLEAN_OPTION_FIELDS: + return bool(value) + if name in _STR_OR_SEQUENCE_OPTION_FIELDS: + # Mirror ``_prepare_nvrtc_options_impl``: it checks ``isinstance(v, str)`` + # first, then ``is_sequence(v)`` (which is ``isinstance(v, Sequence)``). + # We therefore accept any ``collections.abc.Sequence`` (range, deque, + # user subclass, etc.), not just list/tuple. + if isinstance(value, str): + return True + if isinstance(value, collections.abc.Sequence): + return len(value) > 0 + return False + return True + + +def _hash_probe_failure(update, label: str, exc: BaseException) -> None: + """Mix a probe failure into the digest under a stable, content-free label. + + Hashing only the exception's CLASS NAME (not its message) keeps the + digest stable across repeated calls within one process (e.g. NVVM's + loader reports different messages on first vs. cached-failure attempts) + AND across processes that hit the same failure mode. The + ``_probe_failed`` label differs from every backend's success label, so a + broken environment never collides with a working one -- the cache + "fails closed" between broken and working environments while staying + persistent within either. + """ + update(f"{label}_probe_failed", type(exc).__name__.encode()) + + +class _KeyBackend(abc.ABC): + """Strategy for deriving the cache key for one ``Program`` ``code_type``. + + Each subclass owns the backend-specific guard logic, code coercion, + option fingerprinting, name-expression handling, version probing, and + extra-payload hashing. The orchestrator :func:`make_program_cache_key` + validates the code_type / target_type pair, dispatches to the right + backend, and assembles the digest. + """ + + @abc.abstractmethod + def validate(self, options: ProgramOptions, target_type: str, extra_digest: bytes | None) -> None: + """Reject inputs the cache cannot key safely. + + Raises ``ValueError`` for options that have compile-time side + effects, options that pull in external file content the cache + can't observe, or any other backend-specific invariants. + """ + + def encode_code(self, code: object, code_type: str) -> bytes: + """Coerce ``code`` to bytes. Default rejects bytes-like input + (only NVVM accepts it; ``Program()`` does the same).""" + if isinstance(code, str): + return code.encode("utf-8") + if isinstance(code, (bytes, bytearray)): + raise TypeError( + f"code must be str for code_type={code_type!r}; bytes/bytearray are only accepted for code_type='nvvm'." + ) + raise TypeError(f"code must be str or bytes, got {type(code).__name__}") + + @abc.abstractmethod + def option_fingerprint(self, options: ProgramOptions, target_type: str) -> list[bytes]: + """Fingerprint of the ``ProgramOptions`` fields that reach the compiler.""" + + def encode_name_expressions(self, name_expressions: Sequence) -> tuple[bytes, ...] | None: # noqa: ARG002 + """Sorted, type-tagged name expressions, or ``None`` if the + backend does not consume them. + + ``None`` means the orchestrator emits no ``names_count`` / + ``name`` entries at all (a backend that ignores + ``name_expressions`` should never have them perturb its key). An + empty tuple means the backend supports them but the caller + passed none -- the orchestrator still emits ``names_count=0`` so + the schema is stable across "absent" and "empty". + """ + return None + + @abc.abstractmethod + def hash_version_probe(self, update) -> None: + """Mix the runtime/compiler version probe into the digest via + ``update(label, payload)``. On probe failure, mix + ``_hash_probe_failure(update, "