pydata / sparse

Sparse multi-dimensional arrays for the PyData ecosystem

Home Page:https://sparse.pydata.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

GCXS slice bug

israelmcmc opened this issue · comments

Describe the bug
The slice operator (__getitem__) produces incorrect results in some cases for the class GCXS.

To Reproduce
I found this error working with a large matrix. This is the minimal version that reproduces the error

dok = DOK(shape = (12, 1147, 12, 32, 1147, 3, 3))
dok[1,1,1,1,1,1,1] = 5
coo = dok.to_coo()
gcxs = coo.asformat('gcxs')

print( coo[1:11, 1:2, 1:11, 1:31, 1:1146, 1:2, 1:2])
print(gcxs[1:11, 1:2, 1:11, 1:31, 1:1146, 1:2, 1:2])

Results in

<COO: shape=(10, 1, 10, 30, 1145, 1, 1), dtype=float64, nnz=1, fill_value=0.0>
<GCXS: shape=(10, 1, 10, 30, 1145, 1, 1), dtype=float64, nnz=0, fill_value=0.0, compressed_axes=(5,)>

They should be the same.

I tried to reproduce it on small arrays, but it worked just fine. On medium-size matrices however, like the following, sometimes it works and sometimes it doesn't (running exactly the same code, seems random):

dok = DOK(shape = (4,5,6,7,8,9,10))
dok[1,1,1,1,1,1,1] = 5
coo = dok.to_coo()
gcxs = coo.asformat('gcxs')

print( coo[1:3, 1:2, 1:5, 1:6, 1:7, 1:8, 1:9])
print(gcxs[1:3, 1:2, 1:5, 1:6, 1:7, 1:8, 1:9])

Sometimes results in :

<COO: shape=(2, 1, 4, 5, 6, 7, 8), dtype=float64, nnz=1, fill_value=0.0>
<GCXS: shape=(2, 1, 4, 5, 6, 7, 8), dtype=float64, nnz=1, fill_value=0.0, compressed_axes=(0,)>

And if I keep executing the cell, sometimes it outputs:

<COO: shape=(2, 1, 4, 5, 6, 7, 8), dtype=float64, nnz=1, fill_value=0.0>
<GCXS: shape=(2, 1, 4, 5, 6, 7, 8), dtype=float64, nnz=0, fill_value=0.0, compressed_axes=(0,)>

Expected behavior
COO and GCXS should have consistent results. In particular for the examples above, the result should have nnz=1 always (COO gets it right).

System

  • OS and version: macOS 12.3.1
  • sparse version (sparse.__version__): 0.13.0
  • NumPy version (np.__version__): 1.21.5
  • Numba version (numba.__version__): 0.53.0

Additional context
None

Can confirm that this is happening on master.

I was curious whether it might have to do with the fact that asformat('gcxs') is trying to guess the best axis to compress, so I started passing different kwargs. I found it was somewhat random but mostly occurred regardless of the compressed axis, but with compressed_axes=(1,) it often crashes. 🙃 Not with 4, though.

It does print the first line though so it's indeed in __getitem__, not in conversion...

Based on the crash I'm guessing this is in get_slicing_selection because that's a numba-fied function.

edit: Interestingly, if I disable the use of that function I get way crazier errors: nnz goes into up to 60-80 somehow. Maybe there's an assumption in get_array_selection that I'm missing but that seems wrong.

I was wrong, I think it's actually somewhere in sparse.convert.convert_to_flat (at least, the crash is coming from there, not sure about the bug)

Sunday Funday 🎉

I think I tracked this down but I still don't fully understand why it's happening so I can't say for sure.

The issue is in convert.py. That block is checking if the current position is at the end of a block, and if so it resets the counter and increments the higher dimension. However it was only checking the first case, not multi-dimensional cases, and this led to out-of-bounds accessing in some cases and (I guess?) incorrect indexing in other cases.

The fix I have is simple but I am not sure it is complete:

for i in range(operations):
-   if i != 0 and positions[pos] == increments[pos].shape[0]:
+   while i != 0 and positions[pos] == increments[pos].shape[0]:
        positions[pos] = 0
        pos -= 1
        positions[pos] += 1
-       pos += 1
+ pos = len(increments) - 2  # resetting to the initial value

I'll open a PR but probably @hameerabbasi should review and maybe there is a better solution.

I think @daletovar wrote that code, he's the best person to review.