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..3d0b3631d01 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -127,9 +127,26 @@ 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"): + if isinstance(buffer, dpt.usm_ndarray): + if dtype is None: + dtype = buffer.dtype + + # `buffer._element_offset` is expressed in units of the + # buffer's own dtype, while `offset` is interpreted in units + # of `dtype`, so the displacement has to be rescaled through + # bytes whenever the two itemsizes differ + byte_offset = buffer._element_offset * buffer.itemsize + new_itemsize = dpnp.dtype(dtype).itemsize + add_offset, rem = divmod(byte_offset, new_itemsize) + if rem != 0: + raise ValueError( + "The offset of the buffer's data in memory is not " + "a multiple of the requested dtype size and so the " + "requested view is not possible" + ) + offset += add_offset + elif dtype is None and hasattr(buffer, "dtype"): dtype = buffer.dtype else: buffer = usm_type diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index f30317605aa..58916545823 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -529,6 +529,68 @@ 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): + # 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)) + + 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:]) + def test_misaligned_offset_error(self): ia = dpnp.arange(10, dtype=dpnp.int16) # numpy supports such a view, but usm_ndarray cannot address memory