dace icon indicating copy to clipboard operation
dace copied to clipboard

Memory Bug, Possible Undefined Baheviour

Open philip-paul-mueller opened this issue 10 months ago • 2 comments

Create the following unit test file, which is based on program_strides_2() from tests/numpy/array_creation_test.py.

import dace
import numpy as np
import copy

@dace.program
def program():
    # Was originally `program_strides_2()` in `tests/numpy/array_creation_test.py`.
    A = dace.ndarray((2, 2), dtype=dace.int32, strides=(1, 2))
    for i, j in dace.map[0:2, 0:2]:
        A[i, j] = j + i
    return A

def _impl_of_memory_test():
    sdfg = program.to_sdfg()
    csdfg = sdfg.compile()

    A1 = csdfg()
    Res1 = copy.deepcopy(A1)
    A2 = csdfg()
    assert A1 is A2
    Res2 = copy.deepcopy(A2)

    assert A2.strides == (4, 8)
    assert np.allclose(Res2, [[0, 1], [1, 2]]), "Never expected that this fails."
    assert np.allclose(Res1, [[0, 1], [1, 2]]), "Expected that this fails."

def test():
    _impl_of_memory_test()

if __name__ == "__main__":
    test()

Note that the bug does not surfaces every time, so you have to call the test in the following way:

for i in $(seq 100) ; do echo "ITERATION ${i}" ; pytest memory_issue_test.py --pdb ; done

It usually happens within the first 10 to 20 iterations.

I tried the following:

  • If the Maps are serial, then it works as expected.
  • If we add an additional operation, i.e. at the end we add A[0, 0] += 1 the error still surfaces.
  • If we set may_alias of __return to True it still fails.
  • if we change the assignment to any of A[i, j] = 0, A[i, j] = i or A[i, j] = j then the test passes.
  • If we move the for loop that calls the test, i.e. the bash loop suggested above, into python, then it seems that the bug is not triggered.
  • Passing A as an argument, i.e. allocating it explicitly, the result is still the same, it fails; See below for the code.
  • I do not observe the issue if A is allocated in C order (for that I used the version that passes A as argument).

I tried it on commit 8c24a345277ecf5d0e8f9a9c3f85a2630c367a9a, which was main at the point.

I am using Python 3.9.20 and 3.12.3 (but I have the impression that it happens less often for it).

philip-paul-mueller avatar Feb 04 '25 08:02 philip-paul-mueller

Here is a second reproducer, that allocates the array outside.

  • If the memory order of A is switched from F to C then the code works as expected (did not crash within 100 invocations).
  • If A is not initialized with np.empty but with np.zeros it works as expected (this is the same behaviour I see; Only the second write takes effect).
import dace
import numpy as np
import copy

# Setting `order` to `C` will stop the issues.
order = "F"

if order == "F":
    AType = dace.data.Array(dace.int32, shape=(2, 2), strides=(1, 2))
else:
    assert order == "C"
    AType = dace.data.Array(dace.int32, shape=(2, 2))

# Was originally `program_strides_2()` in `tests/numpy/array_creation_test.py`.
@dace.program
def program(A: AType):
    for i, j in dace.map[0:2, 0:2]:
        A[i, j] = j + i

def _impl_of_memory_test():
    sdfg = program.to_sdfg()
    csdfg = sdfg.compile()

    A = np.empty((2, 2), dtype=np.int32, order=order)
    if order == "F":
        assert A.strides == (4, 8)
    else:
        assert A.strides == (8, 4)

    csdfg(A)
    Res1 = copy.deepcopy(A)
    csdfg(A)
    Res2 = copy.deepcopy(A)

    assert np.all((Res1 - Res2) == 0)
    assert np.allclose(Res2, [[0, 1], [1, 2]])
    assert np.allclose(Res1, [[0, 1], [1, 2]])

def test():
    _impl_of_memory_test()

if __name__ == "__main__":
    test()

philip-paul-mueller avatar Feb 06 '25 12:02 philip-paul-mueller

This is a reproducer, that also fails, here the copy operation is omitted, so we get the genuine true output:

import dace
import numpy as np

@dace.program
def program():
    # Was originally `program_strides_2()` in `tests/numpy/array_creation_test.py`.
    A = dace.ndarray((2, 2), dtype=dace.int32, strides=(1, 2))
    for i, j in dace.map[0:2, 0:2]:
        A[i, j] = j + i
    return A

def _impl_of_memory_test():
    sdfg = program.to_sdfg()
    csdfg = sdfg.compile()

    A1 = csdfg()
    assert np.allclose(A1, [[0, 1], [1, 2]])
    assert A1.strides == (4, 8)
    A2 = csdfg()
    assert np.allclose(A2, [[0, 1], [1, 2]])
    assert A2.strides == (4, 8)

def test():
    _impl_of_memory_test()

if __name__ == "__main__":
    test()

philip-paul-mueller avatar Feb 06 '25 12:02 philip-paul-mueller