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 @@ -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/3037)

### Security

Expand Down
18 changes: 17 additions & 1 deletion dpnp/dpnp_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions dpnp/tests/test_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,))
Expand Down
232 changes: 232 additions & 0 deletions dpnp/tests/test_ndarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -328,6 +340,216 @@ 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)

@pytest.mark.parametrize("dt", _view_offset_dtypes)
@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_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
# at a fraction of its element size
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
Expand All @@ -339,6 +561,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
Expand Down