From e39bbbd92646f5106fbb4b6a2a9a9e2606276b9c Mon Sep 17 00:00:00 2001 From: "Todd A. Anderson" Date: Fri, 16 Apr 2021 17:07:40 -0500 Subject: [PATCH 1/4] support inplace ops in array_ufunc --- dpctl/dptensor/numpy_usm_shared.py | 21 +++++++++++++-------- dpctl/tests/test_dparray.py | 3 +++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/dpctl/dptensor/numpy_usm_shared.py b/dpctl/dptensor/numpy_usm_shared.py index e15d537222..6afc6ff10b 100644 --- a/dpctl/dptensor/numpy_usm_shared.py +++ b/dpctl/dptensor/numpy_usm_shared.py @@ -78,6 +78,15 @@ def _get_usm_base(ary): return None +def convert_ndarray_to_np_ndarray(x): + if isinstance(x, ndarray): + return np.ndarray(x.shape, x.dtype, x) + elif isinstance(x, tuple): + return tuple([convert_ndarray_to_np_ndarray(y) for y in x]) + else: + return x + + class ndarray(np.ndarray): """ numpy.ndarray subclass whose underlying memory buffer is allocated @@ -267,7 +276,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): # USM memory. However, if kwarg has numpy_usm_shared-typed out then # array_ufunc is called recursively so we cast out as regular # NumPy ndarray (having a USM data pointer). - if kwargs.get("out", None) is None: + out_arg = kwargs.get("out", None) + if out_arg is None: # maybe copy? # deal with multiple returned arrays, so kwargs['out'] can be tuple res_type = np.result_type(*typing) @@ -277,13 +287,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): else: # If they manually gave numpy_usm_shared as out kwarg then we # have to also cast as regular NumPy ndarray to avoid recursion. - if isinstance(kwargs["out"], ndarray): - out = kwargs["out"] - kwargs["out"] = np.ndarray(out.shape, out.dtype, out) - else: - out = kwargs["out"] - ret = ufunc(*scalars, **kwargs) - return out + kwargs["out"] = convert_ndarray_to_np_ndarray(out_arg) + return ufunc(*scalars, **kwargs) else: return NotImplemented diff --git a/dpctl/tests/test_dparray.py b/dpctl/tests/test_dparray.py index 9938a5dbc6..43ab23e22d 100644 --- a/dpctl/tests/test_dparray.py +++ b/dpctl/tests/test_dparray.py @@ -47,6 +47,9 @@ def test_multiplication_dparray(self): C = self.X * 5 self.assertIsInstance(C, dparray.ndarray) + def test_inplace_sub(self): + self.X -= 1 + def test_dparray_through_python_func(self): def func_operation_with_const(dpctl_array): return dpctl_array * 2.0 + 13 From fe945ccebcfe2bdf6f6b2a3308894427ba21cc3b Mon Sep 17 00:00:00 2001 From: "Todd A. Anderson" Date: Fri, 16 Apr 2021 19:13:53 -0500 Subject: [PATCH 2/4] implement array_ufunc reduce --- dpctl/dptensor/numpy_usm_shared.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dpctl/dptensor/numpy_usm_shared.py b/dpctl/dptensor/numpy_usm_shared.py index 6afc6ff10b..9ddb659701 100644 --- a/dpctl/dptensor/numpy_usm_shared.py +++ b/dpctl/dptensor/numpy_usm_shared.py @@ -289,6 +289,30 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): # have to also cast as regular NumPy ndarray to avoid recursion. kwargs["out"] = convert_ndarray_to_np_ndarray(out_arg) return ufunc(*scalars, **kwargs) + elif method == "reduce": + N = None + scalars = [] + typing = [] + for inp in inputs: + if isinstance(inp, Number): + scalars.append(inp) + typing.append(inp) + elif isinstance(inp, (self.__class__, np.ndarray)): + if isinstance(inp, self.__class__): + scalars.append(np.ndarray(inp.shape, inp.dtype, inp)) + typing.append(np.ndarray(inp.shape, inp.dtype)) + else: + scalars.append(inp) + typing.append(inp) + if N is not None: + if N != inp.shape: + raise TypeError("inconsistent sizes") + else: + N = inp.shape + else: + return NotImplemented + assert("out" not in kwargs) + return super().__array_ufunc__(ufunc, method, *scalars, **kwargs) else: return NotImplemented From 289f444f710a6915542cdf0648c15a71bf1837d0 Mon Sep 17 00:00:00 2001 From: "Todd A. Anderson" Date: Fri, 16 Apr 2021 20:53:52 -0500 Subject: [PATCH 3/4] black fixes --- dpctl/dptensor/numpy_usm_shared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpctl/dptensor/numpy_usm_shared.py b/dpctl/dptensor/numpy_usm_shared.py index 9ddb659701..9b6dcdbaf9 100644 --- a/dpctl/dptensor/numpy_usm_shared.py +++ b/dpctl/dptensor/numpy_usm_shared.py @@ -311,7 +311,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): N = inp.shape else: return NotImplemented - assert("out" not in kwargs) + assert "out" not in kwargs return super().__array_ufunc__(ufunc, method, *scalars, **kwargs) else: return NotImplemented From ada9420f90f4c054b4d02a773e7a2a5e829657fc Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk Date: Tue, 27 Apr 2021 12:49:05 -0500 Subject: [PATCH 4/4] Fixed introduced test failures, and fixed unbounded recursion noted on PR. Added few more tests pertaining to out keyword use --- dpctl/dptensor/numpy_usm_shared.py | 34 ++++++++++++++++++++++-------- dpctl/tests/test_dparray.py | 15 +++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/dpctl/dptensor/numpy_usm_shared.py b/dpctl/dptensor/numpy_usm_shared.py index 9b6dcdbaf9..21355bb3de 100644 --- a/dpctl/dptensor/numpy_usm_shared.py +++ b/dpctl/dptensor/numpy_usm_shared.py @@ -78,11 +78,15 @@ def _get_usm_base(ary): return None -def convert_ndarray_to_np_ndarray(x): +def convert_ndarray_to_np_ndarray(x, require_ndarray=False): if isinstance(x, ndarray): - return np.ndarray(x.shape, x.dtype, x) + return np.array(x, copy=False, subok=False) elif isinstance(x, tuple): - return tuple([convert_ndarray_to_np_ndarray(y) for y in x]) + return tuple( + convert_ndarray_to_np_ndarray(y, require_ndarray=require_ndarray) for y in x + ) + elif require_ndarray: + raise TypeError else: return x @@ -243,7 +247,7 @@ def __array_finalize__(self, obj): # Convert to a NumPy ndarray. def as_ndarray(self): - return np.copy(np.ndarray(self.shape, self.dtype, self)) + return np.array(self, copy=True, subok=False) def __array__(self): return self @@ -281,14 +285,22 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): # maybe copy? # deal with multiple returned arrays, so kwargs['out'] can be tuple res_type = np.result_type(*typing) - out = empty(inputs[0].shape, dtype=res_type) - out_as_np = np.ndarray(out.shape, out.dtype, out) + out_arg = empty(inputs[0].shape, dtype=res_type) + out_as_np = convert_ndarray_to_np_ndarray(out_arg) kwargs["out"] = out_as_np else: # If they manually gave numpy_usm_shared as out kwarg then we # have to also cast as regular NumPy ndarray to avoid recursion. - kwargs["out"] = convert_ndarray_to_np_ndarray(out_arg) - return ufunc(*scalars, **kwargs) + try: + kwargs["out"] = convert_ndarray_to_np_ndarray( + out_arg, require_ndarray=True + ) + except TypeError: + raise TypeError( + "Return arrays must each be {}".format(self.__class__) + ) + ufunc(*scalars, **kwargs) + return out_arg elif method == "reduce": N = None scalars = [] @@ -324,7 +336,11 @@ def __array_function__(self, func, types, args, kwargs): cm = sys.modules[__name__] affunc = getattr(cm, fname) fargs = [x.view(np.ndarray) if isinstance(x, ndarray) else x for x in args] - return affunc(*fargs, **kwargs) + fkwargs = { + key: convert_ndarray_to_np_ndarray(val) for key, val in kwargs.items() + } + res = affunc(*fargs, **fkwargs) + return kwargs["out"] if "out" in kwargs else res return NotImplemented diff --git a/dpctl/tests/test_dparray.py b/dpctl/tests/test_dparray.py index 43ab23e22d..76e5c8a0d9 100644 --- a/dpctl/tests/test_dparray.py +++ b/dpctl/tests/test_dparray.py @@ -61,6 +61,7 @@ def func_operation_with_const(dpctl_array): def test_dparray_mixing_dpctl_and_numpy(self): dp_numpy = numpy.ones((256, 4), dtype="d") res = dp_numpy * self.X + self.assertIsInstance(self.X, dparray.ndarray) self.assertIsInstance(res, dparray.ndarray) def test_dparray_shape(self): @@ -79,6 +80,20 @@ def test_numpy_sum_with_dparray(self): res = numpy.sum(self.X) self.assertEqual(res, 1024.0) + def test_numpy_sum_with_dparray_out(self): + res = dparray.empty((self.X.shape[1],), dtype=self.X.dtype) + res2 = numpy.sum(self.X, axis=0, out=res) + self.assertTrue(res is res2) + self.assertIsInstance(res2, dparray.ndarray) + + def test_frexp_with_out(self): + X = dparray.array([0.5, 4.7]) + mant = dparray.empty((2,), dtype="d") + exp = dparray.empty((2,), dtype="i4") + res = numpy.frexp(X, out=(mant, exp)) + self.assertTrue(res[0] is mant) + self.assertTrue(res[1] is exp) + if __name__ == "__main__": unittest.main()