Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ This release is compatible with NumPy 2.5.
* Fixed `dpnp.cumsum`, `dpnp.cumprod`, and their `nan`/`cumulative_*` variants (including `dpnp.tensor.cumulative_sum`/`cumulative_prod`) silently returning incorrect results when accumulating along an axis of an array with more than one row [#3063](https://github.com/IntelPython/dpnp/pull/3063)
* Fixed `dpnp.einsum` returning a result whose memory layout differs from NumPy for the default `order="K"`, and ignoring `out` and `order` for a contraction over a size-0 dimension [#3058](https://github.com/IntelPython/dpnp/pull/3058)
* Fixed operations on a boolean array whose bytes are not `0x00`/`0x01` [#3055](https://github.com/IntelPython/dpnp/pull/3055)
* Fixed the `dpnp.ndarray` constructor returning a view at the wrong address [#3068](https://github.com/IntelPython/dpnp/pull/3068)

### Security

Expand Down
39 changes: 29 additions & 10 deletions dpnp/dpnp_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,15 @@ def __init__(
# or as USM memory allocation
if isinstance(buffer, dpnp_array):
buffer = buffer.get_array()
offset += buffer._element_offset

if dtype is None and hasattr(buffer, "dtype"):
dtype = buffer.dtype

if isinstance(buffer, dpt.usm_ndarray):
# `_element_offset` is in buffer-dtype units; the ctor's
# `offset` is in `dtype` units, so rescale via bytes when
# itemsizes differ
offset += dpnp_array._rescaled_element_offset(buffer, dtype)
else:
buffer = usm_type

Expand Down Expand Up @@ -673,6 +678,28 @@ def _create_from_usm_ndarray(usm_ary: dpt.usm_ndarray):
res._array_obj._set_namespace(dpnp)
return res

@staticmethod
def _rescaled_element_offset(usm_ary, new_dtype):
"""
Return the element offset of `usm_ary` within its USM allocation,
expressed in units of `new_dtype`.

The offset carried by :attr:`usm_ndarray._element_offset` is in units
of the array's own dtype, so it has to be rescaled through bytes
whenever `new_dtype` has a different itemsize.

"""

byte_offset = usm_ary._element_offset * usm_ary.itemsize
offset, rem = divmod(byte_offset, dpnp.dtype(new_dtype).itemsize)
if rem:
raise ValueError(
"The offset of the array data in memory is not a multiple "
"of the new data type size and so the requested view is "
"not possible"
)
return offset

def _create_view(self, array_class, shape, dtype, strides):
"""
Create a view of an array with the specified class.
Expand Down Expand Up @@ -705,15 +732,7 @@ def _create_view(self, array_class, shape, dtype, strides):

# `buffer=self._array_obj` views the whole USM allocation, so `self`'s
# element offset within it must be forwarded explicitly

byte_offset = self._array_obj._element_offset * self.itemsize
offset, rem = divmod(byte_offset, new_itemsize)
if rem:
raise ValueError(
"The offset of the array data in memory is not a multiple "
"of the new data type size and so the requested view is "
"not possible"
)
offset = dpnp_array._rescaled_element_offset(self._array_obj, dtype)

# create the underlying usm_ndarray view
usm_view = dpt.usm_ndarray(
Expand Down
78 changes: 78 additions & 0 deletions dpnp/tests/test_ndarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,84 @@ def test_nonzero_offset_buffer_ctor(self):
assert_array_equal(ia.view(), expected)
assert_array_equal(ia.view(dpnp.uint32), expected.view(numpy.uint32))

@pytest.mark.parametrize(
"src_dt, new_dt",
[
(dpnp.complex64, dpnp.uint16),
(dpnp.complex128, dpnp.float64),
(dpnp.float64, dpnp.float32),
(dpnp.int64, dpnp.int8),
(dpnp.int32, dpnp.int16),
(dpnp.int16, dpnp.int64),
],
)
def test_nonzero_offset_buffer_ctor_dtype_mismatch(self, src_dt, new_dt):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two small gaps worth filling while we're here (non-blocking):

  1. No test exercises a non-zero incoming offset= arg together with a dtype-mismatched buffer — the offset=1 case uses a same-itemsize dtype, so the offset += add_offset addition is never checked with a non-trivial add_offset. A case like dpnp.ndarray((size,), dtype=new_dt, buffer=sl, offset=1) would cover it.

  2. The misalignment ValueError is only asserted for a dpnp_array buffer (test_misaligned_offset_buffer_ctor_error). Since the fix routes bare usm_ndarray buffers through the same branch, a matching case with buffer=base[3:].get_array() would guard that path too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed both aspects

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test hard-fails on devices without native fp64/complex128 support.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

guarded it

if not has_support_aspect64() and (
dpnp.dtype(src_dt) in [dpnp.float64, dpnp.complex128]
or dpnp.dtype(new_dt) in [dpnp.float64, dpnp.complex128]
):
pytest.skip("requires fp64 support")

# the element offset of the `buffer=` array is expressed in units of
# the buffer's own dtype and has to be rescaled when the requested
# dtype has a different itemsize
base = dpnp.arange(32, dtype=src_dt)
sl = base[8:]

byte_offset = 8 * dpnp.dtype(src_dt).itemsize
size = (base.nbytes - byte_offset) // dpnp.dtype(new_dt).itemsize

ia = dpnp.ndarray((size,), dtype=new_dt, buffer=sl)
assert ia.data.ptr == sl.data.ptr
assert_array_equal(ia, dpnp.asnumpy(sl).view(new_dt))

# an explicit `offset` is expressed in units of the requested dtype
# and adds up with the rescaled offset of the buffer
ia = dpnp.ndarray((size - 1,), dtype=new_dt, buffer=sl, offset=1)
assert ia.data.ptr == sl.data.ptr + dpnp.dtype(new_dt).itemsize
assert_array_equal(ia, dpnp.asnumpy(sl).view(new_dt)[1:])

def test_nonzero_offset_buffer_ctor_usm_ndarray(self):
# the same rescaling applies when `buffer=` is a bare usm_ndarray
# rather than a dpnp.ndarray
base = dpnp.arange(16, dtype=dpnp.complex64)
sl = base[4:]
usm_sl = sl.get_array()

for dt in [dpnp.complex64, dpnp.uint16, dpnp.float32]:
size = usm_sl.nbytes // dpnp.dtype(dt).itemsize
ia = dpnp.ndarray((size,), dtype=dt, buffer=usm_sl)
assert ia.data.ptr == sl.data.ptr

# and the dtype still defaults to the buffer's one
ia = dpnp.ndarray((12,), buffer=usm_sl)
assert ia.dtype == base.dtype
assert ia.data.ptr == sl.data.ptr

def test_nonzero_offset_buffer_ctor_write_through(self):
# a write through the dtype-mismatched view must land in the parent
# allocation at the offset the buffer points at
base = dpnp.zeros(16, dtype=dpnp.complex64)
sl = base[8:]

ia = dpnp.ndarray((16,), dtype=dpnp.float32, buffer=sl)
ia[:] = 1

expected = numpy.zeros(16, dtype=numpy.complex64)
expected[8:] = 1 + 1j
assert_array_equal(base, expected)

def test_misaligned_offset_buffer_ctor_error(self):
base = dpnp.arange(16, dtype=dpnp.int16)
# the buffer starts at a byte offset of 6, which is not addressable
# with an itemsize of 8
with pytest.raises(ValueError, match="not a multiple"):
dpnp.ndarray((3,), dtype=dpnp.int64, buffer=base[3:])

# and the same holds for a bare usm_ndarray buffer
with pytest.raises(ValueError, match="not a multiple"):
dpnp.ndarray((3,), dtype=dpnp.int64, buffer=base[3:].get_array())

def test_misaligned_offset_error(self):
ia = dpnp.arange(10, dtype=dpnp.int16)
# numpy supports such a view, but usm_ndarray cannot address memory
Expand Down
Loading