From 16cfa2adf534b2f4263d79855add4ce14ae9fdb1 Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty Date: Thu, 10 Sep 2026 13:09:42 -0500 Subject: [PATCH 1/3] Fix for dpnp buffer-arg ndarray ignoring offset --- CHANGELOG.md | 1 + dpnp/dpnp_array.py | 21 +++++++++++-- dpnp/tests/test_ndarray.py | 62 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f47c903d54..35eab2faa81 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 when `buffer=` has a non-zero USM element offset and either the requested `dtype` differs in itemsize or the buffer is a bare `dpnp.tensor.usm_ndarray` [#3066](https://github.com/IntelPython/dpnp/pull/3066) ### 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 From 706570a8ec2935a68d4a1c650531c1deeab28106 Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty Date: Thu, 10 Sep 2026 13:36:21 -0500 Subject: [PATCH 2/3] fixed CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35eab2faa81..e451cc74fca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,7 +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 when `buffer=` has a non-zero USM element offset and either the requested `dtype` differs in itemsize or the buffer is a bare `dpnp.tensor.usm_ndarray` [#3066](https://github.com/IntelPython/dpnp/pull/3066) +* Fixed the `dpnp.ndarray` constructor returning a view at the wrong address [#3068](https://github.com/IntelPython/dpnp/pull/3068) ### Security From 2d9a324a1379fda5b0680d56cda950703e77c050 Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty Date: Fri, 11 Sep 2026 09:49:05 -0500 Subject: [PATCH 3/3] address PR comments --- dpnp/dpnp_array.py | 58 ++++++++++++++++++++------------------ dpnp/tests/test_ndarray.py | 16 +++++++++++ 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 3d0b3631d01..2c79782796d 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -128,26 +128,14 @@ def __init__( if isinstance(buffer, dpnp_array): buffer = buffer.get_array() - 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"): + 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 @@ -690,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. @@ -722,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 58916545823..94331d34269 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -541,6 +541,12 @@ def test_nonzero_offset_buffer_ctor(self): ], ) 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 @@ -554,6 +560,12 @@ def test_nonzero_offset_buffer_ctor_dtype_mismatch(self, src_dt, new_dt): 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 @@ -591,6 +603,10 @@ def test_misaligned_offset_buffer_ctor_error(self): 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