diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f47c903d54..e451cc74fca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index b225fb2c732..2c79782796d 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -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 @@ -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. @@ -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( diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index f30317605aa..94331d34269 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -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): + 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