Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a5cf623
Correct signature for .allocate method, stream is kwarg only now
oleksandr-pavlyk Aug 3, 2026
8070510
Add example of using state.set_stream
oleksandr-pavlyk Aug 3, 2026
f6130e0
Add script to run examples by groups
oleksandr-pavlyk Aug 3, 2026
42369d0
Update axes.py example to use .allocate with kwarg
oleksandr-pavlyk Aug 3, 2026
70c590b
Break single requirements.txt into multiple files per example group
oleksandr-pavlyk Aug 3, 2026
f8bde85
Add workflow to execute Python examples in GPU runner
oleksandr-pavlyk Aug 3, 2026
ddc165f
Include stream.py in run_python_examples.py script
oleksandr-pavlyk Aug 3, 2026
bf40e53
Constrain Python example CI dependencies
oleksandr-pavlyk Aug 3, 2026
4b48fe5
Scheduled jobs should include heavier examples
oleksandr-pavlyk Aug 3, 2026
2ed6410
Grant lowest required permission in the workflow
oleksandr-pavlyk Aug 3, 2026
53274b9
Expand the comment to explain what use of set_stream accomplishes
oleksandr-pavlyk Aug 3, 2026
cd45304
Add a comment stating the objective of example-running step in workflow
oleksandr-pavlyk Aug 3, 2026
e1f1e66
Specify 60 minutes timeout for the workflow step to run examples
oleksandr-pavlyk Aug 3, 2026
c282659
wrap subprocess.run calls in try/except
oleksandr-pavlyk Aug 3, 2026
decbc43
Handle FileNotFoundError in run_python_examples.py
oleksandr-pavlyk Aug 3, 2026
cf4ae90
Make sure to fail if compiler is not installed
oleksandr-pavlyk Aug 3, 2026
49864d2
Propagate non-zero return status from running examples
oleksandr-pavlyk Aug 3, 2026
89fe314
Address Ruff lint
oleksandr-pavlyk Aug 3, 2026
477776e
Refactor common logic of outputting RUN/PASS/FAIL for examples
oleksandr-pavlyk Aug 3, 2026
b36036e
Refactor duplicated logic
oleksandr-pavlyk Aug 3, 2026
f1738c3
Make sure we search for wheel corresponding to ABI tag of the Python …
oleksandr-pavlyk Aug 3, 2026
dcf282c
Catch all exceptions around find_spec
oleksandr-pavlyk Aug 3, 2026
708fa77
Execute run_example_env in subshell to isolate it
oleksandr-pavlyk Aug 3, 2026
9b63513
Reduce time constraints on running example groups to fit 60 minutes
oleksandr-pavlyk Aug 3, 2026
e4000d6
Echo ::endgroup:: unconditionally, error or normal run
oleksandr-pavlyk Aug 3, 2026
69f33a4
No need to carry Python test file for CI script
oleksandr-pavlyk Aug 3, 2026
886df7c
Address feedback
oleksandr-pavlyk Aug 3, 2026
f1a7097
Use command -v, instead of which
oleksandr-pavlyk Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/build-and-test-python-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,31 @@ name: Build and Test Python Wheels

on:
workflow_call:
inputs:
include_heavy_python_examples:
description: "Also test heavyweight Python examples such as Torch and CuTe."
required: false
type: boolean
default: false
floating_python_example_dependencies:
description: "Test Python examples with unconstrained floating dependencies and print constraints candidates."
required: false
type: boolean
default: false
workflow_dispatch:
inputs:
include_heavy_python_examples:
description: "Also test heavyweight Python examples such as Torch and CuTe."
required: false
type: boolean
default: false
floating_python_example_dependencies:
description: "Test Python examples with unconstrained floating dependencies and print constraints candidates."
required: false
type: boolean
default: false
schedule:
- cron: '0 10 * * 1'

defaults:
run:
Expand Down Expand Up @@ -76,12 +100,65 @@ jobs:
-py-version ${{ matrix.python }} \
-cuda-version ${{ matrix.cuda }}

# Exercise committed examples against the built wheel. PR runs use constrained
# dependencies for stability; scheduled/manual floating runs catch upstream
# dependency drift.
test-python-examples:
name: Test Python examples (standard, CUDA 13, Python 3.13)
needs: build-wheels
runs-on: linux-amd64-gpu-l4-latest-1
# Sized for the bounded per-case timeouts in ci/run_python_examples.py.
timeout-minutes: 60
Comment thread
oleksandr-pavlyk marked this conversation as resolved.
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: wheel-cuda_bench-py3.13
path: wheelhouse

- name: Test Python examples
env:
INCLUDE_HEAVY_PYTHON_EXAMPLES: ${{ github.event_name == 'schedule' || inputs.include_heavy_python_examples }}
FLOATING_PYTHON_EXAMPLE_DEPENDENCIES: ${{ github.event_name == 'schedule' || inputs.floating_python_example_dependencies }}
run: |
set -o pipefail
args=()
if [[ "${INCLUDE_HEAVY_PYTHON_EXAMPLES}" == "true" ]]; then
args+=("-include-heavy-examples")
fi
if [[ "${FLOATING_PYTHON_EXAMPLE_DEPENDENCIES}" == "true" ]]; then
args+=("-floating-deps")
fi

mkdir -p python-example-results
bash ci/test_python_examples.sh \
-py-version 3.13 \
"${args[@]}" | tee python-example-results/output.log

- name: Upload Python example results
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: python-example-results
path: python-example-results/
if-no-files-found: ignore

verify-workflow:
name: Verify all builds and tests succeeded
if: ${{ always() }}
needs:
- build-wheels
- test-wheels
- test-python-examples
runs-on: ubuntu-latest
steps:
- name: Check build results
Expand All @@ -94,4 +171,8 @@ jobs:
echo "Wheel tests failed!"
exit 1
fi
if [[ "${{ needs.test-python-examples.result }}" != "success" ]]; then
echo "Python example tests failed!"
exit 1
fi
echo "All wheels built and tested successfully!"
184 changes: 184 additions & 0 deletions ci/format_python_example_constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

from __future__ import annotations

import argparse
import re
from pathlib import Path

PIN_RE = re.compile(r"^\s*([A-Za-z0-9_.-]+)\s*==\s*(\S+)\s*$")
DIRECT_REF_RE = re.compile(r"^\s*([A-Za-z0-9_.-]+)\s*@\s*(\S.*)$")
REQ_NAME_RE = re.compile(r"^\s*([A-Za-z0-9_.-]+)")


def normalize_name(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()


def parse_requirement_include(stripped_line: str) -> str | None:
if stripped_line.startswith(("-r ", "--requirement ")):
_, include_path = stripped_line.split(maxsplit=1)
return include_path
if stripped_line.startswith("--requirement="):
return stripped_line.split("=", 1)[1].strip()
if stripped_line.startswith("-r") and len(stripped_line) > 2:
return stripped_line[2:].strip()
return None


def iter_requirement_lines(path: Path, seen: set[Path] | None = None):
if seen is None:
seen = set()
path = path.resolve()
if path in seen:
return
seen.add(path)

for raw_line in path.read_text(encoding="utf-8").splitlines():
stripped = raw_line.split("#", 1)[0].strip()
if not stripped:
continue
if include_path := parse_requirement_include(stripped):
yield from iter_requirement_lines(path.parent / include_path, seen)
continue
if stripped.startswith("-"):
continue
yield stripped


def parse_constraint_names(path: Path) -> dict[str, str]:
names: dict[str, str] = {}
for stripped in iter_requirement_lines(path):
match = REQ_NAME_RE.match(stripped)
if match:
original_name = match.group(1)
names[normalize_name(original_name)] = original_name
return names


def parse_freeze_file(path: Path) -> dict[str, str]:
pins: dict[str, str] = {}
for line in path.read_text(encoding="utf-8").splitlines():
match = PIN_RE.match(line)
if match:
pins[normalize_name(match.group(1))] = f"{match.group(1)}=={match.group(2)}"
continue
match = DIRECT_REF_RE.match(line)
if match:
package_name = match.group(1)
pins[normalize_name(package_name)] = (
f"# DIRECT {package_name}: {line.strip()}"
)
return pins


def parse_requirement_names(path: Path) -> set[str]:
names: set[str] = set()
for stripped in iter_requirement_lines(path):
req = stripped.split(";", 1)[0].strip()
req = req.split("[", 1)[0].strip()
match = REQ_NAME_RE.match(req)
if match:
names.add(normalize_name(match.group(1)))
return names


def collect_freeze_pins(freeze_dir: Path) -> dict[str, dict[str, set[str]]]:
package_to_envs: dict[str, dict[str, set[str]]] = {}
for freeze_file in sorted(freeze_dir.glob("*.txt")):
env_name = freeze_file.stem
for package_name, pin in parse_freeze_file(freeze_file).items():
envs = package_to_envs.setdefault(package_name, {})
envs.setdefault(pin, set()).add(env_name)
return package_to_envs


def print_constraints_candidate(
*,
constraints_file: Path,
freeze_dir: Path,
python_version: str,
cuda_extra: str,
) -> bool:
tracked_names = parse_constraint_names(constraints_file)
package_to_envs = collect_freeze_pins(freeze_dir)

print("::group::Constraints candidate for tracked Python example packages")
print(f"# Source: {constraints_file}")
print("# Generated from floating example dependency run")
print(f"# Python: {python_version}")
print(f"# CUDA extra: {cuda_extra}")

conflicts: list[str] = []
missing: list[str] = []
for normalized_name in sorted(tracked_names):
pins = package_to_envs.get(normalized_name)
if not pins:
missing.append(tracked_names[normalized_name])
continue
if len(pins) > 1:
detail = ", ".join(
f"{pin} ({', '.join(sorted(envs))})"
for pin, envs in sorted(pins.items())
)
conflicts.append(f"# CONFLICT {tracked_names[normalized_name]}: {detail}")
continue
print(next(iter(pins)))

for line in conflicts:
print(line)
for package_name in missing:
print(f"# MISSING {package_name}")
print("::endgroup::")
return not conflicts and not missing


def print_untracked_direct_requirements(
*, requirements_file: Path, constraints_file: Path
) -> None:
tracked_names = set(parse_constraint_names(constraints_file))
direct_names = parse_requirement_names(requirements_file)
untracked_names = sorted(direct_names - tracked_names)

print("::group::Untracked direct Python example requirements")
print(f"# Requirements source: {requirements_file}")
print(f"# Constraints source: {constraints_file}")
if untracked_names:
for name in untracked_names:
print(name)
else:
print("# None")
print("::endgroup::")


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Format floating Python example dependency results for CI logs."
)
parser.add_argument("--constraints-file", type=Path, required=True)
parser.add_argument("--requirements-file", type=Path, required=True)
parser.add_argument("--freeze-dir", type=Path, required=True)
parser.add_argument("--python-version", required=True)
parser.add_argument("--cuda-extra", required=True)
return parser.parse_args()


def main() -> int:
args = parse_args()
constraints_ok = print_constraints_candidate(
constraints_file=args.constraints_file,
freeze_dir=args.freeze_dir,
python_version=args.python_version,
cuda_extra=args.cuda_extra,
)
print_untracked_direct_requirements(
requirements_file=args.requirements_file,
constraints_file=args.constraints_file,
)
return 0 if constraints_ok else 1


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading