cunumpy 0.1.3__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cunumpy-0.1.5/PKG-INFO ADDED
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: cunumpy
3
+ Version: 0.1.5
4
+ Summary: Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
5
+ Author: Max
6
+ Project-URL: Source, https://github.com/max-models/cunumpy
7
+ Keywords: python
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: array-api-compat
19
+ Requires-Dist: numpy
20
+ Provides-Extra: dev
21
+ Requires-Dist: black[jupyter]; extra == "dev"
22
+ Requires-Dist: isort; extra == "dev"
23
+ Requires-Dist: cunumpy[docs,test-compiled]; extra == "dev"
24
+ Provides-Extra: docs
25
+ Requires-Dist: ipykernel; extra == "docs"
26
+ Requires-Dist: myst-parser; extra == "docs"
27
+ Requires-Dist: nbconvert; extra == "docs"
28
+ Requires-Dist: nbsphinx; extra == "docs"
29
+ Requires-Dist: jupyterlab; extra == "docs"
30
+ Requires-Dist: pre-commit; extra == "docs"
31
+ Requires-Dist: pyproject-fmt; extra == "docs"
32
+ Requires-Dist: sphinx; extra == "docs"
33
+ Requires-Dist: sphinx-book-theme; extra == "docs"
34
+ Provides-Extra: test
35
+ Requires-Dist: coverage; extra == "test"
36
+ Requires-Dist: pytest; extra == "test"
37
+ Provides-Extra: test-compiled
38
+ Requires-Dist: cunumpy[test]; extra == "test-compiled"
39
+ Requires-Dist: pyccel; extra == "test-compiled"
40
+
41
+ # CuNumpy
42
+
43
+ Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
44
+
45
+ # Install
46
+
47
+ ```bash
48
+ pip install cunumpy
49
+ ```
50
+
51
+ Example usage:
52
+
53
+ ```
54
+ export ARRAY_BACKEND=cupy
55
+ ```
56
+
57
+ ```python
58
+ import cunumpy as xp
59
+
60
+ xp.set_backend("cupy")
61
+
62
+ arr = xp.array([1, 2])
63
+
64
+ print(f"{type(arr) = }")
65
+ print(f"{xp.__version__ = }")
66
+
67
+ # Convert to NumPy
68
+ arr_np = xp.to_numpy(arr)
69
+
70
+ # Convert to active backend
71
+ arr_xp = xp.to_cunumpy(arr)
72
+
73
+ # Inspect backend
74
+ print(f"{xp.get_backend(arr) = }")
75
+ print(f"{xp.is_gpu(arr) = }")
76
+ print(f"{xp.is_cpu(arr) = }")
77
+
78
+ # Temporarily switch backend
79
+ with xp.use_backend("numpy"):
80
+ # This code runs on CPU even if ARRAY_BACKEND=cupy
81
+ arr_cpu = xp.zeros(100)
82
+
83
+ # Set backend globally
84
+ xp.set_backend("cupy")
85
+
86
+ # Synchronize GPU operations (no-op on CPU)
87
+ xp.synchronize()
88
+ ```
89
+
90
+ Output:
91
+
92
+ ```
93
+ type(arr) = <class 'cupy.ndarray'>
94
+ xp.__version__ = '0.1.4'
95
+ xp.get_backend(arr) = 'cupy'
96
+ xp.is_gpu(arr) = True
97
+ xp.is_cpu(arr) = False
98
+ ```
99
+
100
+ # Pyodide
101
+
102
+ cuNumPy supports Pyodide with the NumPy backend. In an initialized Pyodide
103
+ JavaScript runtime, install the package and run a Python-source kernel:
104
+
105
+ ```javascript
106
+ await pyodide.loadPackage("micropip");
107
+ await pyodide.runPythonAsync(`
108
+ import micropip
109
+ await micropip.install("cunumpy")
110
+
111
+ import cunumpy as xp
112
+ xp.set_backend("numpy")
113
+
114
+ def scale(values, factor):
115
+ values[:] *= factor
116
+ return values
117
+
118
+ values = xp.array([1.0, 2.0, 3.0])
119
+ result = xp.PyccelKernel(scale)(values, 2.0)
120
+ assert result is values
121
+ print(xp.to_numpy(result)) # [2. 4. 6.]
122
+ `);
123
+ ```
124
+
125
+ NumPy is the default backend when `ARRAY_BACKEND` is unset. CuPy/CUDA and
126
+ native Pyccel compilation are not supported in Pyodide. Despite its name,
127
+ `PyccelKernel` accepts ordinary Python callables and neither imports Pyccel nor
128
+ compiles code. Applications must supply Python-source kernels with
129
+ Pyodide-compatible imports.
130
+
131
+ CI tests the built wheel in Pyodide's WebAssembly runtime under Node.js, including
132
+ array operations, conversions, mutation/aliasing, backend context restoration,
133
+ and Python kernels, while rejecting CuPy or Pyccel imports. This does not test
134
+ browser-specific integration such as page loading or workers. See the
135
+ [Pyodide guide](docs/source/pyodide.md) for details and local test commands.
136
+
137
+ # Development tests
138
+
139
+ ```bash
140
+ pip install -e '.[test]'
141
+ pytest tests/portable
142
+ ```
143
+
144
+ The `test` extra is compiler-free. For compiled-kernel tests, install
145
+ `'.[test-compiled]'` and a working native compiler, then run `pytest`.
146
+ The `dev` extra includes these compiled-test dependencies as before.
147
+
148
+ # Build docs
149
+
150
+
151
+ ```
152
+ make html
153
+ cd ../
154
+ open docs/_build/html/index.html
155
+ ```
@@ -0,0 +1,115 @@
1
+ # CuNumpy
2
+
3
+ Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
4
+
5
+ # Install
6
+
7
+ ```bash
8
+ pip install cunumpy
9
+ ```
10
+
11
+ Example usage:
12
+
13
+ ```
14
+ export ARRAY_BACKEND=cupy
15
+ ```
16
+
17
+ ```python
18
+ import cunumpy as xp
19
+
20
+ xp.set_backend("cupy")
21
+
22
+ arr = xp.array([1, 2])
23
+
24
+ print(f"{type(arr) = }")
25
+ print(f"{xp.__version__ = }")
26
+
27
+ # Convert to NumPy
28
+ arr_np = xp.to_numpy(arr)
29
+
30
+ # Convert to active backend
31
+ arr_xp = xp.to_cunumpy(arr)
32
+
33
+ # Inspect backend
34
+ print(f"{xp.get_backend(arr) = }")
35
+ print(f"{xp.is_gpu(arr) = }")
36
+ print(f"{xp.is_cpu(arr) = }")
37
+
38
+ # Temporarily switch backend
39
+ with xp.use_backend("numpy"):
40
+ # This code runs on CPU even if ARRAY_BACKEND=cupy
41
+ arr_cpu = xp.zeros(100)
42
+
43
+ # Set backend globally
44
+ xp.set_backend("cupy")
45
+
46
+ # Synchronize GPU operations (no-op on CPU)
47
+ xp.synchronize()
48
+ ```
49
+
50
+ Output:
51
+
52
+ ```
53
+ type(arr) = <class 'cupy.ndarray'>
54
+ xp.__version__ = '0.1.4'
55
+ xp.get_backend(arr) = 'cupy'
56
+ xp.is_gpu(arr) = True
57
+ xp.is_cpu(arr) = False
58
+ ```
59
+
60
+ # Pyodide
61
+
62
+ cuNumPy supports Pyodide with the NumPy backend. In an initialized Pyodide
63
+ JavaScript runtime, install the package and run a Python-source kernel:
64
+
65
+ ```javascript
66
+ await pyodide.loadPackage("micropip");
67
+ await pyodide.runPythonAsync(`
68
+ import micropip
69
+ await micropip.install("cunumpy")
70
+
71
+ import cunumpy as xp
72
+ xp.set_backend("numpy")
73
+
74
+ def scale(values, factor):
75
+ values[:] *= factor
76
+ return values
77
+
78
+ values = xp.array([1.0, 2.0, 3.0])
79
+ result = xp.PyccelKernel(scale)(values, 2.0)
80
+ assert result is values
81
+ print(xp.to_numpy(result)) # [2. 4. 6.]
82
+ `);
83
+ ```
84
+
85
+ NumPy is the default backend when `ARRAY_BACKEND` is unset. CuPy/CUDA and
86
+ native Pyccel compilation are not supported in Pyodide. Despite its name,
87
+ `PyccelKernel` accepts ordinary Python callables and neither imports Pyccel nor
88
+ compiles code. Applications must supply Python-source kernels with
89
+ Pyodide-compatible imports.
90
+
91
+ CI tests the built wheel in Pyodide's WebAssembly runtime under Node.js, including
92
+ array operations, conversions, mutation/aliasing, backend context restoration,
93
+ and Python kernels, while rejecting CuPy or Pyccel imports. This does not test
94
+ browser-specific integration such as page loading or workers. See the
95
+ [Pyodide guide](docs/source/pyodide.md) for details and local test commands.
96
+
97
+ # Development tests
98
+
99
+ ```bash
100
+ pip install -e '.[test]'
101
+ pytest tests/portable
102
+ ```
103
+
104
+ The `test` extra is compiler-free. For compiled-kernel tests, install
105
+ `'.[test-compiled]'` and a working native compiler, then run `pytest`.
106
+ The `dev` extra includes these compiled-test dependencies as before.
107
+
108
+ # Build docs
109
+
110
+
111
+ ```
112
+ make html
113
+ cd ../
114
+ open docs/_build/html/index.html
115
+ ```
@@ -5,7 +5,7 @@ requires = [ "setuptools", "wheel" ]
5
5
 
6
6
  [project]
7
7
  name = "cunumpy"
8
- version = "0.1.3"
8
+ version = "0.1.5"
9
9
  description = "Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`."
10
10
  readme = "README.md"
11
11
  keywords = [ "python" ]
@@ -23,13 +23,14 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.13",
24
24
  ]
25
25
  dependencies = [
26
+ "array-api-compat",
26
27
  "numpy",
27
28
  ]
28
29
 
29
30
  optional-dependencies.dev = [
30
31
  "black[jupyter]",
31
32
  "isort",
32
- "cunumpy[test,docs]",
33
+ "cunumpy[test-compiled,docs]",
33
34
  ]
34
35
  # https://medium.com/@pratikdomadiya123/build-project-documentation-quickly-with-the-sphinx-python-2a9732b66594
35
36
  optional-dependencies.docs = [
@@ -44,6 +45,7 @@ optional-dependencies.docs = [
44
45
  "sphinx-book-theme",
45
46
  ]
46
47
  optional-dependencies.test = [ "coverage", "pytest" ]
48
+ optional-dependencies.test-compiled = [ "cunumpy[test]", "pyccel" ]
47
49
  urls."Source" = "https://github.com/max-models/cunumpy"
48
50
 
49
51
  [tool.setuptools.packages.find]
@@ -2,11 +2,14 @@
2
2
  from importlib.metadata import PackageNotFoundError, version
3
3
 
4
4
  from . import xp
5
+ from .kernel import PyccelKernel
5
6
  from .xp import (
7
+ assert_same_backend,
6
8
  cupy_available,
7
9
  get_backend,
8
10
  is_cpu,
9
11
  is_gpu,
12
+ same_backend,
10
13
  set_backend,
11
14
  set_device,
12
15
  synchronize,
@@ -22,13 +25,16 @@ except PackageNotFoundError:
22
25
  __version__ = "0.0.0+unknown"
23
26
 
24
27
  __all__ = [
28
+ "PyccelKernel",
25
29
  "__version__",
30
+ "assert_same_backend",
26
31
  "cupy_available",
27
32
  "cupy_backend",
28
33
  "get_backend",
29
34
  "is_cpu",
30
35
  "is_gpu",
31
36
  "numpy_backend",
37
+ "same_backend",
32
38
  "set_backend",
33
39
  "set_device",
34
40
  "synchronize",
@@ -8,6 +8,7 @@ import numpy as np
8
8
  from numpy import *
9
9
 
10
10
  from . import xp as xp
11
+ from .kernel import PyccelKernel as PyccelKernel
11
12
 
12
13
  def to_numpy(array: Any) -> np.ndarray: ...
13
14
  def to_cupy(array: Any) -> Any: ...
@@ -16,6 +17,8 @@ def cupy_available() -> bool: ...
16
17
  def get_backend(array: Any) -> str: ...
17
18
  def is_gpu(array: Any) -> bool: ...
18
19
  def is_cpu(array: Any) -> bool: ...
20
+ def same_backend(*arrays: Any) -> bool: ...
21
+ def assert_same_backend(*arrays: Any) -> None: ...
19
22
  @contextmanager
20
23
  def use_backend(backend: str) -> Generator[None]: ...
21
24
  def set_backend(backend: str) -> None: ...
@@ -0,0 +1,347 @@
1
+ """Interface for calling Pyccel-compiled kernels with CuPy arrays.
2
+
3
+ Kernels generated by `pyccel <https://github.com/pyccel/pyccel>`_ are compiled
4
+ C/Fortran routines that only understand NumPy (host) arrays. :class:`PyccelKernel`
5
+ wraps such a kernel so that it can be called transparently with CuPy (device)
6
+ arrays: the arguments are copied to the host before the call, in-place updates
7
+ made by the kernel are copied back to the device afterwards, and any arrays
8
+ returned by the kernel are moved back to the device.
9
+
10
+ On the NumPy backend the wrapper is a no-op and the kernel is called directly.
11
+ Ordinary Python callables are supported, including in Pyodide. This module
12
+ neither imports Pyccel nor compiles kernels; compilation, if desired, is the
13
+ caller's responsibility.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import copy
19
+ from typing import Any, Callable, Sequence
20
+
21
+ import array_api_compat
22
+ import numpy as np
23
+
24
+ from .xp import _cupy_backend, to_cupy, to_numpy
25
+
26
+ __all__ = ["PyccelKernel"]
27
+
28
+
29
+ class PyccelKernel:
30
+ """Call a NumPy callable or Pyccel-compiled kernel with NumPy or CuPy arrays.
31
+
32
+ Parameters
33
+ ----------
34
+ kernel : callable
35
+ The pyccelized kernel (or any callable expecting NumPy arrays).
36
+ use_cupy : bool, optional
37
+ Force host/device conversion on (``True``) or off (``False``). By
38
+ default (``None``) it is decided at call time: conversion happens when
39
+ the active backend is CuPy or when a CuPy array is passed in.
40
+ object_modules : sequence of str, optional
41
+ Module prefixes (e.g. ``("struphy.", "feectools.")``) whose instances
42
+ should be traversed attribute-by-attribute when looking for arrays to
43
+ convert. Objects from other modules are passed through untouched.
44
+ outputs : sequence of int or str, optional
45
+ Which arguments the kernel writes to. Only those are copied back to the
46
+ device after the call, which avoids pointless device transfers for the
47
+ (usually much larger) read-only inputs. Positional arguments are named
48
+ by index, keyword arguments by name::
49
+
50
+ interpolate = PyccelKernel(some_interpolation_kernel, outputs=(5,))
51
+ interpolate(x, y, z, basis, coeffs, out) # `out` is argument 5
52
+
53
+ Pyccel-compiled kernels are builtins with no introspectable signature,
54
+ so an index and a name are *not* interchangeable: declare the form you
55
+ actually call with. An empty sequence declares that the kernel writes to
56
+ none of its arguments. By default (``None``) every converted array is
57
+ copied back, which is always correct but does more work.
58
+
59
+ Examples
60
+ --------
61
+ >>> from cunumpy.kernel import PyccelKernel
62
+ >>> kernel = PyccelKernel(my_pyccelized_function)
63
+ >>> kernel(out, x, y) # `out`, `x`, `y` may be NumPy or CuPy arrays
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ kernel: Callable[..., Any],
69
+ use_cupy: bool | None = None,
70
+ object_modules: Sequence[str] = (),
71
+ outputs: Sequence[int | str] | None = None,
72
+ ) -> None:
73
+ self._kernel = kernel
74
+ self._use_cupy = use_cupy
75
+ self._object_modules = tuple(object_modules)
76
+
77
+ if outputs is None:
78
+ self._outputs: tuple[int | str, ...] | None = None
79
+ else:
80
+ if isinstance(outputs, (int, str)):
81
+ raise TypeError(
82
+ "outputs must be a sequence of argument indices/names, "
83
+ f"not a bare {type(outputs).__name__} "
84
+ f"(did you mean outputs=({outputs!r},)?)"
85
+ )
86
+ for entry in outputs:
87
+ if not isinstance(entry, (int, str)) or isinstance(entry, bool):
88
+ raise TypeError(
89
+ "outputs entries must be argument indices (int) or "
90
+ f"names (str), got {entry!r}"
91
+ )
92
+ self._outputs = tuple(outputs)
93
+
94
+ def __repr__(self) -> str:
95
+ return (
96
+ f"PyccelKernel(kernel={self.name!r}, use_cupy={self.use_cupy!r}, "
97
+ f"outputs={self._outputs!r})"
98
+ )
99
+
100
+ def _convert_to_numpy(
101
+ self,
102
+ value: Any,
103
+ converted: list[tuple[Any, np.ndarray]],
104
+ memo: dict[int, Any],
105
+ ) -> Any:
106
+ """Recursively replace CuPy arrays in `value` by host copies.
107
+
108
+ Every replacement is appended to `converted` as a
109
+ ``(device_array, host_copy)`` pair.
110
+
111
+ `memo` maps ``id(original) -> converted`` and is shared across all
112
+ arguments of a single call. It serves two purposes: a device array
113
+ reachable by several paths is copied to the host exactly once (so the
114
+ kernel sees one shared array, as the caller intended, and the write-back
115
+ happens once), and reference cycles terminate instead of recursing
116
+ forever. Everything traversed here stays reachable from the caller's
117
+ arguments for the duration of the call, so the `id` keys cannot be
118
+ reused by unrelated objects.
119
+ """
120
+ key = id(value)
121
+ if key in memo:
122
+ return memo[key]
123
+
124
+ if array_api_compat.is_cupy_array(value):
125
+ value_np = to_numpy(value)
126
+ memo[key] = value_np
127
+ converted.append((value, value_np))
128
+ return value_np
129
+
130
+ if isinstance(value, tuple):
131
+ # A tuple cannot be memoized before its items are converted, but it
132
+ # can only take part in a cycle through a mutable container, and
133
+ # those are memoized before they are filled in below.
134
+ value_np = tuple(
135
+ self._convert_to_numpy(item, converted, memo) for item in value
136
+ )
137
+ memo[key] = value_np
138
+ return value_np
139
+
140
+ if isinstance(value, list):
141
+ value_np = []
142
+ memo[key] = value_np
143
+ value_np.extend(
144
+ self._convert_to_numpy(item, converted, memo) for item in value
145
+ )
146
+ return value_np
147
+
148
+ if isinstance(value, dict):
149
+ value_np = {}
150
+ memo[key] = value_np
151
+ for k, v in value.items():
152
+ value_np[k] = self._convert_to_numpy(v, converted, memo)
153
+ return value_np
154
+
155
+ if hasattr(value, "__dict__") and value.__class__.__module__.startswith(
156
+ self._object_modules
157
+ ):
158
+ # Shallow-copy the object so the caller's instance keeps pointing at
159
+ # its device arrays; only the copy holds the host views.
160
+ value_np = copy.copy(value)
161
+ memo[key] = value_np
162
+ for name, attr in vars(value).items():
163
+ setattr(value_np, name, self._convert_to_numpy(attr, converted, memo))
164
+ return value_np
165
+
166
+ return value
167
+
168
+ @staticmethod
169
+ def _convert_from_numpy(value: Any) -> Any:
170
+ """Move NumPy arrays returned by the kernel back to the device."""
171
+ if isinstance(value, np.ndarray):
172
+ return to_cupy(value)
173
+ if isinstance(value, tuple):
174
+ return tuple(PyccelKernel._convert_from_numpy(item) for item in value)
175
+ if isinstance(value, list):
176
+ return [PyccelKernel._convert_from_numpy(item) for item in value]
177
+ return value
178
+
179
+ def _collect_host_arrays(self, value: Any, found: set[int], seen: set[int]) -> None:
180
+ """Record the id of every host array reachable from `value`.
181
+
182
+ Runs over the *converted* arguments, using the same traversal rules as
183
+ :meth:`_convert_to_numpy`, so that an output declared as a container or
184
+ an object contributes the arrays nested inside it.
185
+ """
186
+ if isinstance(value, np.ndarray):
187
+ found.add(id(value))
188
+ return
189
+
190
+ if id(value) in seen:
191
+ return
192
+
193
+ if isinstance(value, (tuple, list)):
194
+ seen.add(id(value))
195
+ for item in value:
196
+ self._collect_host_arrays(item, found, seen)
197
+ return
198
+
199
+ if isinstance(value, dict):
200
+ seen.add(id(value))
201
+ for item in value.values():
202
+ self._collect_host_arrays(item, found, seen)
203
+ return
204
+
205
+ if hasattr(value, "__dict__") and value.__class__.__module__.startswith(
206
+ self._object_modules
207
+ ):
208
+ seen.add(id(value))
209
+ for attr in vars(value).values():
210
+ self._collect_host_arrays(attr, found, seen)
211
+
212
+ def _output_host_arrays(
213
+ self, args_np: list[Any], kwargs_np: dict[str, Any]
214
+ ) -> set[int]:
215
+ """Ids of the host arrays reachable from the declared output arguments.
216
+
217
+ Raises
218
+ ------
219
+ IndexError, KeyError
220
+ If a declared output does not correspond to an argument of this
221
+ call -- typically because an argument declared by index was passed
222
+ as a keyword, or vice versa.
223
+ """
224
+ found: set[int] = set()
225
+ seen: set[int] = set()
226
+
227
+ for entry in self._outputs or ():
228
+ if isinstance(entry, int):
229
+ index = entry + len(args_np) if entry < 0 else entry
230
+ if not 0 <= index < len(args_np):
231
+ raise IndexError(
232
+ f"{self.name}() was declared with output argument "
233
+ f"{entry}, but was called with {len(args_np)} "
234
+ "positional argument(s). Note that an output passed as "
235
+ "a keyword must be declared by name, not by index."
236
+ )
237
+ self._collect_host_arrays(args_np[index], found, seen)
238
+ else:
239
+ if entry not in kwargs_np:
240
+ raise KeyError(
241
+ f"{self.name}() was declared with output argument "
242
+ f"{entry!r}, but no such keyword argument was passed. "
243
+ "Note that an output passed positionally must be "
244
+ "declared by index, not by name."
245
+ )
246
+ self._collect_host_arrays(kwargs_np[entry], found, seen)
247
+
248
+ return found
249
+
250
+ def _contains_cupy(self, value: Any, seen: set[int] | None = None) -> bool:
251
+ """Whether `value` holds a CuPy array, following the same traversal
252
+ rules as :meth:`_convert_to_numpy`.
253
+
254
+ `seen` tracks already-visited containers so that reference cycles
255
+ terminate.
256
+ """
257
+ if array_api_compat.is_cupy_array(value):
258
+ return True
259
+
260
+ if seen is None:
261
+ seen = set()
262
+ if id(value) in seen:
263
+ return False
264
+
265
+ if isinstance(value, (tuple, list)):
266
+ seen.add(id(value))
267
+ return any(self._contains_cupy(item, seen) for item in value)
268
+
269
+ if isinstance(value, dict):
270
+ seen.add(id(value))
271
+ return any(self._contains_cupy(item, seen) for item in value.values())
272
+
273
+ if hasattr(value, "__dict__") and value.__class__.__module__.startswith(
274
+ self._object_modules
275
+ ):
276
+ seen.add(id(value))
277
+ return any(self._contains_cupy(attr, seen) for attr in vars(value).values())
278
+
279
+ return False
280
+
281
+ def _needs_conversion(self, args: tuple[Any, ...], kwargs: dict[str, Any]) -> bool:
282
+ if self._use_cupy is not None:
283
+ return self._use_cupy
284
+ if _cupy_backend():
285
+ return True
286
+ # The backend is NumPy, but individual CuPy arrays may still have been
287
+ # passed in explicitly.
288
+ return any(self._contains_cupy(value) for value in (*args, *kwargs.values()))
289
+
290
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
291
+ if not self._needs_conversion(args, kwargs):
292
+ return self._kernel(*args, **kwargs)
293
+
294
+ # Convert CuPy arrays in args/kwargs to NumPy arrays on the host. The
295
+ # memo is shared across args and kwargs so that an array passed more
296
+ # than once stays a single array on the host too.
297
+ converted: list[tuple[Any, np.ndarray]] = []
298
+ memo: dict[int, Any] = {}
299
+ args_np = [self._convert_to_numpy(x, converted, memo) for x in args]
300
+ kwargs_np = {
301
+ k: self._convert_to_numpy(v, converted, memo) for k, v in kwargs.items()
302
+ }
303
+
304
+ # Which arrays the kernel may have written to is resolved before the
305
+ # call, so a mis-declared output is reported even if the kernel itself
306
+ # would have raised first.
307
+ writeable = (
308
+ None
309
+ if self._outputs is None
310
+ else self._output_host_arrays(args_np, kwargs_np)
311
+ )
312
+
313
+ result = self._kernel(*args_np, **kwargs_np)
314
+
315
+ # Copy in-place kernel updates back to the device arrays.
316
+ for device_array, host_array in converted:
317
+ if writeable is None or id(host_array) in writeable:
318
+ device_array[...] = to_cupy(host_array)
319
+
320
+ return self._convert_from_numpy(result)
321
+
322
+ @property
323
+ def name(self) -> str:
324
+ """Name of the wrapped kernel."""
325
+ return getattr(self._kernel, "__name__", type(self._kernel).__name__)
326
+
327
+ @property
328
+ def kernel(self) -> Callable[..., Any]:
329
+ """The wrapped kernel."""
330
+ return self._kernel
331
+
332
+ @property
333
+ def use_cupy(self) -> bool:
334
+ """Whether calls currently convert between device and host arrays."""
335
+ if self._use_cupy is not None:
336
+ return self._use_cupy
337
+ return _cupy_backend()
338
+
339
+ @property
340
+ def object_modules(self) -> tuple[str, ...]:
341
+ """Module prefixes whose instances are traversed for arrays."""
342
+ return self._object_modules
343
+
344
+ @property
345
+ def outputs(self) -> tuple[int | str, ...] | None:
346
+ """Declared output arguments, or ``None`` if every array is copied back."""
347
+ return self._outputs
@@ -4,7 +4,8 @@ from contextlib import contextmanager
4
4
  from types import ModuleType
5
5
  from typing import TYPE_CHECKING, Any, Generator, Literal
6
6
 
7
- import numpy as np
7
+ import array_api_compat
8
+ import array_api_compat.numpy as np
8
9
 
9
10
  BackendType = Literal["numpy", "cupy"]
10
11
 
@@ -55,7 +56,7 @@ class ArrayBackend:
55
56
  def _load_backend(self, backend: BackendType, verbose: bool = False) -> ModuleType:
56
57
  if backend == "cupy":
57
58
  if cupy_available():
58
- import cupy as cp
59
+ import array_api_compat.cupy as cp
59
60
 
60
61
  self._backend = "cupy"
61
62
  return cp
@@ -66,10 +67,8 @@ class ArrayBackend:
66
67
  )
67
68
  self._backend = "numpy"
68
69
  return np
69
- import numpy as np_mod
70
-
71
70
  self._backend = "numpy"
72
- return np_mod
71
+ return np
73
72
 
74
73
  def __repr__(self) -> str:
75
74
  return f"ArrayBackend(backend={self._backend!r}, module={self._xp.__name__!r})"
@@ -166,7 +165,7 @@ def to_cupy(array: Any) -> Any:
166
165
  if not cupy_available():
167
166
  raise ImportError("CuPy is not available or not functional.")
168
167
 
169
- import cupy as cp
168
+ import array_api_compat.cupy as cp
170
169
 
171
170
  return cp.asarray(array)
172
171
 
@@ -180,8 +179,7 @@ def to_cunumpy(array: Any) -> Any:
180
179
 
181
180
  def get_backend(array: Any) -> BackendType:
182
181
  """Return 'cupy' or 'numpy' depending on the array type."""
183
- module = getattr(type(array), "__module__", "")
184
- return "cupy" if "cupy" in module else "numpy"
182
+ return "cupy" if array_api_compat.is_cupy_array(array) else "numpy"
185
183
 
186
184
 
187
185
  def is_gpu(array: Any) -> bool:
@@ -194,6 +192,34 @@ def is_cpu(array: Any) -> bool:
194
192
  return get_backend(array) == "numpy"
195
193
 
196
194
 
195
+ def same_backend(*arrays: Any) -> bool:
196
+ """Return True if all given arrays live on the same backend.
197
+
198
+ Trivially True for zero or one array.
199
+ """
200
+ if len(arrays) <= 1:
201
+ return True
202
+ backends = {get_backend(array) for array in arrays}
203
+ return len(backends) == 1
204
+
205
+
206
+ def assert_same_backend(*arrays: Any) -> None:
207
+ """Raise TypeError if the given arrays don't all live on the same backend.
208
+
209
+ Mixing NumPy and CuPy arrays in an operation typically fails with a
210
+ confusing, backend-internal error (e.g. a CuPy kernel dispatch error
211
+ complaining about an "unsupported type"). Call this upfront to fail
212
+ with a clear message instead. Use `to_cunumpy()`/`to_numpy()`/`to_cupy()`
213
+ to align mismatched arrays onto one backend first.
214
+ """
215
+ if not same_backend(*arrays):
216
+ backends = [get_backend(array) for array in arrays]
217
+ raise TypeError(
218
+ f"Arrays are on mismatched backends: {backends}. Use "
219
+ "xp.to_cunumpy()/xp.to_numpy()/xp.to_cupy() to align them first."
220
+ )
221
+
222
+
197
223
  # TYPE_CHECKING is True when type checking (e.g., mypy), but False at runtime.
198
224
  # This allows us to use autocompletion for xp (i.e., numpy/cupy) as if numpy was imported.
199
225
  if TYPE_CHECKING:
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: cunumpy
3
+ Version: 0.1.5
4
+ Summary: Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
5
+ Author: Max
6
+ Project-URL: Source, https://github.com/max-models/cunumpy
7
+ Keywords: python
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: array-api-compat
19
+ Requires-Dist: numpy
20
+ Provides-Extra: dev
21
+ Requires-Dist: black[jupyter]; extra == "dev"
22
+ Requires-Dist: isort; extra == "dev"
23
+ Requires-Dist: cunumpy[docs,test-compiled]; extra == "dev"
24
+ Provides-Extra: docs
25
+ Requires-Dist: ipykernel; extra == "docs"
26
+ Requires-Dist: myst-parser; extra == "docs"
27
+ Requires-Dist: nbconvert; extra == "docs"
28
+ Requires-Dist: nbsphinx; extra == "docs"
29
+ Requires-Dist: jupyterlab; extra == "docs"
30
+ Requires-Dist: pre-commit; extra == "docs"
31
+ Requires-Dist: pyproject-fmt; extra == "docs"
32
+ Requires-Dist: sphinx; extra == "docs"
33
+ Requires-Dist: sphinx-book-theme; extra == "docs"
34
+ Provides-Extra: test
35
+ Requires-Dist: coverage; extra == "test"
36
+ Requires-Dist: pytest; extra == "test"
37
+ Provides-Extra: test-compiled
38
+ Requires-Dist: cunumpy[test]; extra == "test-compiled"
39
+ Requires-Dist: pyccel; extra == "test-compiled"
40
+
41
+ # CuNumpy
42
+
43
+ Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
44
+
45
+ # Install
46
+
47
+ ```bash
48
+ pip install cunumpy
49
+ ```
50
+
51
+ Example usage:
52
+
53
+ ```
54
+ export ARRAY_BACKEND=cupy
55
+ ```
56
+
57
+ ```python
58
+ import cunumpy as xp
59
+
60
+ xp.set_backend("cupy")
61
+
62
+ arr = xp.array([1, 2])
63
+
64
+ print(f"{type(arr) = }")
65
+ print(f"{xp.__version__ = }")
66
+
67
+ # Convert to NumPy
68
+ arr_np = xp.to_numpy(arr)
69
+
70
+ # Convert to active backend
71
+ arr_xp = xp.to_cunumpy(arr)
72
+
73
+ # Inspect backend
74
+ print(f"{xp.get_backend(arr) = }")
75
+ print(f"{xp.is_gpu(arr) = }")
76
+ print(f"{xp.is_cpu(arr) = }")
77
+
78
+ # Temporarily switch backend
79
+ with xp.use_backend("numpy"):
80
+ # This code runs on CPU even if ARRAY_BACKEND=cupy
81
+ arr_cpu = xp.zeros(100)
82
+
83
+ # Set backend globally
84
+ xp.set_backend("cupy")
85
+
86
+ # Synchronize GPU operations (no-op on CPU)
87
+ xp.synchronize()
88
+ ```
89
+
90
+ Output:
91
+
92
+ ```
93
+ type(arr) = <class 'cupy.ndarray'>
94
+ xp.__version__ = '0.1.4'
95
+ xp.get_backend(arr) = 'cupy'
96
+ xp.is_gpu(arr) = True
97
+ xp.is_cpu(arr) = False
98
+ ```
99
+
100
+ # Pyodide
101
+
102
+ cuNumPy supports Pyodide with the NumPy backend. In an initialized Pyodide
103
+ JavaScript runtime, install the package and run a Python-source kernel:
104
+
105
+ ```javascript
106
+ await pyodide.loadPackage("micropip");
107
+ await pyodide.runPythonAsync(`
108
+ import micropip
109
+ await micropip.install("cunumpy")
110
+
111
+ import cunumpy as xp
112
+ xp.set_backend("numpy")
113
+
114
+ def scale(values, factor):
115
+ values[:] *= factor
116
+ return values
117
+
118
+ values = xp.array([1.0, 2.0, 3.0])
119
+ result = xp.PyccelKernel(scale)(values, 2.0)
120
+ assert result is values
121
+ print(xp.to_numpy(result)) # [2. 4. 6.]
122
+ `);
123
+ ```
124
+
125
+ NumPy is the default backend when `ARRAY_BACKEND` is unset. CuPy/CUDA and
126
+ native Pyccel compilation are not supported in Pyodide. Despite its name,
127
+ `PyccelKernel` accepts ordinary Python callables and neither imports Pyccel nor
128
+ compiles code. Applications must supply Python-source kernels with
129
+ Pyodide-compatible imports.
130
+
131
+ CI tests the built wheel in Pyodide's WebAssembly runtime under Node.js, including
132
+ array operations, conversions, mutation/aliasing, backend context restoration,
133
+ and Python kernels, while rejecting CuPy or Pyccel imports. This does not test
134
+ browser-specific integration such as page loading or workers. See the
135
+ [Pyodide guide](docs/source/pyodide.md) for details and local test commands.
136
+
137
+ # Development tests
138
+
139
+ ```bash
140
+ pip install -e '.[test]'
141
+ pytest tests/portable
142
+ ```
143
+
144
+ The `test` extra is compiler-free. For compiled-kernel tests, install
145
+ `'.[test-compiled]'` and a working native compiler, then run `pytest`.
146
+ The `dev` extra includes these compiled-test dependencies as before.
147
+
148
+ # Build docs
149
+
150
+
151
+ ```
152
+ make html
153
+ cd ../
154
+ open docs/_build/html/index.html
155
+ ```
@@ -2,6 +2,7 @@ README.md
2
2
  pyproject.toml
3
3
  src/cunumpy/__init__.py
4
4
  src/cunumpy/__init__.pyi
5
+ src/cunumpy/kernel.py
5
6
  src/cunumpy/main.py
6
7
  src/cunumpy/py.typed
7
8
  src/cunumpy/xp.py
@@ -1,9 +1,10 @@
1
+ array-api-compat
1
2
  numpy
2
3
 
3
4
  [dev]
4
5
  black[jupyter]
5
6
  isort
6
- cunumpy[docs,test]
7
+ cunumpy[docs,test-compiled]
7
8
 
8
9
  [docs]
9
10
  ipykernel
@@ -19,3 +20,7 @@ sphinx-book-theme
19
20
  [test]
20
21
  coverage
21
22
  pytest
23
+
24
+ [test-compiled]
25
+ cunumpy[test]
26
+ pyccel
cunumpy-0.1.3/PKG-INFO DELETED
@@ -1,91 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: cunumpy
3
- Version: 0.1.3
4
- Summary: Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
5
- Author: Max
6
- Project-URL: Source, https://github.com/max-models/cunumpy
7
- Keywords: python
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Programming Language :: Python :: 3 :: Only
10
- Classifier: Programming Language :: Python :: 3.8
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Requires-Python: >=3.8
17
- Description-Content-Type: text/markdown
18
- Requires-Dist: numpy
19
- Provides-Extra: dev
20
- Requires-Dist: black[jupyter]; extra == "dev"
21
- Requires-Dist: isort; extra == "dev"
22
- Requires-Dist: cunumpy[docs,test]; extra == "dev"
23
- Provides-Extra: docs
24
- Requires-Dist: ipykernel; extra == "docs"
25
- Requires-Dist: myst-parser; extra == "docs"
26
- Requires-Dist: nbconvert; extra == "docs"
27
- Requires-Dist: nbsphinx; extra == "docs"
28
- Requires-Dist: jupyterlab; extra == "docs"
29
- Requires-Dist: pre-commit; extra == "docs"
30
- Requires-Dist: pyproject-fmt; extra == "docs"
31
- Requires-Dist: sphinx; extra == "docs"
32
- Requires-Dist: sphinx-book-theme; extra == "docs"
33
- Provides-Extra: test
34
- Requires-Dist: coverage; extra == "test"
35
- Requires-Dist: pytest; extra == "test"
36
-
37
- # CuNumpy
38
-
39
- Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
40
-
41
- # Install
42
-
43
- ```bash
44
- pip install cunumpy
45
- ```
46
-
47
- Example usage:
48
-
49
- ```
50
- export ARRAY_BACKEND=cupy
51
- ```
52
-
53
- ```python
54
- import cunumpy as xp
55
-
56
- arr = xp.array([1, 2])
57
-
58
- print(type(arr))
59
- print(xp.__version__)
60
-
61
- # Convert to NumPy
62
- arr_np = xp.to_numpy(arr)
63
-
64
- # Convert to active backend
65
- arr_xp = xp.to_cunumpy(arr)
66
-
67
- # Inspect backend
68
- print(xp.get_backend(arr))
69
- print(xp.is_gpu(arr))
70
- print(xp.is_cpu(arr))
71
-
72
- # Temporarily switch backend
73
- with xp.use_backend("numpy"):
74
- # This code runs on CPU even if ARRAY_BACKEND=cupy
75
- arr_cpu = xp.zeros(100)
76
-
77
- # Set backend globally
78
- xp.set_backend("cupy")
79
-
80
- # Synchronize GPU operations (no-op on CPU)
81
- xp.synchronize()
82
- ```
83
-
84
- # Build docs
85
-
86
-
87
- ```
88
- make html
89
- cd ../
90
- open docs/_build/html/index.html
91
- ```
cunumpy-0.1.3/README.md DELETED
@@ -1,55 +0,0 @@
1
- # CuNumpy
2
-
3
- Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
4
-
5
- # Install
6
-
7
- ```bash
8
- pip install cunumpy
9
- ```
10
-
11
- Example usage:
12
-
13
- ```
14
- export ARRAY_BACKEND=cupy
15
- ```
16
-
17
- ```python
18
- import cunumpy as xp
19
-
20
- arr = xp.array([1, 2])
21
-
22
- print(type(arr))
23
- print(xp.__version__)
24
-
25
- # Convert to NumPy
26
- arr_np = xp.to_numpy(arr)
27
-
28
- # Convert to active backend
29
- arr_xp = xp.to_cunumpy(arr)
30
-
31
- # Inspect backend
32
- print(xp.get_backend(arr))
33
- print(xp.is_gpu(arr))
34
- print(xp.is_cpu(arr))
35
-
36
- # Temporarily switch backend
37
- with xp.use_backend("numpy"):
38
- # This code runs on CPU even if ARRAY_BACKEND=cupy
39
- arr_cpu = xp.zeros(100)
40
-
41
- # Set backend globally
42
- xp.set_backend("cupy")
43
-
44
- # Synchronize GPU operations (no-op on CPU)
45
- xp.synchronize()
46
- ```
47
-
48
- # Build docs
49
-
50
-
51
- ```
52
- make html
53
- cd ../
54
- open docs/_build/html/index.html
55
- ```
@@ -1,91 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: cunumpy
3
- Version: 0.1.3
4
- Summary: Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
5
- Author: Max
6
- Project-URL: Source, https://github.com/max-models/cunumpy
7
- Keywords: python
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Programming Language :: Python :: 3 :: Only
10
- Classifier: Programming Language :: Python :: 3.8
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Requires-Python: >=3.8
17
- Description-Content-Type: text/markdown
18
- Requires-Dist: numpy
19
- Provides-Extra: dev
20
- Requires-Dist: black[jupyter]; extra == "dev"
21
- Requires-Dist: isort; extra == "dev"
22
- Requires-Dist: cunumpy[docs,test]; extra == "dev"
23
- Provides-Extra: docs
24
- Requires-Dist: ipykernel; extra == "docs"
25
- Requires-Dist: myst-parser; extra == "docs"
26
- Requires-Dist: nbconvert; extra == "docs"
27
- Requires-Dist: nbsphinx; extra == "docs"
28
- Requires-Dist: jupyterlab; extra == "docs"
29
- Requires-Dist: pre-commit; extra == "docs"
30
- Requires-Dist: pyproject-fmt; extra == "docs"
31
- Requires-Dist: sphinx; extra == "docs"
32
- Requires-Dist: sphinx-book-theme; extra == "docs"
33
- Provides-Extra: test
34
- Requires-Dist: coverage; extra == "test"
35
- Requires-Dist: pytest; extra == "test"
36
-
37
- # CuNumpy
38
-
39
- Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`.
40
-
41
- # Install
42
-
43
- ```bash
44
- pip install cunumpy
45
- ```
46
-
47
- Example usage:
48
-
49
- ```
50
- export ARRAY_BACKEND=cupy
51
- ```
52
-
53
- ```python
54
- import cunumpy as xp
55
-
56
- arr = xp.array([1, 2])
57
-
58
- print(type(arr))
59
- print(xp.__version__)
60
-
61
- # Convert to NumPy
62
- arr_np = xp.to_numpy(arr)
63
-
64
- # Convert to active backend
65
- arr_xp = xp.to_cunumpy(arr)
66
-
67
- # Inspect backend
68
- print(xp.get_backend(arr))
69
- print(xp.is_gpu(arr))
70
- print(xp.is_cpu(arr))
71
-
72
- # Temporarily switch backend
73
- with xp.use_backend("numpy"):
74
- # This code runs on CPU even if ARRAY_BACKEND=cupy
75
- arr_cpu = xp.zeros(100)
76
-
77
- # Set backend globally
78
- xp.set_backend("cupy")
79
-
80
- # Synchronize GPU operations (no-op on CPU)
81
- xp.synchronize()
82
- ```
83
-
84
- # Build docs
85
-
86
-
87
- ```
88
- make html
89
- cd ../
90
- open docs/_build/html/index.html
91
- ```
File without changes
File without changes
File without changes