From 14fb37038524bd7b39ff90b10c29e04ae24ec0ad Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 10 Oct 2026 15:12:10 +0200 Subject: [PATCH] Fake cupyx.scipy: copy and deepcopy of proxy objects Splines of the fake cupyx.scipy are proxies that hold the fake CuPy module, so copy.deepcopy (e.g. of an object that stores its constructor arguments) failed with "cannot pickle 'module' object", while real CuPy splines can be copied. Copies now wrap a copy of the SciPy object and share the fake CuPy module. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ src/cunumpy/_fake_cupyx.py | 9 +++++++++ tests/unit/test_fake_cupyx.py | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bff2a62..41b0fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 warnings as errors. Docstring style: `docs/source/contributing.md`. ### Fixed +- Splines and other objects of the fake `cupyx.scipy` can be copied with + `copy.copy` and `copy.deepcopy` (also inside a copied container), as real + CuPy splines can. - `xp.algorithms.segment_sum` and `SegmentPlan.sum` work on the fake CuPy: it cannot compile the CUDA kernel, so the sum runs on the host buffers of the fake arrays and the CuPy path of a program runs without a GPU. diff --git a/src/cunumpy/_fake_cupyx.py b/src/cunumpy/_fake_cupyx.py index 0a85e11..2ee28e6 100644 --- a/src/cunumpy/_fake_cupyx.py +++ b/src/cunumpy/_fake_cupyx.py @@ -21,6 +21,7 @@ from __future__ import annotations +import copy import importlib import sys import types @@ -263,6 +264,14 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: def __repr__(self) -> str: return f"" + # copies wrap a copy of the SciPy object and share the fake CuPy module (a + # module cannot be copied), as real CuPy splines can be copied + def __copy__(self) -> _Proxy: + return _Proxy(copy.copy(self._obj), self._cupy) + + def __deepcopy__(self, memo: dict) -> _Proxy: + return _Proxy(copy.deepcopy(self._obj, memo), self._cupy) + class _ProxyClass: """A SciPy class seen as a CuPy class: instances are :class:`_Proxy`.""" diff --git a/tests/unit/test_fake_cupyx.py b/tests/unit/test_fake_cupyx.py index f392ad2..36b1870 100644 --- a/tests/unit/test_fake_cupyx.py +++ b/tests/unit/test_fake_cupyx.py @@ -48,6 +48,26 @@ def test_splines_fit_and_evaluate_on_the_device(): assert "ok" in run(code) +def test_splines_can_be_copied(): + code = """ +import copy +import numpy as np +import cunumpy as xp +xp.set_backend("cupy") +x = xp.linspace(0.0, 1.0, 11) +spl = xp.scipy.interpolate.UnivariateSpline(x, xp.sin(3 * x), k=3, s=0.0, ext=3) +for c in (copy.copy(spl), copy.deepcopy(spl), copy.deepcopy({"spline": spl})["spline"]): + assert isinstance(c, xp.scipy.interpolate.UnivariateSpline) + assert xp.is_gpu(c(x)) + assert np.array_equal(xp.to_numpy(c(x)), xp.to_numpy(spl(x))) +# a deep copy is independent of the original +c = copy.deepcopy(spl) +assert c._obj is not spl._obj +print("ok") +""" + assert "ok" in run(code) + + def test_host_arrays_are_rejected_like_cupy(): code = """ import numpy as np