Repository: numba/numba

Issue:
Slicing device array with negative step produces wrong result
## Reporting a bug

- [X] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [X] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

Consider
```python
import numpy as np
import numba
import numba.cuda as cuda
arr = np.arange(4)
d_arr = cuda.to_device(arr)

print('numba version', numba.__version__)
print('numpy array:',arr[::-1].__array_interface__)
print('numba.cuda device array', d_arr[::-1].__cuda_array_interface__)
```
that outputs
```
numba version 0.42.0
numpy array: {'data': (94375105644824, False), 'strides': (-8,), 'descr': [('', '<i8')], 'typestr': '<i8', 'shape': (4,), 'version': 3}
numba.cuda device array {'shape': (-3,), 'strides': (-8,), 'data': (None, False), 'typestr': '<i8', 'version': 0}
```
Clearly, the shape `(-3,)` of `d_arr[::-1]` is wrong, it should be `(4,)`.

As a result, `d_arr[::-1].copy_to_host()` fails:
```python-traceback
Traceback (most recent call last):
  File "pyarrow/tests/numba_bug.py", line 9, in <module>
    d_arr[::-1].copy_to_host()
  File "/home/pearu/miniconda3/envs/pyarrow-dev/lib/python3.7/site-packages/numba/cuda/cudadrv/devices.py", line 212, in _require_cuda_context
    return fn(*args, **kws)
  File "/home/pearu/miniconda3/envs/pyarrow-dev/lib/python3.7/site-packages/numba/cuda/cudadrv/devicearray.py", line 240, in copy_to_host
    strides=self.strides, buffer=hostary)
ValueError: strides is incompatible with shape of requested array and size of buffer
```


Discussion:

Resolving patch:
diff --git a/numba/cuda/cudadrv/devicearray.py b/numba/cuda/cudadrv/devicearray.py
--- a/numba/cuda/cudadrv/devicearray.py
+++ b/numba/cuda/cudadrv/devicearray.py
@@ -220,6 +220,10 @@ def copy_to_host(self, ary=None, stream=0):
 
             result_array = d_arr.copy_to_host()
         """
+        if any(s < 0 for s in self.strides):
+            msg = 'D->H copy not implemented for negative strides: {}'
+            raise NotImplementedError(msg.format(self.strides))
+        assert self.alloc_size >= 0, "Negative memory size"
         stream = self._default_stream(stream)
         if ary is None:
             hostary = np.empty(shape=self.alloc_size, dtype=np.byte)
@@ -227,7 +231,6 @@ def copy_to_host(self, ary=None, stream=0):
             check_array_compatibility(self, ary)
             hostary = ary
 
-        assert self.alloc_size >= 0, "Negative memory size"
         if self.alloc_size != 0:
             _driver.device_to_host(hostary, self, self.alloc_size, stream=stream)
 
diff --git a/numba/cuda/cudadrv/driver.py b/numba/cuda/cudadrv/driver.py
--- a/numba/cuda/cudadrv/driver.py
+++ b/numba/cuda/cudadrv/driver.py
@@ -1715,7 +1715,7 @@ def device_memory_size(devmem):
         s, e = device_extents(devmem)
         sz = e - s
         devmem._cuda_memsize_ = sz
-    assert sz > 0, "zero length array"
+    assert sz >= 0, "{} length array".format(sz)
     return sz
 
 
diff --git a/numba/dummyarray.py b/numba/dummyarray.py
--- a/numba/dummyarray.py
+++ b/numba/dummyarray.py
@@ -43,8 +43,6 @@ class Dim(object):
     __slots__ = 'start', 'stop', 'size', 'stride', 'single'
 
     def __init__(self, start, stop, size, stride, single):
-        if stop < start:
-            raise ValueError("end offset is before start offset")
         self.start = start
         self.stop = stop
         self.size = size
@@ -54,54 +52,31 @@ def __init__(self, start, stop, size, stride, single):
 
     def __getitem__(self, item):
         if isinstance(item, slice):
-            start, stop, step = item.start, item.stop, item.step
-            single = False
-        else:
-            single = True
-            start = item
-            stop = start + 1
-            step = None
-
-        # Default values
-        #   Start value is default to zero
-        if start is None:
-            start = 0
-        #   Stop value is default to self.size
-        if stop is None:
-            stop = self.size
-        #   Step is default to 1
-        if step is None:
-            step = 1
-
-        stride = step * self.stride
-
-        # Compute start in bytes
-        if start >= 0:
-            start = self.start + start * self.stride
-        else:
-            start = self.stop + start * self.stride
-        start = max(start, self.start)
-
-        # Compute stop in bytes
-        if stop >= 0:
-            stop = self.start + stop * self.stride
-        else:
-            stop = self.stop + stop * self.stride
-        stop = min(stop, self.stop)
-
-        # Clip stop
-        if (stop - start) > self.size * self.stride:
-            stop = start + self.size * stride
-
-        if stop < start:
-            start = stop
-            size = 0
-        elif stride == 0:
-            size = 1 if single else ((stop - start) // step)
+            start, stop, step = item.indices(self.size)
+            stride = step * self.stride
+            start = self.start + start * abs(self.stride)
+            stop = self.start + stop * abs(self.stride)
+            if stride == 0:
+                size = 1
+            else:
+                size = _compute_size(start, stop, stride)
+            ret = Dim(
+                start=start,
+                stop=stop,
+                size=size,
+                stride=stride,
+                single=False
+            )
+            return ret
         else:
-            size = (stop - start + (stride - 1)) // stride
-
-        return Dim(start, stop, size, stride, single)
+            sliced = self[item:item + 1]
+            return Dim(
+                start=sliced.start,
+                stop=sliced.stop,
+                size=sliced.size,
+                stride=sliced.stride,
+                single=True,
+            )
 
     def get_offset(self, idx):
         return self.start + idx * self.stride
@@ -428,3 +403,17 @@ def is_element_indexing(item, ndim):
 
     return False
 
+
+def _compute_size(start, stop, step):
+    """Algorithm adapted from cpython rangeobject.c
+    """
+    if step > 0:
+        lo = start
+        hi = stop
+    else:
+        lo = stop
+        hi = start
+        step = -step
+    if lo >= hi:
+        return 0
+    return (hi - lo - 1) // step + 1


Test patch:
diff --git a/numba/cuda/tests/cudapy/test_cuda_array_interface.py b/numba/cuda/tests/cudapy/test_cuda_array_interface.py
--- a/numba/cuda/tests/cudapy/test_cuda_array_interface.py
+++ b/numba/cuda/tests/cudapy/test_cuda_array_interface.py
@@ -170,6 +170,39 @@ def test_array_views(self):
             h_arr[1::2]
         )
 
+    def test_negative_strided_issue(self):
+        # issue #3705
+        h_arr = np.random.random(10)
+        c_arr = cuda.to_device(h_arr)
+
+        def base_offset(orig, sliced):
+            return sliced['data'][0] - orig['data'][0]
+
+        h_ai = h_arr.__array_interface__
+        c_ai = c_arr.__cuda_array_interface__
+
+        h_ai_sliced = h_arr[::-1].__array_interface__
+        c_ai_sliced = c_arr[::-1].__cuda_array_interface__
+
+        # Check data offset is correct
+        self.assertEqual(
+            base_offset(h_ai, h_ai_sliced),
+            base_offset(c_ai, c_ai_sliced),
+        )
+        # Check shape and strides are correct
+        self.assertEqual(h_ai_sliced['shape'], c_ai_sliced['shape'])
+        self.assertEqual(h_ai_sliced['strides'], c_ai_sliced['strides'])
+
+    def test_negative_strided_copy_to_host(self):
+        # issue #3705
+        h_arr = np.random.random(10)
+        c_arr = cuda.to_device(h_arr)
+        sliced = c_arr[::-1]
+        with self.assertRaises(NotImplementedError) as raises:
+            sliced.copy_to_host()
+        expected_msg = 'D->H copy not implemented for negative strides'
+        self.assertIn(expected_msg, str(raises.exception))
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/numba/tests/test_dummyarray.py b/numba/tests/test_dummyarray.py
--- a/numba/tests/test_dummyarray.py
+++ b/numba/tests/test_dummyarray.py
@@ -117,6 +117,44 @@ def test_slice2_2d(self):
             self.assertEqual(got.shape, expect.shape)
             self.assertEqual(got.strides, expect.strides)
 
+    #### Strided
+
+    def test_strided_1d(self):
+        nparr = np.empty(4)
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+        xx = -2, -1, 1, 2
+        for x in xx:
+            expect = nparr[::x]
+            got = arr[::x]
+            self.assertSameContig(got, expect)
+            self.assertEqual(got.shape, expect.shape)
+            self.assertEqual(got.strides, expect.strides)
+
+    def test_strided_2d(self):
+        nparr = np.empty((4, 5))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+        xx = -2, -1, 1, 2
+        for a, b in itertools.product(xx, xx):
+            expect = nparr[::a, ::b]
+            got = arr[::a, ::b]
+            self.assertSameContig(got, expect)
+            self.assertEqual(got.shape, expect.shape)
+            self.assertEqual(got.strides, expect.strides)
+
+    def test_strided_3d(self):
+        nparr = np.empty((4, 5, 6))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+        xx = -2, -1, 1, 2
+        for a, b, c in itertools.product(xx, xx, xx):
+            expect = nparr[::a, ::b, ::c]
+            got = arr[::a, ::b, ::c]
+            self.assertSameContig(got, expect)
+            self.assertEqual(got.shape, expect.shape)
+            self.assertEqual(got.strides, expect.strides)
+
 
 class TestReshape(unittest.TestCase):
     def test_reshape_2d2d(self):
@@ -190,7 +228,7 @@ def test_squeeze_invalid_axis(self):
             arr.squeeze(axis=1)
         with self.assertRaises(ValueError):
             arr.squeeze(axis=(2, 3))
-        
+
 
 class TestExtent(unittest.TestCase):
     def test_extent_1d(self):