Repository: numba/numba

Issue:
Copy to host broken for __cuda_array_interface__-derived strided DeviceNDArray.
The copy-to-host interface of strided DeviceNDArray instances created via numba.cuda.as_cuda_arrary is currently broken, due an error in allocation of the result buffer. 

This can be reproduced by creating an "externally managed' view of an existing DeviceNDArray:

### Repro Script

```
$ cat repro_copy.py
import numpy
import numba.cuda

arr = numba.cuda.to_device(numpy.arange(100))
numpy.testing.assert_array_equal(arr.copy_to_host(), numpy.arange(100))

ext_arr = numba.cuda.as_cuda_array(arr)
numpy.testing.assert_array_equal(ext_arr.copy_to_host(), numpy.arange(100))

ext_strided_arr = numba.cuda.as_cuda_array(arr[::2])
numpy.testing.assert_array_equal(ext_strided_arr.copy_to_host(), numpy.arange(100)[::2])
```

### Repro result
```
$ python repro_copy.py
Traceback (most recent call last):
  File "repro_copy.py", line 11, in <module>
    numpy.testing.assert_array_equal(ext_strided_arr.copy_to_host(), numpy.arange(100)[::2])
  File "/scratch/USERS/fordas/workspace/numba_min/.conda/lib/python3.7/site-packages/numba/cuda/cudadrv/devices.py", line 212, in _require_cuda_context
    return fn(*args, **kws)
  File "/scratch/USERS/fordas/workspace/numba_min/.conda/lib/python3.7/site-packages/numba/cuda/cudadrv/devicearray.py", line 260, in copy_to_host
    strides=self.strides, buffer=hostary)
ValueError: strides is incompatible with shape of requested array and size of buffer
```

### Test Env
```
$ cat environment.yml
channels:
  - numba
dependencies:
  - numba
  - cudatoolkit



$ conda list
# packages in environment at /scratch/USERS/fordas/workspace/numba_min/.conda:
#
# Name                    Version                   Build  Channel
blas                      1.0                         mkl
ca-certificates           2018.03.07                    0
certifi                   2018.8.24                py37_1
cudatoolkit               9.1                  h85f986d_0    numba
intel-openmp              2019.0                      118
libedit                   3.1.20170329         h6b74fdf_2
libffi                    3.2.1                hd88cf55_4
libgcc-ng                 8.2.0                hdf63c60_1
libgfortran-ng            7.3.0                hdf63c60_0
libstdcxx-ng              8.2.0                hdf63c60_1
llvmlite                  0.25.0           py37hf484d3e_0    numba
mkl                       2019.0                      118
mkl_fft                   1.0.4            py37h4414c95_1
mkl_random                1.0.1            py37h4414c95_1
ncurses                   6.1                  hf484d3e_0
numba                     0.41.0dev0      np115py37hf484d3e_17    numba
numpy                     1.15.1           py37h1d66e8a_0
numpy-base                1.15.1           py37h81de0dd_0
openssl                   1.0.2p               h14c3975_0
pip                       10.0.1                   py37_0
python                    3.7.0                hc3d631a_0
readline                  7.0                  h7b6447c_5
setuptools                40.2.0                   py37_0
sqlite                    3.24.0               h84994c4_0
tk                        8.6.8                hbc83047_0
wheel                     0.31.1                   py37_0
xz                        5.2.4                h14c3975_4
zlib                      1.2.11               ha838bed_2
```


Discussion:

Resolving patch:
diff --git a/numba/cuda/api.py b/numba/cuda/api.py
--- a/numba/cuda/api.py
+++ b/numba/cuda/api.py
@@ -36,11 +36,11 @@ def from_cuda_array_interface(desc, owner=None):
 
     shape, strides, dtype = _prepare_shape_strides_dtype(
         shape, strides, dtype, order='C')
+    size = driver.memory_size_from_info(shape, strides, dtype.itemsize)
 
     devptr = driver.get_devptr_for_active_ctx(desc['data'][0])
     data = driver.MemoryPointer(
-        current_context(), devptr, size=np.prod(shape) * dtype.itemsize,
-        owner=owner)
+        current_context(), devptr, size=size, owner=owner)
     da = devicearray.DeviceNDArray(shape=shape, strides=strides,
                                    dtype=dtype, gpu_data=data)
     return da
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
@@ -1211,7 +1211,13 @@ def view(self, start, stop=None):
                 raise RuntimeError('size cannot be negative')
             pointer = drvapi.cu_device_ptr(base)
             view = MemoryPointer(self.context, pointer, size, owner=self.owner)
-        return OwnedPointer(weakref.proxy(self.owner), view)
+
+        if isinstance(self.owner, (MemoryPointer, OwnedPointer)):
+            # Owned by a numba-managed memory segment, take an owned reference
+            return OwnedPointer(weakref.proxy(self.owner), view)
+        else:
+            # Owned by external alloc, return view with same external owner
+            return view
 
     @property
     def device_ctypes_pointer(self):


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
@@ -106,6 +106,70 @@ def vadd(inp, val, out):
         self.assertEqual(returned.device_ctypes_pointer.value,
                          out._arr.device_ctypes_pointer.value)
 
-
-if __name__ == '__main__':
+    def test_array_views(self):
+        """Views created via array interface support:
+            - Strided slices
+            - Strided slices
+        """
+        h_arr = np.random.random(10)
+        c_arr = cuda.to_device(h_arr)
+
+        arr = cuda.as_cuda_array(c_arr)
+
+        # __getitem__ interface accesses expected data
+
+        # Direct views
+        np.testing.assert_array_equal(arr.copy_to_host(), h_arr)
+        np.testing.assert_array_equal(arr[:].copy_to_host(), h_arr)
+
+        # Slicing
+        np.testing.assert_array_equal(arr[:5].copy_to_host(), h_arr[:5])
+
+        # Strided view
+        np.testing.assert_array_equal(arr[::2].copy_to_host(), h_arr[::2])
+
+        # View of strided array
+        arr_strided = cuda.as_cuda_array(c_arr[::2])
+        np.testing.assert_array_equal(arr_strided.copy_to_host(), h_arr[::2])
+
+        # A strided-view-of-array and view-of-strided-array have the same
+        # shape, strides, itemsize, and alloc_size
+        self.assertEqual(arr[::2].shape, arr_strided.shape)
+        self.assertEqual(arr[::2].strides, arr_strided.strides)
+        self.assertEqual(arr[::2].dtype.itemsize, arr_strided.dtype.itemsize)
+        self.assertEqual(arr[::2].alloc_size, arr_strided.alloc_size)
+
+        # __setitem__ interface propogates into external array
+
+        # Writes to a slice
+        arr[:5] = np.pi
+        np.testing.assert_array_equal(
+            c_arr.copy_to_host(),
+            np.concatenate((np.full(5, np.pi), h_arr[5:]))
+        )
+
+        # Writes to a slice from a view
+        arr[:5] = arr[5:]
+        np.testing.assert_array_equal(
+            c_arr.copy_to_host(),
+            np.concatenate((h_arr[5:], h_arr[5:]))
+        )
+
+        # Writes through a view
+        arr[:] = cuda.to_device(h_arr)
+        np.testing.assert_array_equal(c_arr.copy_to_host(), h_arr)
+
+        # Writes to a strided slice
+        arr[::2] = np.pi
+        np.testing.assert_array_equal(
+            c_arr.copy_to_host()[::2],
+            np.full(5, np.pi),
+        )
+        np.testing.assert_array_equal(
+            c_arr.copy_to_host()[1::2],
+            h_arr[1::2]
+        )
+
+
+if __name__ == "__main__":
     unittest.main()