Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
*.ipynb filter=strip-notebook-output
*.ipynb filter=strip-notebook-output
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ concurrency:

jobs:
docs:
uses: max-models/starlight-theme/.github/workflows/docs.yml@v0.4.0
uses: max-models/starlight-theme/.github/workflows/docs.yml@v0.5.0
with:
notebooks: false
# stable-branch: main # deploy the latest release at /<repo>/ and devel at /<repo>/dev/
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/gpu_ci_trigger.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SETUP INSTRUCTIONS:
# ------------------
# This workflow synchronizes the code to GitLab via SSH to trigger GPU-enabled CI.
#
#
# 1. GENERATE SSH KEY PAIR (on your local machine):
# ssh-keygen -t ed25519 -f ~/.ssh/gitlab_sync_key -N "" -C "github-to-gitlab-sync"
#
Expand Down Expand Up @@ -63,8 +63,10 @@ jobs:
# 3. Add GitLab SSH remote
git remote add gitlab git@gitlab.mpcdf.mpg.de:maxlin/cunumpy.git

# 4. Force push (This automatically starts the GitLab Pipeline)
git push -f gitlab "HEAD:refs/heads/$TARGET_BRANCH"
# 4. Force push (This automatically starts the GitLab Pipeline). The pushed branch
# is gpu-test-*, so pass the target branch for the max-models dependencies.
git push -f -o ci.variable="MAX_MODELS_TARGET=${{ github.base_ref || github.ref_name }}" \
gitlab "HEAD:refs/heads/$TARGET_BRANCH"
echo "PUSHED_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"

# 5. Provide the direct link
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ jobs:
- name: Install project
run: |
pip install --upgrade pip
# devel branches of the max-models dependencies, except on runs that target main
if [ "${GITHUB_BASE_REF:-$GITHUB_REF_NAME}" != main ]; then python -m pip install -r requirements-devel.txt; fi
pip install ".[test-compiled,metal]"

# Hosted macOS runners are virtual machines and usually have no Metal GPU,
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,3 @@ jobs:
TWINE_USERNAME: __token__ # Use API token
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*

8 changes: 7 additions & 1 deletion .github/workflows/pyodide.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
pull_request:
branches: [main, devel]

# Runs that do not target main take the max-models packages from their devel branches
# (requirements-devel.txt); micropip cannot install from git, so they reach Pyodide as wheels.
jobs:
wheel-test:
runs-on: ubuntu-latest
Expand All @@ -22,11 +24,15 @@ jobs:
cache-dependency-path: tests/pyodide/package-lock.json
- name: Build wheel and install compiler-free test dependencies
run: |
if [ "${GITHUB_BASE_REF:-$GITHUB_REF_NAME}" != main ]; then
python -m pip install -r requirements-devel.txt
python -m pip wheel --no-deps -w devel-wheels -r requirements-devel.txt
fi
python -m pip install build '.[test]'
python -m build --wheel
- name: Run portable tests in native Python
run: python -m pytest -q tests/portable
- name: Install Pyodide
run: npm ci --prefix tests/pyodide
- name: Run portable tests against the wheel in Pyodide
run: node tests/pyodide/run.mjs dist/cunumpy-*-py3-none-any.whl
run: node tests/pyodide/run.mjs dist/cunumpy-*-py3-none-any.whl $(ls devel-wheels/*.whl 2>/dev/null)
14 changes: 7 additions & 7 deletions .github/workflows/static_analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,18 @@ jobs:
- name: Code formatting with ruff
run: |
pip install ruff
ruff format --check src/ tests/
ruff format --check .

mypy:
ssort:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout the code
uses: actions/checkout@v4

- name: Type checking with mypy
- name: Statement order with ssort
run: |
pip install mypy
mypy src/ || true
pip install ssort
ssort --check --diff .

prospector:
runs-on: ubuntu-latest
Expand All @@ -72,7 +71,7 @@ jobs:
- name: Linting with ruff (including import sorting)
run: |
pip install ruff
ruff check src/ tests/
ruff check .

numpydoc:
runs-on: ubuntu-latest
Expand All @@ -82,6 +81,7 @@ jobs:

- name: Docstring sections with numpydoc
run: |
if [ "${GITHUB_BASE_REF:-$GITHUB_REF_NAME}" != main ]; then python -m pip install -r requirements-devel.txt; fi
pip install . numpydoc
python -m numpydoc lint $(git ls-files 'src/cunumpy/*.py' 'src/cunumpy/**/*.py' | grep -v _fake_cupy)

Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ on:
- devel
workflow_call:

# Runs that do not target main install the devel branches of the max-models dependencies
# (requirements-devel.txt) before cunumpy; PRs to main and the release use PyPI only.
jobs:
compiled-fake-cupy:
runs-on: ubuntu-latest
Expand All @@ -22,6 +24,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y gcc gfortran
if [ "${GITHUB_BASE_REF:-$GITHUB_REF_NAME}" != main ]; then python -m pip install -r requirements-devel.txt; fi
python -m pip install ".[test-compiled]"
- name: Test compiled host kernels with fake device arrays
env:
Expand Down Expand Up @@ -61,6 +64,7 @@ jobs:
- name: Install project
run: |
pip install --upgrade pip
if [ "${GITHUB_BASE_REF:-$GITHUB_REF_NAME}" != main ]; then python -m pip install -r requirements-devel.txt; fi
pip install ".[dev]"

- name: Run tests
Expand All @@ -70,3 +74,18 @@ jobs:
- name: Run docstring examples
run: |
pytest --doctest-modules src/cunumpy

# PyPI rejects direct references (`name @ git+...`): the wheel's metadata must have none.
no-direct-references:
if: (github.base_ref || github.ref_name) == 'main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Check the wheel for direct references
run: |
python -m pip install build
python -m build --wheel
! unzip -p dist/*.whl '*.dist-info/METADATA' | grep -E '^Requires-Dist:.* @ '
9 changes: 9 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ variables:
stages:
- test

# Runs that do not target main install the devel branches of the max-models dependencies
# first; main uses PyPI only. The GitHub trigger pushes gpu-test-* branches and passes the
# target branch in MAX_MODELS_TARGET.
.install_devel_dependencies: &install_devel_dependencies
- TARGET="${MAX_MODELS_TARGET:-${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-$CI_COMMIT_BRANCH}}"
- if [ "$TARGET" != main ]; then python3 -m pip install --user -r requirements-devel.txt; fi

.gpu_job_nvidia:
stage: test
image: ${CUDA_IMAGE}
Expand All @@ -27,6 +34,7 @@ stages:
- nvidia-smi
# Keep NVRTC, CUDA libraries and headers on the same toolkit generation.
- python3 -m pip install --user "cupy-cuda13x[ctk]${CUPY_PIN}" "cuda-toolkit==13.2.*"
- *install_devel_dependencies
- python3 -m pip install --user -e ".[test]"
- export PATH="$HOME/.local/bin:$PATH"
- python3 -c "import cupy; cupy.show_config()"
Expand All @@ -49,6 +57,7 @@ stages:
- export ROCM_HOME="${ROCM_PATH:-/opt/rocm}"
- export CUPY_INSTALL_USE_HIP=1
- python3 -m pip install --user "cupy${CUPY_PIN}"
- *install_devel_dependencies
- python3 -m pip install --user -e ".[test]"
- export PATH="$HOME/.local/bin:$PATH"
- python3 -c "import cupy; cupy.show_config()"
Expand Down
36 changes: 27 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Use the latest stable version
rev: v6.0.0
hooks:
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-added-large-files # Prevent giant files from being committed.
args: ["--maxkb=1000"]
- id: check-merge-conflict # Check for files that contain merge conflict strings.
- id: check-toml # Attempts to load all TOML files to verify syntax.
- id: check-yaml # Attempts to load all yaml files to verify syntax.
args: ["--unsafe"]

args: [--maxkb=500]
exclude: ^(uv\.lock|docs/package-lock\.json)$ # generated lock files
- id: check-merge-conflict # Check for files that contain merge conflict strings.
- repo: https://github.com/kynan/nbstripout
rev: 0.8.1
rev: 0.9.1
hooks:
- id: nbstripout # remove jupyter notebook cell output
- id: nbstripout # strip jupyter notebook output
- repo: https://github.com/bwhmather/ssort
rev: 0.17.0
hooks:
- id: ssort # order statements so names are defined before they are used
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.17.3
hooks:
- id: ruff-check # lint, including import sorting (the I rules)
args: [--fix]
- id: ruff-format
- repo: https://github.com/numpy/numpydoc
rev: v1.11.0
hooks:
- id: numpydoc-validation # docstring sections, configured in pyproject.toml
files: ^src/cunumpy/
exclude: _fake_cupy
30 changes: 29 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.2] - 2026-10-27

### Fixed
- `PyccelCache` imports a new build once in a subprocess before it stores it: a build the
interpreter cannot import, or one that crashes it (an extension linked to `libpython` in a
Python that has it built in, such as uv's CPython 3.12 on macOS), is a `RuntimeError` and is
not stored, instead of a segmentation fault in every process that loads it.
- The `PyccelCache` key includes the NumPy version, so environments that share an interpreter
but not NumPy no longer share builds.

### Added
- `xp.kernels.KernelPackage(name)`: the `PyccelCache` and the `Kernel.from_folder` options
shared by the kernel folders of one package (`host_suffix="_pyccel"`, `dispatch="arrays"`,
`block_size=256`, `n_threads_from="last_axis"`), with `from_folder(package, **overrides)`
and `metal_kernel(...)` (float32 `MetalKernel`s with zeroed outputs), so packages no longer
keep their own copies of these settings.
- `xp.kernels.source_roots(*packages)`: the source roots of installed packages, for
`include_dirs` of a CUDA kernel that includes another package's header.
- `xp.cuda.shared_memory_fits(num_values, itemsize=8)`: whether a per-block buffer fits
`DEFAULT_SHARED_MEMORY_PER_BLOCK`.
- scope-profiler regions around the coarse operations (`Kernel` calls as
`cunumpy.kernel.<name>`, CUDA and Pyccel compilation, `sort_by_key`,
`SegmentPlan.sum`, `compact_by_mask`, `morton_keys`, the solvers, `exchange`,
and the device copies of `mpi_buffer` and `DeviceMirror`), behind the opt-in
tag `cunumpy`: off unless the application enables it, e.g.
`sp.setup(enable_tags=["cunumpy"])` or `SCOPE_PROFILER_ENABLE_TAGS=cunumpy`.
New dependency: `scope-profiler>=0.7.5`.

### Changed
- `xp.algorithms.sort_by_key` gathers a 2-D NumPy array along its last axis (component-major
markers, `axis=-1`) row by row, about 2x faster than `take` (1e6 markers: 3 rows 12.9 to 6.7 ms,
Expand Down Expand Up @@ -440,7 +468,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- **Dynamic Dispatch Architecture**: Refactored `src/cunumpy/xp.py` to use module-level `__getattr__`. This ensures that `cunumpy.<op>` calls always resolve to the currently active backend module, enabling seamless runtime switching via `set_backend`.
- **Type Safety**: Updated `src/cunumpy/__init__.pyi` stubs to provide full IDE autocompletion and type-checking for all new API methods.
- **Documentation**:
- **Documentation**:
- Simplified `README.md` and documentation to exclusively focus on PyPI installation (`pip install cunumpy`).
- Enhanced `quickstart.md` and `api.md` with usage examples for the new backend control and synchronization features.
- **CI/CD**: Restricted GitHub Pages documentation deployment to the `devel` branch only.
Expand Down
5 changes: 5 additions & 0 deletions docs/src/content/docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ python -m pytest tests/unit

Tests that need a GPU are skipped automatically where CuPy is not functional.

On `devel`, cunumpy is developed against the `devel` branches of the other max-models packages
(currently maybempi): `uv sync` picks them up from `[tool.uv.sources]`, and with pip run
`python -m pip install -r requirements-devel.txt` before the install above. CI does the same for
pull requests to `devel`; pull requests to `main` and the PyPI release use PyPI versions only.

## Environment variables

| Variable | Effect |
Expand Down
22 changes: 22 additions & 0 deletions docs/src/content/docs/guides/profiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,28 @@ another library converting internally) show up in Nsight as `memcpy` rows on the
timeline; NVTX ranges tell you which part of the step issued them. See [Moving
data between host and device](/cunumpy/guides/data-movement/).

## Cunumpy's own regions in scope-profiler

Cunumpy ships [scope-profiler](https://github.com/max-models/scope-profiler)
regions around its coarse operations: every `Kernel` call
(`cunumpy.kernel.<name>`), CUDA and Pyccel compilation, `sort_by_key`,
`segment_sum` (`cunumpy.SegmentPlan.sum`), `compact_by_mask`, `morton_keys`, the
solvers, `exchange` and the host/device copies of `mpi_buffer` and
`DeviceMirror`. They are off by default, so an application that profiles itself
does not see them, and cost at most about 0.2 µs per call while off. Turn them on with the `cunumpy` tag:

```python
import scope_profiler as sp

sp.setup(enable_tags=["cunumpy"])
```

or `SCOPE_PROFILER_ENABLE_TAGS=cunumpy`, or
`scope-profiler run --enable-tag cunumpy simulate.py` (`"*"` enables every
library's tag). The regions time the host side: on CuPy, a kernel region
measures the launch, not the kernel; wrap a synchronizing `timed_region` around
a loop of steps for device times.

## A profiling checklist

1. Run a few steps with `count_transfers()` and remove every transfer from the
Expand Down
45 changes: 43 additions & 2 deletions docs/src/content/docs/kernels/dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,15 +257,19 @@ inside `with xp.kernels.use_host_kernel_implementation("numpy"):` (or set

`epyccel` compiles again on every call, in every process. `PyccelCache(name)`
compiles a module once with the `pyccel compile` command and keeps the build
on disk, keyed on the module source, the OpenMP setting, the Pyccel and Python
versions and the platform, so only the first run after an edit compiles:
on disk, keyed on the module source, the OpenMP setting, the Pyccel, NumPy and
Python versions and the platform, so only the first run after an edit compiles:

- builds go to `~/.cache/<name>/kernels`, or the directory in
`<NAME>_KERNEL_CACHE`; if that is not writable, to `<tmp>/<name>-kernels`;
- modules are built with OpenMP; `<NAME>_OPENMP=0` builds them serial, except
modules that call the OpenMP runtime;
- builds are published with an atomic rename, so MPI ranks compiling the same
module on a cold cache never load a partial build;
- a new build is imported once in a subprocess before it is published; if that
import fails or crashes the interpreter (an extension linked to `libpython` in
a Python that has it built in), `compile` raises instead and nothing is
stored, so the process that uses the cache does not crash;
- the key covers the module's own source only: after editing a module the
kernel imports, edit the kernel or clear the cache.

Expand All @@ -282,6 +286,43 @@ Packages that compile their kernels at install time with the `pyccel` command
do not need `compile_host`: the compiled modules are imported like the Python
ones.

### One set of options per package

A package with many kernel folders passes the same options to every
`from_folder()`. A `KernelPackage` holds them, with the package's
`PyccelCache`, so they are declared once:

```python
# my_sim/kernels/__init__.py
KERNEL_PACKAGE = xp.kernels.KernelPackage("my-sim")

# my_sim/kernels/push/__init__.py
from my_sim.kernels import KERNEL_PACKAGE

kernel = KERNEL_PACKAGE.from_folder(__name__)

# my_sim/kernels/push/push_metal.py
_KERNEL = KERNEL_PACKAGE.metal_kernel("push", SOURCE, ["x", "v"], ["x_new"])
```

The defaults are `host_suffix="_pyccel"`, `dispatch="arrays"` (see below),
`block_size=256` and `n_threads_from="last_axis"` (the marker count of a
component-major `(ncomp, N)` array). A folder overrides any of them in its own
call, e.g. `KERNEL_PACKAGE.from_folder(__name__, n_threads_from=rows)`;
`KERNEL_PACKAGE.options` are the same options for
`KernelCatalog.from_package()`. `metal_kernel()` builds a `MetalKernel` with
`threadgroup=256`, `float64="cast"` and `init_value=0`.

A CUDA source that includes another package's header (`#include
"other_pkg/kernels/cell.cuh"`) needs that package's source root as an include
directory; `xp.kernels.source_roots()` finds it without importing the package:

```python
kernel = KERNEL_PACKAGE.from_folder(
__name__, include_dirs=xp.kernels.source_roots("my_sim", "other_pkg")
)
```

## Choosing the kernel by where the arrays are

`Kernel` picks the CUDA kernel on the CuPy backend. Real codes also hand host
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The submodules are named so that they do not hide a NumPy name (`rng`, not
| Submodule | Backends | Contents |
|---|---|---|
| [`cunumpy`](/cunumpy/api/cunumpy/) | both | NumPy/CuPy namespace, backend selection, array inspection and conversion, `as_device_array`, `to_host_async`, `synchronize`, `host_call`, `evaluate_on_host`, `setup_on_host`, `scipy`, `require_version` |
| [`cunumpy.kernels`](/cunumpy/api/cunumpy/kernels/) | both | `Kernel`, `KernelCatalog`, `PyccelKernel`, `CudaKernel`, `CudaKernelVariants`, `MetalKernel`, `metal_available`, `PyccelCache`, host implementations, `as_kernel_array`, `kernel_output`, `kernel_outputs`, `fuse` |
| [`cunumpy.kernels`](/cunumpy/api/cunumpy/kernels/) | both | `Kernel`, `KernelCatalog`, `PyccelKernel`, `CudaKernel`, `CudaKernelVariants`, `MetalKernel`, `metal_available`, `PyccelCache`, `KernelPackage`, `source_roots`, host implementations, `as_kernel_array`, `kernel_output`, `kernel_outputs`, `fuse` |
| [`cunumpy.arguments`](/cunumpy/api/cunumpy/arguments/) | CUDA only | `CudaArguments`, `CudaStruct`, `CudaStructArguments`, `CudaStructValue`, `write_cuda_header` |
| [`cunumpy.cuda`](/cunumpy/api/cunumpy/cuda/) | CUDA only | device selection and memory, `stream`, streams/events, `pin_memory`, debug mode, CUDA headers and source tools (`cuda_include_dir`, `parse_cuda_signature`) |
| [`cunumpy.rng`](/cunumpy/api/cunumpy/rng/) | both | `random_streams`, `get_rng`, `philox_*` |
Expand Down
Loading
Loading