From 340a225d731ec46b44d586d3e6d513272fe1b3b2 Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty Date: Fri, 21 Aug 2026 11:52:54 -0500 Subject: [PATCH 1/4] Fix ndarray.view() ignoring the USM element offset of the source array _create_view() (introduced in #2815) rebuilds the view with dpt.usm_ndarray(shape, dtype, buffer=self._array_obj, strides=...), and the constructor rebases such a view onto the whole underlying USM allocation with a default offset of 0. Any source array that does not start at the base of its allocation (e.g. a slice) therefore got a view onto the wrong memory, silently returning values from the base of the parent buffer. This is the same class of bug as the ones fixed in #2651 (ndarray constructor with buffer=) and #2812 (.data.ptr on views), but for the _create_view() helper added later for ndarray subclassing support. The impact is not limited to explicit .view() calls: dpnp.einsum() takes a "returns_view" fast path for a single operand with no summed index (any pure permutation, including the identity 'abc->abc') and calls .view() on each operand, so einsum over a sliced operand silently returned wrong values. The fix forwards the source array's element offset (converted to units of the view's dtype) to the usm_ndarray constructor, and raises a clear ValueError in the one case a usm_ndarray cannot represent: a byte offset that is not a multiple of the new itemsize. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 1 + dpnp/dpnp_array.py | 18 ++++++++++- dpnp/tests/test_linalg.py | 11 +++++++ dpnp/tests/test_ndarray.py | 61 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 113b0060fdf..bee74b72681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ This release is compatible with NumPy 2.5. * Fixed a crash in boolean-mask advanced indexing (`dpnp.ndarray` get/set item) when the selection is empty (e.g. a scalar `False` index that injects a length-0 axis) [#3019](https://github.com/IntelPython/dpnp/pull/3019) * Released the GIL before the remaining blocking OneMKL BLAS and LAPACK calls to prevent host tasks contention, completing the work started in [#2850](https://github.com/IntelPython/dpnp/pull/2850) [#3027](https://github.com/IntelPython/dpnp/pull/3027) * Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/pull/3024) +* Fixed `dpnp.ndarray.view` ignoring the USM element offset of a sliced array, which also caused `dpnp.einsum` to silently return wrong results for a single sliced operand with no summed index [#3035](https://github.com/IntelPython/dpnp/pull/3035) ### Security diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 14cd519bbe8..d72aae8f778 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -701,12 +701,28 @@ def _create_view(self, array_class, shape, dtype, strides): if dtype is None: dtype = self.dtype + new_itemsize = dpnp.dtype(dtype).itemsize + + # passing `buffer=self._array_obj` gives a view onto the whole + # underlying USM allocation, so the element offset of `self` within + # that allocation must be forwarded explicitly (in units of the + # view's dtype) + 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" + ) + # create the underlying usm_ndarray view usm_view = dpt.usm_ndarray( shape, dtype=dtype, buffer=self._array_obj, - strides=tuple(s // dpnp.dtype(dtype).itemsize for s in strides), + strides=tuple(s // new_itemsize for s in strides), + offset=offset, ) # wrap the view into the appropriate class diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 1aa3781dcf6..cfcaf7f9ec2 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -593,6 +593,17 @@ def test_trivial_cases(self): expected = numpy.einsum("i,i,i", b_np, b_np, b_np, optimize="greedy") assert_dtype_allclose(result, expected) + def test_sliced_operand_view_path(self): + # a single-operand einsum with no summed index returns a view of the + # operand; the view must respect the USM offset of a sliced operand + a = numpy.arange(24.0, dtype=numpy.float32).reshape(2, 3, 4) + ia = dpnp.array(a) + + for subscripts in ["abc->abc", "abc->cab"]: + result = dpnp.einsum(subscripts, ia[:, 1:, :]) + expected = numpy.einsum(subscripts, a[:, 1:, :]) + assert_dtype_allclose(result, expected) + def test_out(self): a = dpnp.ones((5, 5)) out = dpnp.empty((5,)) diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index b34410f308b..8b98b02fe21 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -328,6 +328,57 @@ def test_python_types(self, dt): expected = a.view(dt) assert_allclose(result, expected) + def test_nonzero_offset(self): + # the view must preserve the USM element offset of a sliced array + # instead of rebasing onto the start of the allocation + a = numpy.arange(10, dtype=numpy.int32) + ia = dpnp.array(a) + + expected = a[3:].view() + result = ia[3:].view() + assert_array_equal(result, expected) + + expected = a[3:].view(numpy.uint32) + result = ia[3:].view(dpnp.uint32) + assert_array_equal(result, expected) + + def test_nonzero_offset_2d(self): + a = numpy.arange(12, dtype=numpy.int32).reshape(3, 4) + ia = dpnp.array(a) + + expected = a[1:].view() + result = ia[1:].view() + assert_array_equal(result, expected) + + # itemsize-decreasing view on an array with non-zero offset + expected = a[1:].view(numpy.int16) + result = ia[1:].view(dpnp.int16) + assert_array_equal(result, expected) + + # itemsize-increasing view on an array with non-zero offset + expected = a[1:].view(numpy.int64) + result = ia[1:].view(dpnp.int64) + assert_array_equal(result, expected) + + def test_nonzero_offset_negative_strides(self): + a = numpy.arange(10, dtype=numpy.int32) + ia = dpnp.array(a) + + expected = a[::-1].view() + result = ia[::-1].view() + assert_array_equal(result, expected) + + expected = a[8:2:-2].view() + result = ia[8:2:-2].view() + assert_array_equal(result, expected) + + def test_misaligned_offset_error(self): + ia = dpnp.arange(10, dtype=dpnp.int16) + # numpy supports such a view, but usm_ndarray cannot address memory + # at a fraction of its element size + with pytest.raises(ValueError, match="not a multiple"): + ia[1:9].view(dpnp.int32) + def test_subclass_basic(self): class MyArray(dpnp.ndarray): pass @@ -339,6 +390,16 @@ class MyArray(dpnp.ndarray): assert type(view) is MyArray assert (view == x).all() + def test_subclass_nonzero_offset(self): + class MyArray(dpnp.ndarray): + pass + + x = dpnp.arange(10, dtype=dpnp.int32) + view = x[3:].view(type=MyArray) + + assert type(view) is MyArray + assert_array_equal(view, numpy.arange(3, 10, dtype=numpy.int32)) + def test_dtype_type_subclass(self): class MyArray(dpnp.ndarray): pass From e8f248febfaf2a084bd0608307b591f87c638252 Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty Date: Fri, 21 Aug 2026 11:57:22 -0500 Subject: [PATCH 2/4] Expand view() USM-offset test coverage Parametrize over all supported dtypes and several slice patterns (positive/negative steps, empty result), and add cases for 0-d views, chained views with accumulated offsets, memory sharing / write-through, complex<->real reinterpretation with an offset, and a second misaligned-offset error case. Co-Authored-By: Claude Fable 5 --- dpnp/tests/test_ndarray.py | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index 8b98b02fe21..6b27c7fe7c9 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -372,6 +372,75 @@ def test_nonzero_offset_negative_strides(self): result = ia[8:2:-2].view() assert_array_equal(result, expected) + @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize( + "sl", + [ + slice(3, None), + slice(1, 8), + slice(2, None, 3), + slice(None, None, -1), + slice(8, 2, -2), + slice(10, None), # empty result with non-zero offset + ], + ) + def test_nonzero_offset_all_dtypes(self, dt, sl): + a = numpy.arange(10).astype(dt) + ia = dpnp.array(a) + + expected = a[sl].view() + result = ia[sl].view() + assert_array_equal(result, expected) + + def test_nonzero_offset_0d(self): + a = numpy.arange(10, dtype=numpy.int32) + ia = dpnp.array(a) + + expected = a[5, ...].view() + result = ia[5, ...].view() + assert_array_equal(result, expected) + + def test_nonzero_offset_chained_views(self): + a = numpy.arange(20, dtype=numpy.int32) + ia = dpnp.array(a) + + # offsets accumulated over several slicing steps + expected = a[2:][3:].view() + result = ia[2:][3:].view() + assert_array_equal(result, expected) + + # a view of a view keeps the offset as well + expected = a[4:].view().view(numpy.uint32) + result = ia[4:].view().view(dpnp.uint32) + assert_array_equal(result, expected) + + def test_nonzero_offset_shares_memory(self): + a = numpy.arange(10, dtype=numpy.int32) + ia = dpnp.array(a) + + iv = ia[3:].view() + assert iv.data.ptr == ia[3:].data.ptr + + # writing through the view must modify the parent array + iv[0] = -7 + a[3] = -7 + assert_array_equal(ia, a) + + def test_nonzero_offset_complex_real(self): + a = numpy.arange(8).astype(numpy.complex64) + ia = dpnp.array(a) + + expected = a[2:].view(numpy.float32) + result = ia[2:].view(dpnp.float32) + assert_array_equal(result, expected) + + b = numpy.arange(8).astype(numpy.float32) + ib = dpnp.array(b) + + expected = b[2:].view(numpy.complex64) + result = ib[2:].view(dpnp.complex64) + assert_array_equal(result, expected) + def test_misaligned_offset_error(self): ia = dpnp.arange(10, dtype=dpnp.int16) # numpy supports such a view, but usm_ndarray cannot address memory @@ -379,6 +448,10 @@ def test_misaligned_offset_error(self): with pytest.raises(ValueError, match="not a multiple"): ia[1:9].view(dpnp.int32) + ia = dpnp.arange(8, dtype=dpnp.float32) + with pytest.raises(ValueError, match="not a multiple"): + ia[1:7].view(dpnp.complex64) + def test_subclass_basic(self): class MyArray(dpnp.ndarray): pass From 3129e62948a6a5ed9a876d88ec89aeba7b4cb516 Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty <59661409+abagusetty@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:16:13 -0500 Subject: [PATCH 3/4] Update CHANGELOG with recent bug fixes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bee74b72681..12f169789d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,7 +85,7 @@ This release is compatible with NumPy 2.5. * Fixed a crash in boolean-mask advanced indexing (`dpnp.ndarray` get/set item) when the selection is empty (e.g. a scalar `False` index that injects a length-0 axis) [#3019](https://github.com/IntelPython/dpnp/pull/3019) * Released the GIL before the remaining blocking OneMKL BLAS and LAPACK calls to prevent host tasks contention, completing the work started in [#2850](https://github.com/IntelPython/dpnp/pull/2850) [#3027](https://github.com/IntelPython/dpnp/pull/3027) * Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/pull/3024) -* Fixed `dpnp.ndarray.view` ignoring the USM element offset of a sliced array, which also caused `dpnp.einsum` to silently return wrong results for a single sliced operand with no summed index [#3035](https://github.com/IntelPython/dpnp/pull/3035) +* Fixed `dpnp.ndarray.view` ignoring the USM element offset of a sliced array, which also caused `dpnp.einsum` to silently return wrong results for a single sliced operand with no summed index [#3035](https://github.com/IntelPython/dpnp/pull/3037) ### Security From 516df9e5d0e1a952d609fc521c7003d4c4ae8b5d Mon Sep 17 00:00:00 2001 From: Abhishek Bagusetty Date: Fri, 21 Aug 2026 12:53:36 -0500 Subject: [PATCH 4/4] Cover more corner cases for view() with a USM offset Widen the dtype sweep to 1- and 2-byte integer types and float16, which exercise the byte-offset / itemsize arithmetic far more than the default dtype list (4, 8 and 16-byte types only). Add cases for 3-D slices, F-ordered arrays, non-contiguous sources (transposed and column slices, including the numpy-compatible refusal to change the itemsize when the last axis is not contiguous), usm_type and sycl_queue preservation, write-through via a dtype-changing view, arithmetic and reduction kernels reading through an offset view, and an array built with both a `buffer=` offset and a constructor `offset=`. Co-Authored-By: Claude Opus 5 (1M context) --- dpnp/tests/test_ndarray.py | 100 ++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/dpnp/tests/test_ndarray.py b/dpnp/tests/test_ndarray.py index 6b27c7fe7c9..a8091070a09 100644 --- a/dpnp/tests/test_ndarray.py +++ b/dpnp/tests/test_ndarray.py @@ -19,6 +19,7 @@ get_all_dtypes, get_complex_dtypes, get_float_dtypes, + get_integer_dtypes, has_support_aspect64, ) from .third_party.cupy import testing @@ -304,6 +305,17 @@ def test_basic(self, data): assert_array_equal(ia.tolist(), a.tolist()) +# dtypes spanning itemsizes of 1, 2, 4, 8 and 16 bytes, to stress the byte +# offset arithmetic performed when creating a view of an array with a non-zero +# USM offset +_view_offset_dtypes = list( + dict.fromkeys( + get_all_dtypes(no_none=True, no_float16=False) + + get_integer_dtypes(all_int_types=True) + ) +) + + class TestView: def test_none_dtype(self): a = numpy.ones((1, 2, 4), dtype=numpy.int32) @@ -372,7 +384,7 @@ def test_nonzero_offset_negative_strides(self): result = ia[8:2:-2].view() assert_array_equal(result, expected) - @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("dt", _view_offset_dtypes) @pytest.mark.parametrize( "sl", [ @@ -441,6 +453,92 @@ def test_nonzero_offset_complex_real(self): result = ib[2:].view(dpnp.complex64) assert_array_equal(result, expected) + def test_nonzero_offset_3d(self): + a = numpy.arange(24, dtype=numpy.int32).reshape(2, 3, 4) + ia = dpnp.array(a) + + for sl in [ + numpy.s_[1:], + numpy.s_[:, 1:, :], + numpy.s_[:, :, 2:], + numpy.s_[1:, 1:, 1:], + ]: + expected = a[sl].view() + result = ia[sl].view() + assert_array_equal(result, expected) + + def test_nonzero_offset_f_order(self): + a = numpy.asfortranarray( + numpy.arange(12, dtype=numpy.int32).reshape(3, 4) + ) + ia = dpnp.asarray(a, order="F") + + expected = a[1:].view() + result = ia[1:].view() + assert_array_equal(result, expected) + assert result.strides == ia[1:].strides + + def test_nonzero_offset_non_contiguous(self): + a = numpy.arange(12, dtype=numpy.int32).reshape(3, 4) + ia = dpnp.array(a) + + # a transposed slice and a column slice both have a non-zero offset + # and a non-contiguous last axis + for expected, result in [ + (a[1:].T.view(), ia[1:].T.view()), + (a[:, 1:].view(), ia[:, 1:].view()), + ]: + assert_array_equal(result, expected) + + # changing the itemsize is rejected for a non-contiguous last axis, + # exactly as numpy does + for xp, x in [(numpy, a[1:].T), (dpnp, ia[1:].T)]: + with pytest.raises( + ValueError, match="last axis must be contiguous" + ): + x.view(xp.int16) + + @pytest.mark.parametrize("usm_type", ["device", "shared", "host"]) + def test_nonzero_offset_keeps_usm_type_and_queue(self, usm_type): + ia = dpnp.arange(10, dtype=dpnp.int32, usm_type=usm_type) + iv = ia[3:].view() + + assert iv.usm_type == ia.usm_type + assert iv.sycl_queue == ia.sycl_queue + assert_array_equal(iv, numpy.arange(3, 10, dtype=numpy.int32)) + + def test_nonzero_offset_write_through_dtype_change(self): + a = numpy.arange(6, dtype=numpy.int32) + ia = dpnp.array(a) + + av, iav = a[2:].view(numpy.int16), ia[2:].view(dpnp.int16) + av[0] = 99 + iav[0] = 99 + + # the write must land in the parent array, at the right offset + assert_array_equal(ia, a) + + def test_nonzero_offset_compute(self): + # the kernels must read the view through its own offset, not from the + # base of the parent allocation + a = numpy.arange(24, dtype=numpy.float32).reshape(2, 3, 4) + ia = dpnp.array(a) + + av, iav = a[:, 1:, :].view(), ia[:, 1:, :].view() + assert_allclose(dpnp.sum(iav), numpy.sum(av)) + assert_allclose(iav * 2, av * 2) + + def test_nonzero_offset_buffer_ctor(self): + # combines the offset of the `buffer=` array with the offset passed + # to the ndarray constructor, and then views the result + base = dpnp.arange(12, dtype=dpnp.int32) + ia = dpnp.ndarray((4,), dtype=dpnp.int32, buffer=base[2:], offset=1) + + expected = numpy.arange(3, 7, dtype=numpy.int32) + assert_array_equal(ia, expected) + assert_array_equal(ia.view(), expected) + assert_array_equal(ia.view(dpnp.uint32), expected.view(numpy.uint32)) + def test_misaligned_offset_error(self): ia = dpnp.arange(10, dtype=dpnp.int16) # numpy supports such a view, but usm_ndarray cannot address memory