diff --git a/.github/actions/griffe-api-check/action.yml b/.github/actions/griffe-api-check/action.yml new file mode 100644 index 00000000000..05363126800 --- /dev/null +++ b/.github/actions/griffe-api-check/action.yml @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: griffe API check + +description: >- + Check a package's public API (as defined by `__all__`) for changes using + griffe, comparing the current code against both the PR's merge-base and the + package's latest matching release tag. This action itself always succeeds; + it reports findings via the `has-findings` output and a job summary so the + caller can decide whether/how to fail its own job. + +inputs: + package-name: + description: "Importable package name to check, e.g. cuda.core" + required: true + package-dir: + description: "Directory to search for the package sources, e.g. cuda_core" + required: true + merge-base: + description: >- + Git ref/sha to compare the current code against, typically the PR's + merge-base with its target branch. + required: true + tag-pattern: + description: >- + Glob pattern (as passed to `git describe --match`) matching the + package's release tags, e.g. cuda-core-v* + required: true + +outputs: + has-findings: + description: "true if griffe reported API differences in either comparison" + value: ${{ steps.summarize.outputs.has-findings }} + +runs: + using: composite + steps: + - name: Install uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: false + + - name: Check API vs. PR base branch + id: vs-base + shell: bash --noprofile --norc -euo pipefail {0} + continue-on-error: true + env: + PACKAGE_NAME: ${{ inputs.package-name }} + PACKAGE_DIR: ${{ inputs.package-dir }} + MERGE_BASE: ${{ inputs.merge-base }} + run: | + # griffe writes its `::warning::`/`::error::` annotations to stderr, + # so stdout and stderr must both be captured for the summary file + # below to contain anything. + uvx griffe check "$PACKAGE_NAME" \ + --search "$PACKAGE_DIR" \ + --find-stubs-packages \ + --against "$MERGE_BASE" \ + --format github \ + 2>&1 | tee griffe-vs-base.txt + + - name: Find latest release tag + id: latest-tag + shell: bash --noprofile --norc -euo pipefail {0} + env: + TAG_PATTERN: ${{ inputs.tag-pattern }} + run: | + # No matching tag (e.g. a brand-new package) is not an error: the + # release-compatibility comparison is simply skipped below. + tag="$(git describe --tags --match "$TAG_PATTERN" --abbrev=0 2>/dev/null || true)" + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + + - name: Check API vs. latest release tag + id: vs-tag + if: ${{ steps.latest-tag.outputs.tag != '' }} + shell: bash --noprofile --norc -euo pipefail {0} + continue-on-error: true + env: + PACKAGE_NAME: ${{ inputs.package-name }} + PACKAGE_DIR: ${{ inputs.package-dir }} + LATEST_TAG: ${{ steps.latest-tag.outputs.tag }} + run: | + # See the comment in the "Check API vs. PR base branch" step: griffe's + # annotations go to stderr, so redirect it into the captured stream. + uvx griffe check "$PACKAGE_NAME" \ + --search "$PACKAGE_DIR" \ + --find-stubs-packages \ + --against "$LATEST_TAG" \ + --format github \ + 2>&1 | tee griffe-vs-tag.txt + + - name: Summarize results + id: summarize + if: always() + shell: bash --noprofile --norc -euo pipefail {0} + env: + PACKAGE_NAME: ${{ inputs.package-name }} + VS_BASE_OUTCOME: ${{ steps.vs-base.outcome }} + VS_TAG_OUTCOME: ${{ steps.vs-tag.outcome }} + LATEST_TAG: ${{ steps.latest-tag.outputs.tag }} + run: | + has_findings=false + + { + echo "### API check: \`${PACKAGE_NAME}\`" + echo "" + echo "_This check is informational; it does not block the PR. Review findings and confirm any API changes are intentional._" + echo "" + + if [[ "$VS_BASE_OUTCOME" == "failure" ]]; then + has_findings=true + echo "#### :warning: Changes vs. PR base branch" + echo '```' + cat griffe-vs-base.txt + echo '```' + else + echo "#### :white_check_mark: No changes vs. PR base branch" + fi + echo "" + + if [[ -z "$LATEST_TAG" ]]; then + echo "#### No release tag matched; skipped release-compatibility check." + elif [[ "$VS_TAG_OUTCOME" == "failure" ]]; then + has_findings=true + echo "#### :warning: Changes vs. latest release (\`${LATEST_TAG}\`)" + echo '```' + cat griffe-vs-tag.txt + echo '```' + else + echo "#### :white_check_mark: No changes vs. latest release (\`${LATEST_TAG}\`)" + fi + } >> "$GITHUB_STEP_SUMMARY" + + echo "has-findings=${has_findings}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe43b52d01a..1d31e97a93b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,6 +103,7 @@ jobs: test_bindings: ${{ steps.compose.outputs.test_bindings }} test_core: ${{ steps.compose.outputs.test_core }} test_pathfinder: ${{ steps.compose.outputs.test_pathfinder }} + pr_merge_base: ${{ steps.filter.outputs.merge_base }} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -156,6 +157,7 @@ jobs: 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$)')" + echo "merge_base=${base}" } >> "$GITHUB_OUTPUT" - name: Compose gating outputs @@ -228,6 +230,50 @@ jobs: echo "test_pathfinder=${test_pathfinder}" } >> "$GITHUB_OUTPUT" + # Compares the public API of cuda_core (as defined by `__all__` in its + # modules/subpackages) against the PR's base branch and the latest + # cuda-core release tag, using griffe (see .github/actions/griffe-api-check). + # Findings fail this job (shows red in the UI), but it never blocks merging + # the PR: it's intentionally excluded from `checks:`'s `needs`, so it isn't + # part of the required aggregator status. + api-check-core: + name: API check (cuda_core) + if: >- + ${{ startsWith(github.ref_name, 'pull-request/') && + !fromJSON(needs.should-skip.outputs.skip) && + fromJSON(needs.detect-changes.outputs.core) }} + runs-on: ubuntu-latest + needs: + - should-skip + - detect-changes + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # griffe checks out the "against" ref into a worktree internally, + # and finding the latest release tag needs the full tag history, so + # the full commit graph (though not historical blobs) must be + # available locally. + fetch-depth: 0 + filter: blob:none + + - name: Check cuda_core public API + id: griffe + uses: ./.github/actions/griffe-api-check + with: + package-name: cuda.core + package-dir: cuda_core + merge-base: ${{ needs.detect-changes.outputs.pr_merge_base }} + tag-pattern: "cuda-core-v*" + + - name: Fail this job if API changes were found + if: ${{ steps.griffe.outputs.has-findings == 'true' }} + run: | + echo "::error::cuda_core API check found differences; see the job summary for details." + exit 1 + # 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 diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index dc6fefdffea..1f661b65e06 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -68,6 +68,48 @@ class _PatchedProperty(metaclass=_PatchedPropMeta): del _patch_rlcompleter_for_cython_properties +__all__ = [ + "LEGACY_DEFAULT_STREAM", + "PER_THREAD_DEFAULT_STREAM", + "Buffer", + "Context", + "ContextOptions", + "Device", + "DeviceMemoryResource", + "DeviceMemoryResourceOptions", + "DeviceResources", + "Event", + "EventOptions", + "GraphMemoryResource", + "GraphicsResource", + "Host", + "Kernel", + "LaunchConfig", + "LegacyPinnedMemoryResource", + "Linker", + "LinkerOptions", + "ManagedBuffer", + "ManagedMemoryResource", + "ManagedMemoryResourceOptions", + "MemoryResource", + "ObjectCode", + "PinnedMemoryResource", + "PinnedMemoryResourceOptions", + "Program", + "ProgramOptions", + "SMResource", + "SMResourceOptions", + "Stream", + "StreamOptions", + "TensorMapDescriptor", + "TensorMapDescriptorOptions", + "VirtualMemoryResource", + "VirtualMemoryResourceOptions", + "WorkqueueResource", + "WorkqueueResourceOptions", + "launch", +] + from cuda.core import checkpoint, system, utils from cuda.core._context import Context, ContextOptions from cuda.core._device import Device diff --git a/cuda_core/cuda/core/graph/__init__.py b/cuda_core/cuda/core/graph/__init__.py index e1091114368..79db93aefaf 100644 --- a/cuda_core/cuda/core/graph/__init__.py +++ b/cuda_core/cuda/core/graph/__init__.py @@ -3,6 +3,12 @@ # SPDX-License-Identifier: Apache-2.0 from ._graph_builder import * +from ._graph_builder import __all__ as _graph_builder_all from ._graph_definition import * +from ._graph_definition import __all__ as _graph_definition_all from ._graph_node import * +from ._graph_node import __all__ as _graph_node_all from ._subclasses import * +from ._subclasses import __all__ as _subclasses_all + +__all__ = [*_graph_builder_all, *_graph_definition_all, *_graph_node_all, *_subclasses_all] diff --git a/cuda_core/cuda/core/graph/_graph_definition.pyi b/cuda_core/cuda/core/graph/_graph_definition.pyi index 15f34cec9ab..34f1131cd27 100644 --- a/cuda_core/cuda/core/graph/_graph_definition.pyi +++ b/cuda_core/cuda/core/graph/_graph_definition.pyi @@ -85,7 +85,7 @@ class GraphDefinition: See :meth:`GraphNode.deallocate` for full documentation. """ - def memset(self, dst: int, value, width: int, height: int=1, pitch: int=0) -> MemsetNode: + def memset(self, dst: int, value, width: int, *, height: int=1, pitch: int=0) -> MemsetNode: """Add an entry-point memset node (no dependencies). See :meth:`GraphNode.memset` for full documentation. diff --git a/cuda_core/cuda/core/graph/_graph_definition.pyx b/cuda_core/cuda/core/graph/_graph_definition.pyx index 1ec56978327..78dbb07f9eb 100644 --- a/cuda_core/cuda/core/graph/_graph_definition.pyx +++ b/cuda_core/cuda/core/graph/_graph_definition.pyx @@ -159,6 +159,7 @@ cdef class GraphDefinition: dst: int, value, size_t width, + *, size_t height=1, size_t pitch=0 ) -> MemsetNode: