From a588ce26eae5f8e46c61e9c543c05ac5734690c6 Mon Sep 17 00:00:00 2001 From: abagusetty Date: Fri, 26 Jun 2026 17:30:12 -0500 Subject: [PATCH 1/6] Fix dpnp.asnumpy ignoring the order keyword (gh-2884) dpnp.asnumpy and dpnp.ndarray.asnumpy accepted an order argument but dropped it for dpnp_array and usm_ndarray inputs. The underlying _copy_to_numpy rebuilt the NumPy array from the source strides, so a non-contiguous source was returned with a non-contiguous layout even when order='C' was requested, diverging from NumPy/CuPy. Thread order through the full conversion chain: - _copy_to_numpy(ary, order='K'): apply layout via np.asarray - dpt.asnumpy(usm_ary, order='K'): forward order - dpnp_array.asnumpy(order='C'): forward order - dpnp.asnumpy(a, order='C'): pass order to both array branches Public APIs default to 'C' to match NumPy/CuPy; internal helpers default to 'K' to preserve the existing stride-keeping behavior of their direct callers (to_numpy, _ctors, _print). Add TestAsNumpy covering iface/method/usm_ndarray paths and default semantics. --- CHANGELOG.md | 1 + dpnp/dpnp_array.py | 14 ++++++++-- dpnp/dpnp_iface.py | 4 +-- dpnp/tensor/_copy_utils.py | 19 +++++++++----- dpnp/tests/test_ndarray.py | 53 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fba11c9b713..ea48362cbb21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ This release is compatible with NumPy 2.4.5. * Fixed boolean mask indexing to raise `IndexError` when mask dimensions don't match the indexed array dimensions, aligning with NumPy behavior. Previously, incompatible boolean masks silently returned incorrect results instead of raising an error [#2929](https://github.com/IntelPython/dpnp/pull/2929) * Fixed a bug in `astype` where casting floating point types to unsigned integral types could cause an intermediate signed integral type to overflow, leading to incorrect results [#2930](https://github.com/IntelPython/dpnp/pull/2930) * Fixed incorrect `dpnp.tensor.expm1` result for `complex(±0, 0)` special case on CPU to match the Python Array API specification [#2926](https://github.com/IntelPython/dpnp/pull/2926) +* 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 [gh-2884](https://github.com/IntelPython/dpnp/issues/2884) ### Security diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 899379e837eb..68e69af785f1 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 c9d16a20e83d..2cd82575d246 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 39c4e9cf9fef..50a55d037c71 100644 --- a/dpnp/tensor/_copy_utils.py +++ b/dpnp/tensor/_copy_utils.py @@ -51,12 +51,12 @@ 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 - return np.ndarray(ary.shape, dtype=ary.dtype) + return np.ndarray(ary.shape, dtype=ary.dtype, order=order) nb = ary.usm_data.nbytes q = ary.sycl_queue hh = dpm.MemoryUSMHost(nb, queue=q) @@ -67,13 +67,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 +597,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 +608,16 @@ def asnumpy(usm_ary): Args: usm_ary (usm_ndarray): Input array + order (``"C"``, ``"F"``, ``"A"``, ``"K"``): + The desired memory layout of the returned array. + 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 5a848c9660fc..6164b216206d 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -136,6 +136,59 @@ 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. + def _non_c_contiguous_array(self): + # transposing a C-contiguous 2-D array yields a non-C-contiguous view + a = dpnp.arange(21, dtype="int32").reshape(7, 3).T + assert not a.flags["C_CONTIGUOUS"] + return a + + @pytest.mark.parametrize("order", ["C", "F"]) + def test_iface_order(self, order): + a = self._non_c_contiguous_array() + result = dpnp.asnumpy(a, order=order) + + expected = numpy.asarray(a.asnumpy(), order=order) + assert isinstance(result, numpy.ndarray) + assert result.flags[f"{order}_CONTIGUOUS"] + assert_array_equal(result, expected) + + def test_iface_default_order_is_c(self): + a = self._non_c_contiguous_array() + result = dpnp.asnumpy(a) + assert result.flags["C_CONTIGUOUS"] + + @pytest.mark.parametrize("order", ["C", "F"]) + def test_method_order(self, order): + a = self._non_c_contiguous_array() + result = a.asnumpy(order=order) + assert result.flags[f"{order}_CONTIGUOUS"] + assert_array_equal(result, a.asnumpy(order="K")) + + def test_method_default_order_is_c(self): + # the array method matches ``cupy.ndarray.get`` and defaults to "C" + a = self._non_c_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._non_c_contiguous_array() + result = a.asnumpy(order="K") + assert not result.flags["C_CONTIGUOUS"] + + @pytest.mark.parametrize("order", ["C", "F"]) + def test_usm_ndarray_input_order(self, order): + a = self._non_c_contiguous_array() + usm_a = dpnp.get_usm_ndarray(a) + result = dpnp.asnumpy(usm_a, order=order) + assert result.flags[f"{order}_CONTIGUOUS"] + assert_array_equal(result, dpt.asnumpy(usm_a, order=order)) + + class TestToFile: def _create_data(self): x = generate_random_numpy_array((2, 4, 3), dtype=complex) From 3d0a3310df1a833bee7b27c66a75c271d9fc966b Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty <59661409+abagusetty@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:55:04 -0500 Subject: [PATCH 2/6] Update CHANGELOG.md Co-authored-by: Anton <100830759+antonwolfy@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcbc39ebc564..2b2868fd71b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ This release is compatible with NumPy 2.5. * Fixed `dpnp.linalg.svd(..., hermitian=True)` returning a non-unitary `vh` for singular input arrays due to a zero sign appearing [#2954](https://github.com/IntelPython/dpnp/pull/2954) * 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 `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 [#2884](https://github.com/IntelPython/dpnp/issues/2884) +* 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/issues/2980) ### Security From aa93acc95300a8fa23e1ff90352d988d2ae60a57 Mon Sep 17 00:00:00 2001 From: abagusetty Date: Tue, 30 Jun 2026 10:11:28 -0500 Subject: [PATCH 3/6] Address review: skip order on empty path, doc None/optional, test A/None/empty --- dpnp/tensor/_copy_utils.py | 12 +++++--- dpnp/tests/test_ndarray.py | 60 ++++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/dpnp/tensor/_copy_utils.py b/dpnp/tensor/_copy_utils.py index 50a55d037c71..ea1fdfd0b07a 100644 --- a/dpnp/tensor/_copy_utils.py +++ b/dpnp/tensor/_copy_utils.py @@ -55,8 +55,11 @@ 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 - return np.ndarray(ary.shape, dtype=ary.dtype, order=order) + # 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 hh = dpm.MemoryUSMHost(nb, queue=q) @@ -608,8 +611,9 @@ def asnumpy(usm_ary, order="K"): Args: usm_ary (usm_ndarray): Input array - order (``"C"``, ``"F"``, ``"A"``, ``"K"``): - The desired memory layout of the returned 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: diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index 9fec4947f3ef..42c0198cf5af 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -147,54 +147,78 @@ 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. - def _non_c_contiguous_array(self): - # transposing a C-contiguous 2-D array yields a non-C-contiguous view + 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"] + 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", ["C", "F"]) - def test_iface_order(self, order): - a = self._non_c_contiguous_array() + @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) - expected = numpy.asarray(a.asnumpy(), 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[f"{order}_CONTIGUOUS"] + 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._non_c_contiguous_array() + a = self._f_contiguous_array() result = dpnp.asnumpy(a) assert result.flags["C_CONTIGUOUS"] - @pytest.mark.parametrize("order", ["C", "F"]) + @pytest.mark.parametrize("order", orders) def test_method_order(self, order): - a = self._non_c_contiguous_array() + a = self._f_contiguous_array() result = a.asnumpy(order=order) - assert result.flags[f"{order}_CONTIGUOUS"] - assert_array_equal(result, a.asnumpy(order="K")) + 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._non_c_contiguous_array() + 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._non_c_contiguous_array() + a = self._f_contiguous_array() result = a.asnumpy(order="K") assert not result.flags["C_CONTIGUOUS"] - @pytest.mark.parametrize("order", ["C", "F"]) + @pytest.mark.parametrize("order", orders) def test_usm_ndarray_input_order(self, order): - a = self._non_c_contiguous_array() + a = self._f_contiguous_array() usm_a = dpnp.get_usm_ndarray(a) result = dpnp.asnumpy(usm_a, order=order) - assert result.flags[f"{order}_CONTIGUOUS"] 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)) + class TestToFile: def _create_data(self): From edc744c59d0316d61fed1f961bcee53d56f2120d Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty Date: Tue, 30 Jun 2026 10:21:33 -0500 Subject: [PATCH 4/6] fix formatting --- dpnp/tests/test_ndarray.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index 42c0198cf5af..f5e5f4f6da21 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -163,8 +163,10 @@ def _c_contiguous_array(self): @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() + a = ( + self._c_contiguous_array() + if layout == "c" + else (self._f_contiguous_array()) ) result = dpnp.asnumpy(a, order=order) From 8ecf7a79780d64943023fd4b99ebbefb835b2ab8 Mon Sep 17 00:00:00 2001 From: abagusetty Date: Tue, 30 Jun 2026 15:51:34 -0500 Subject: [PATCH 5/6] Add negative-stride asnumpy regression test --- dpnp/tests/test_ndarray.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index 60323f6199be..b34410f308bb 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -221,6 +221,20 @@ def test_empty_array(self, shape, order): 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): From 5504dc3de30094cb57d87406c7b54a198fcdc376 Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty <59661409+abagusetty@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:57:41 -0500 Subject: [PATCH 6/6] Update CHANGELOG.md Co-authored-by: Anton <100830759+antonwolfy@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85894319da78..fa261deeafca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,7 +57,7 @@ This release is compatible with NumPy 2.5. * Fixed `dpnp.linalg.svd(..., hermitian=True)` returning a non-unitary `vh` for singular input arrays due to a zero sign appearing [#2954](https://github.com/IntelPython/dpnp/pull/2954) * 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 `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/issues/2980) +* 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