diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a781484acb..8f47f2194c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ This release is compatible with NumPy 2.5. * Fixed scalar conversion of size-one `dpnp.tensor.usm_ndarray` (e.g. `int()`, `float()`, indexing) which failed with NumPy 2.5 after the in-place `ndarray.shape` assignment was deprecated [#2958](https://github.com/IntelPython/dpnp/pull/2958) * Fixed `dpnp.mgrid` and `dpnp.ogrid` to return consistent results between single-slice and tuple-of-slices syntax when the step is a complex number with a non-integer magnitude (e.g. `2.5j`) [#2971](https://github.com/IntelPython/dpnp/pull/2971) * Fixed `icx`/`icpx` warning during `conda build` by stripping the GCC-only `-fno-merge-constants` flag injected by conda-forge into `CFLAGS`/`CXXFLAGS` [#2978](https://github.com/IntelPython/dpnp/pull/2978) +* Fixed `dpnp.asnumpy` and `dpnp.ndarray.asnumpy` ignoring the `order` keyword, which caused a non-contiguous source array to be returned with a non-contiguous layout even when `order="C"` was requested [#2980](https://github.com/IntelPython/dpnp/pull/2980) ### Security diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index ea3f1904289..cd78c4556ae 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -920,11 +920,21 @@ def argsort( self, axis, kind, order, descending=descending, stable=stable ) - def asnumpy(self): + def asnumpy(self, order="C"): """ Copy content of the array into :class:`numpy.ndarray` instance of the same shape and data type. + Parameters + ---------- + order : {None, 'C', 'F', 'A', 'K'}, optional + The desired memory layout of the converted array. + When `order` is ``'A'``, it uses ``'F'`` if the array is + column-major and uses ``'C'`` otherwise. And when `order` is + ``'K'``, it keeps strides as closely as possible. + + Default: ``'C'``. + Returns ------- out : numpy.ndarray @@ -933,7 +943,7 @@ def asnumpy(self): """ - return dpt.asnumpy(self._array_obj) + return dpt.asnumpy(self._array_obj, order=order) def astype( self, diff --git a/dpnp/dpnp_iface.py b/dpnp/dpnp_iface.py index ffe947f89dd..00f8bf225db 100644 --- a/dpnp/dpnp_iface.py +++ b/dpnp/dpnp_iface.py @@ -132,10 +132,10 @@ def asnumpy(a, order="C"): """ if isinstance(a, dpnp_array): - return a.asnumpy() + return a.asnumpy(order=order) if isinstance(a, dpt.usm_ndarray): - return dpt.asnumpy(a) + return dpt.asnumpy(a, order=order) return numpy.asarray(a, order=order) diff --git a/dpnp/tensor/_copy_utils.py b/dpnp/tensor/_copy_utils.py index 39c4e9cf9fe..ea1fdfd0b07 100644 --- a/dpnp/tensor/_copy_utils.py +++ b/dpnp/tensor/_copy_utils.py @@ -51,11 +51,14 @@ int32_t_max = 1 + np.iinfo(np.int32).max -def _copy_to_numpy(ary): +def _copy_to_numpy(ary, order="K"): if not isinstance(ary, dpt.usm_ndarray): raise TypeError(f"Expected dpnp.tensor.usm_ndarray, got {type(ary)}") if ary.size == 0: - # no data needs to be copied for zero sized array + # No data needs to be copied for a zero-sized array. A zero-sized + # array is both C- and F-contiguous regardless of ``order``, and + # ``numpy.ndarray`` officially only accepts ``"C"``/``"F"``, so + # ``order`` is intentionally not forwarded here. return np.ndarray(ary.shape, dtype=ary.dtype) nb = ary.usm_data.nbytes q = ary.sycl_queue @@ -67,13 +70,16 @@ def _copy_to_numpy(ary): # ensure that content of ary.usm_data is final q.wait() hh.copy_from_device(ary.usm_data) - return np.ndarray( + result = np.ndarray( ary.shape, dtype=ary.dtype, buffer=h, strides=strides_bytes, offset=offset, ) + # apply the requested memory layout; ``"K"`` preserves the strides of the + # source array as closely as possible and is the default + return np.asarray(result, order=order) def _copy_from_numpy(np_ary, usm_type="device", sycl_queue=None): @@ -594,9 +600,9 @@ def to_numpy(usm_ary, /): return _copy_to_numpy(usm_ary) -def asnumpy(usm_ary): +def asnumpy(usm_ary, order="K"): """ - asnumpy(usm_ary) + asnumpy(usm_ary, order="K") Copies content of :class:`dpctl.tensor.usm_ndarray` instance ``usm_ary`` into :class:`numpy.ndarray` instance of the same shape and same data @@ -605,12 +611,17 @@ def asnumpy(usm_ary): Args: usm_ary (usm_ndarray): Input array + order ({None, ``"C"``, ``"F"``, ``"A"``, ``"K"``}, optional): + The desired memory layout of the returned array. ``None`` does + not enforce any particular layout. + Default: ``"K"``, which keeps the strides of ``usm_ary`` as + closely as possible. Returns: :class:`numpy.ndarray`: An instance of :class:`numpy.ndarray` populated with content of ``usm_ary`` """ - return _copy_to_numpy(usm_ary) + return _copy_to_numpy(usm_ary, order=order) class Dummy: diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index 9bcdf906b9c..b34410f308b 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -143,6 +143,99 @@ def test_roundtrip_binary_str(self, order): assert_array_equal(b, a.asnumpy().flatten(order)) +class TestAsNumpy: + # gh-2884: ``order`` keyword was ignored by ``dpnp.asnumpy`` and the + # resulting NumPy array kept the (possibly non-contiguous) layout of the + # source array. + orders = ["C", "F", "A", "K", None] + + def _f_contiguous_array(self): + # transposing a C-contiguous 2-D array yields an F-contiguous view + a = dpnp.arange(21, dtype="int32").reshape(7, 3).T + assert not a.flags["C_CONTIGUOUS"] and a.flags["F_CONTIGUOUS"] + return a + + def _c_contiguous_array(self): + a = dpnp.arange(21, dtype="int32").reshape(3, 7) + assert a.flags["C_CONTIGUOUS"] + return a + + @pytest.mark.parametrize("order", orders) + @pytest.mark.parametrize("layout", ["c", "f"]) + def test_iface_order(self, layout, order): + a = ( + self._c_contiguous_array() + if layout == "c" + else (self._f_contiguous_array()) + ) + result = dpnp.asnumpy(a, order=order) + + # numpy.asarray on the host copy is the reference for every order value + expected = numpy.asarray(a.asnumpy(order="K"), order=order) + assert isinstance(result, numpy.ndarray) + assert result.flags["C_CONTIGUOUS"] == expected.flags["C_CONTIGUOUS"] + assert result.flags["F_CONTIGUOUS"] == expected.flags["F_CONTIGUOUS"] + assert_array_equal(result, expected) + + def test_iface_default_order_is_c(self): + a = self._f_contiguous_array() + result = dpnp.asnumpy(a) + assert result.flags["C_CONTIGUOUS"] + + @pytest.mark.parametrize("order", orders) + def test_method_order(self, order): + a = self._f_contiguous_array() + result = a.asnumpy(order=order) + expected = numpy.asarray(a.asnumpy(order="K"), order=order) + assert result.flags["C_CONTIGUOUS"] == expected.flags["C_CONTIGUOUS"] + assert result.flags["F_CONTIGUOUS"] == expected.flags["F_CONTIGUOUS"] + assert_array_equal(result, expected) + + def test_method_default_order_is_c(self): + # the array method matches ``cupy.ndarray.get`` and defaults to "C" + a = self._f_contiguous_array() + result = a.asnumpy() + assert result.flags["C_CONTIGUOUS"] + + def test_method_order_k_keeps_strides(self): + # explicit "K" keeps the strides of the source as closely as possible + a = self._f_contiguous_array() + result = a.asnumpy(order="K") + assert not result.flags["C_CONTIGUOUS"] + + @pytest.mark.parametrize("order", orders) + def test_usm_ndarray_input_order(self, order): + a = self._f_contiguous_array() + usm_a = dpnp.get_usm_ndarray(a) + result = dpnp.asnumpy(usm_a, order=order) + assert_array_equal(result, dpt.asnumpy(usm_a, order=order)) + + @pytest.mark.parametrize("order", orders) + @pytest.mark.parametrize("shape", [(0, 3), (3, 0)]) + def test_empty_array(self, shape, order): + # zero-sized arrays are both C- and F-contiguous for any order value + a = dpnp.empty(shape, dtype="int32") + result = dpnp.asnumpy(a, order=order) + assert result.shape == shape + assert result.dtype == a.dtype + assert result.flags["C_CONTIGUOUS"] and result.flags["F_CONTIGUOUS"] + assert_array_equal(result, a.asnumpy(order=order)) + + def test_negative_stride(self): + # a reversed view has a negative stride; "K" preserves it (matching + # ``dpnp.tensor.asnumpy``) while "C" returns a C-contiguous copy + a = dpnp.arange(10, dtype="int32")[::-1] + usm_a = dpnp.get_usm_ndarray(a) + + result_k = dpnp.asnumpy(a, order="K") + assert_array_equal(result_k, dpt.asnumpy(usm_a)) + assert result_k.strides == dpt.asnumpy(usm_a).strides + + result_c = dpnp.asnumpy(a, order="C") + assert result_c.flags["C_CONTIGUOUS"] + assert_array_equal(result_c, result_k) + + class TestToFile: def _create_data(self): x = generate_random_numpy_array((2, 4, 3), dtype=complex)