Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 12 additions & 2 deletions dpnp/dpnp_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -933,7 +943,7 @@ def asnumpy(self):

"""

return dpt.asnumpy(self._array_obj)
return dpt.asnumpy(self._array_obj, order=order)

def astype(
self,
Expand Down
4 changes: 2 additions & 2 deletions dpnp/dpnp_iface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
23 changes: 17 additions & 6 deletions dpnp/tensor/_copy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Comment on lines +80 to +82

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the reason is that this will preserve the strides of the source result array, which are not necessarily correct per NumPy's order="K" semantics themselves



def _copy_from_numpy(np_ary, usm_type="device", sycl_queue=None):
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
93 changes: 93 additions & 0 deletions dpnp/tests/test_ndarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment thread
antonwolfy marked this conversation as resolved.
assert not result.flags["C_CONTIGUOUS"]

Comment thread
antonwolfy marked this conversation as resolved.
@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)
Expand Down
Loading