Skip to content

ndarray.view() ignores the array's USM element offset (silently wrong results, incl. einsum) #3036

Description

@abagusetty

This is the same class of bug as #2641 / #2781 (.data.ptr on views ignoring the USM offset, both fixed), but in _create_view() rather than .data.ptr. I verified .data.ptr is correct in this build, so the two paths have diverged.

Reproducer:

"""
dpnp: ndarray.view() ignores the array's USM element offset

dpnp_array._create_view() rebuilds the result with
    dpt.usm_ndarray(shape, dtype=dtype, buffer=self._array_obj, strides=...)
and never forwards self._array_obj._element_offset. dpctl interprets
`buffer=<usm_ndarray>` as the whole underlying USM allocation, so any array
that does not start at the base of its allocation gets a view onto the wrong
memory.

This is the same class of bug as the already-fixed #2641 / #2781
(".data.ptr on array views ignores USM offset"), but for _create_view()
rather than .data.ptr.

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 does
`operands = [a.view() for a in operands]`. So einsum over any sliced operand
silently returns values read from the base of the parent buffer. It is silent:
no exception, no warning, just wrong numbers.
"""
import sys

import numpy as np
import dpnp

if "gpu4pyscf" in sys.modules:
    raise SystemExit(
        "Run this with a clean interpreter: gpu4pyscf's compatibility shim "
        "patches dpnp_array._create_view and hides the bug.")

print("dpnp", dpnp.__version__)
fail = 0


def check(label, got, want):
    global fail
    got = dpnp.asnumpy(got) if isinstance(got, dpnp.ndarray) else np.asarray(got)
    ok = np.array_equal(got, want)
    fail += not ok
    print(f"{'PASS' if ok else 'FAIL'}  {label}")
    if not ok:
        print(f"        got  {got.ravel()[:8]}")
        print(f"        want {want.ravel()[:8]}")


# ---------------------------------------------------------------- 1. view()
h = np.arange(10.0)
d = dpnp.asarray(h)
check("x[3:].view()                  ", d[3:].view(), h[3:])
check("x[3:].view(dpnp.float64)      ", d[3:].view(dpnp.float64), h[3:])

h2 = np.arange(12.0).reshape(3, 4)
d2 = dpnp.asarray(h2)
check("x[1:].view()  (2-D)           ", d2[1:].view(), h2[1:])

# Zero-offset views are fine -- shows the offset is precisely what is lost.
check("x[0:].view()  (offset 0, ok)  ", d[0:].view(), h[0:])

# --------------------------------------------------- 2. einsum on a view
# einsum's `returns_view` path calls .view() internally, so a pure
# permutation of a sliced operand silently reads the wrong memory.
h3 = np.arange(24.0).reshape(2, 3, 4)
d3 = dpnp.asarray(h3)
check("einsum('abc->abc', x[:,1:,:]) ",
      dpnp.einsum("abc->abc", d3[:, 1:, :]), np.einsum("abc->abc", h3[:, 1:, :]))
check("einsum('abc->cab', x[:,1:,:]) ",
      dpnp.einsum("abc->cab", d3[:, 1:, :]), np.einsum("abc->cab", h3[:, 1:, :]))
# Reducing and two-operand einsums do NOT take the view path and are correct,
# which is what makes the bug so easy to miss.
check("einsum('abc->ab',  x[:,1:,:]) ",
      dpnp.einsum("abc->ab", d3[:, 1:, :]), np.einsum("abc->ab", h3[:, 1:, :]))

print()
print(f"{fail} failure(s)")
raise SystemExit(1 if fail else 0)

output:

$ python3 dpnp_view_offset_repro.py
dpnp 0.21.0dev5+154.ge1585e123a4
FAIL  x[3:].view()                  
        got  [0. 1. 2. 3. 4. 5. 6.]
        want [3. 4. 5. 6. 7. 8. 9.]
FAIL  x[3:].view(dpnp.float64)      
        got  [0. 1. 2. 3. 4. 5. 6.]
        want [3. 4. 5. 6. 7. 8. 9.]
FAIL  x[1:].view()  (2-D)           
        got  [0. 1. 2. 3. 4. 5. 6. 7.]
        want [ 4.  5.  6.  7.  8.  9. 10. 11.]
PASS  x[0:].view()  (offset 0, ok)  
FAIL  einsum('abc->abc', x[:,1:,:]) 
        got  [0. 1. 2. 3. 4. 5. 6. 7.]
        want [ 4.  5.  6.  7.  8.  9. 10. 11.]
FAIL  einsum('abc->cab', x[:,1:,:]) 
        got  [ 0.  4. 12. 16.  1.  5. 13. 17.]
        want [ 4.  8. 16. 20.  5.  9. 17. 21.]
PASS  einsum('abc->ab',  x[:,1:,:]) 

5 failure(s)

Credits: claude-sonnet-5 for reproducer

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions