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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,8 @@ cython_debug/

# Unit test output
junit/

# Pyccel build artefacts and lock files
__pyccel__/
.lock_acquisition.lock
stc.lock
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to the `cunumpy` library are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- `xp.PyccelKernel`: Wraps a kernel compiled with [pyccel](https://github.com/pyccel/pyccel) (which only accepts NumPy arrays) so it can be called with CuPy arrays. Arguments are copied to the host before the call, in-place kernel updates are copied back to the device, and returned arrays are moved back to the device. On the NumPy backend the kernel is called directly, without conversion. Tuples, lists and dicts are traversed recursively; pass `object_modules=("your_package.",)` to also traverse the attributes of your own objects. Pass `outputs=(5,)` (indices for positional arguments, names for keyword arguments) to declare which arguments the kernel writes to, so only those are copied back to the device instead of every converted array; `outputs=()` declares none. Conversion is identity-aware: an array reachable by several paths (passed twice, or both directly and as an object attribute) becomes a single host array, so the kernel sees the aliasing the caller intended and in-place updates are not lost; reference cycles are handled rather than recursed into.

## [0.1.3] - 2026-07-31

### Added
Expand Down
44 changes: 44 additions & 0 deletions docs/source/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,47 @@ with xp.use_backend("numpy"):

### `synchronize()`
Blocks until all preceding GPU operations are complete. This is a no-op when using the NumPy backend.

## Compiled Kernels

### `PyccelKernel(kernel, use_cupy=None, object_modules=(), outputs=None)`
Wraps a kernel compiled with [pyccel](https://github.com/pyccel/pyccel) — which only accepts NumPy arrays — so that it can be called with CuPy arrays as well.

On the CuPy backend the arguments are copied to the host before the call, any in-place updates the kernel makes are copied back to the device afterwards, and arrays returned by the kernel are moved back to the device. On the NumPy backend the kernel is called directly, without any conversion.

```python
import cunumpy as xp
from my_package.kernels import axpy # pyccelized kernel

axpy = xp.PyccelKernel(axpy)

with xp.use_backend("cupy"):
x = xp.arange(10, dtype=xp.float64)
y = xp.ones(10, dtype=xp.float64)
out = xp.zeros(10, dtype=xp.float64)
axpy(2.0, x, y, out) # `out` is updated in place, on the GPU
```

Tuples, lists and dicts are traversed recursively. Pass `object_modules` to also traverse the attributes of your own objects, e.g. `object_modules=("struphy.", "feectools.")`; instances from other modules are handed to the kernel untouched.

#### Declaring outputs

By default every array that was copied to the host is copied back afterwards, since the wrapper cannot know which ones the kernel wrote to. Most pyccel kernels write to one `out` argument and only read the rest, so `outputs` lets you skip the needless transfers:

```python
interpolate = xp.PyccelKernel(some_interpolation_kernel, outputs=(5,))

interpolate(x, y, z, basis, coeffs, out) # `out` is argument 5
```

Only the declared arguments are copied back; `basis` and `coeffs` make the trip to the host and no further. Containers and traversed objects may be declared too — every array nested inside them is copied back. An array that is *also* reachable from a declared output (e.g. passed as both an input and the output) is still copied back.

Declare positional arguments by index (negatives count from the end) and keyword arguments by name, e.g. `outputs=("out",)` for `interpolate(..., out=out)`. The two are not interchangeable: pyccel-compiled kernels are builtins with no introspectable signature, so the wrapper cannot map a name onto a position. A declaration that matches no argument of the call raises `IndexError`/`KeyError` rather than silently copying nothing back.

`outputs=()` declares that the kernel writes to none of its arguments. Leaving `outputs` unset keeps the always-correct default. Note that a *wrong* declaration is a silent-wrong-answer bug: an argument the kernel writes to but that you did not declare keeps its stale values on the GPU.

#### Aliasing and cycles

Conversion is identity-aware: an array reachable by several paths — passed as two arguments, or both directly and as an attribute of a traversed object — becomes a single array on the host, so the kernel sees the aliasing the caller intended and no in-place update is lost on the way back. Reference cycles are handled rather than recursed into.

Set `use_cupy` explicitly to force conversion on or off. By default it is decided per call: conversion happens when the active backend is CuPy, or when a CuPy array is passed in.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ optional-dependencies.docs = [
"sphinx",
"sphinx-book-theme",
]
optional-dependencies.test = [ "coverage", "pytest" ]
optional-dependencies.test = [ "coverage", "pyccel", "pytest" ]
urls."Source" = "https://github.com/max-models/cunumpy"

[tool.setuptools.packages.find]
Expand Down
2 changes: 2 additions & 0 deletions src/cunumpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from importlib.metadata import PackageNotFoundError, version

from . import xp
from .kernel import PyccelKernel
from .xp import (
cupy_available,
get_backend,
Expand All @@ -22,6 +23,7 @@
__version__ = "0.0.0+unknown"

__all__ = [
"PyccelKernel",
"__version__",
"cupy_available",
"cupy_backend",
Expand Down
1 change: 1 addition & 0 deletions src/cunumpy/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import numpy as np
from numpy import *

from . import xp as xp
from .kernel import PyccelKernel as PyccelKernel

def to_numpy(array: Any) -> np.ndarray: ...
def to_cupy(array: Any) -> Any: ...
Expand Down
Loading
Loading