Repository: numba/numba

Issue:
`cuda.to_device()` cannot copy zero-element data
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [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).

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->

Not sure if this is worth fixing. I found it when trying to use Numba CUDA arrays as the backend for mpi4py's GPU tests. Most likely to fix this we need a check in `to_device()` before copying?
```python
>>> from numba import cuda
>>> # in our tests we loop over N from 0 to a fixed positive integer,
... # and only N=0 caused the error
>>> N = 0  
>>> a = range(N)  
>>> arr = cuda.device_array(len(a), 'f')
>>> arr[:] = cuda.to_device(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/cudadrv/devices.py", line 225, in _require_cuda_context
    return fn(*args, **kws)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/cudadrv/devicearray.py", line 530, in __setitem__
    return self._do_setitem(key, value)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/cudadrv/devicearray.py", line 581, in _do_setitem
    _assign_kernel(lhs.ndim).forall(n_elements, stream=stream)(lhs, rhs)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/compiler.py", line 275, in __call__
    sharedmem=self.sharedmem)(*args)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/compiler.py", line 537, in __call__
    sharedmem=self.sharedmem)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/compiler.py", line 611, in _kernel_call
    cu_func(*kernelargs)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/cudadrv/driver.py", line 1578, in __call__
    self.sharedmem, streamhandle, args)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/cudadrv/driver.py", line 1622, in launch_kernel
    None)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/cudadrv/driver.py", line 294, in safe_cuda_api_call
    self._check_error(fname, retcode)
  File "/home/leofang/conda_envs/mpi4py_dev/lib/python3.6/site-packages/numba/cuda/cudadrv/driver.py", line 329, in _check_error
    raise CudaAPIError(retcode, msg)
numba.cuda.cudadrv.driver.CudaAPIError: [1] Call to cuLaunchKernel results in CUDA_ERROR_INVALID_VALUE
```


Discussion:
Thanks for the report, I can reproduce. Whilst perhaps not often encountered in practice it'd be a good idea for a more suitable error message to appear!
I think the issue is when `DeviceNDArray.__setitem__` is called:

```python
In [1]: from numba import cuda                                                                                                                               

In [2]: N = 0                                                                                                                                                

In [3]: a = range(N)                                                                                                                                         

In [4]: arr = cuda.device_array(len(a), 'f')                                                                                                                 

In [5]: a_to_d = cuda.to_device(a)                                                                                                                           

In [6]: arr[:] = a_to_d                                                                                                                                      
---------------------------------------------------------------------------
CudaAPIError                              Traceback (most recent call last)
<ipython-input-6-98648c96befd> in <module>
----> 1 arr[:] = a_to_d

~/numbadev/numba/numba/cuda/cudadrv/devices.py in _require_cuda_context(*args, **kws)
    223     def _require_cuda_context(*args, **kws):
    224         with _runtime.ensure_context():
--> 225             return fn(*args, **kws)
    226 
    227     return _require_cuda_context

~/numbadev/numba/numba/cuda/cudadrv/devicearray.py in __setitem__(self, key, value)
    528     @devices.require_context
    529     def __setitem__(self, key, value):
--> 530         return self._do_setitem(key, value)
    531 
    532     def setitem(self, key, value, stream=0):

~/numbadev/numba/numba/cuda/cudadrv/devicearray.py in _do_setitem(self, key, value, stream)
    579 
    580         n_elements = functools.reduce(operator.mul, lhs.shape, 1)
--> 581         _assign_kernel(lhs.ndim).forall(n_elements, stream=stream)(lhs, rhs)
    582 
    583 

~/numbadev/numba/numba/cuda/compiler.py in __call__(self, *args)
    273 
    274         return kernel.configure(blkct, tpb, stream=self.stream,
--> 275                                 sharedmem=self.sharedmem)(*args)
    276 
    277     def _compute_thread_per_block(self, kernel):

~/numbadev/numba/numba/cuda/compiler.py in __call__(self, *args, **kwargs)
    535                           blockdim=self.blockdim,
    536                           stream=self.stream,
--> 537                           sharedmem=self.sharedmem)
    538 
    539     def bind(self):

~/numbadev/numba/numba/cuda/compiler.py in _kernel_call(self, args, griddim, blockdim, stream, sharedmem)
    609                                    sharedmem=sharedmem)
    610         # Invoke kernel
--> 611         cu_func(*kernelargs)
    612 
    613         if self.debug:

~/numbadev/numba/numba/cuda/cudadrv/driver.py in __call__(self, *args)
   1576 
   1577         launch_kernel(self.handle, self.griddim, self.blockdim,
-> 1578                       self.sharedmem, streamhandle, args)
   1579 
   1580     @property

~/numbadev/numba/numba/cuda/cudadrv/driver.py in launch_kernel(cufunc_handle, griddim, blockdim, sharedmem, hstream, args)
   1620                           hstream,
   1621                           params,
-> 1622                           None)
   1623 
   1624 

~/numbadev/numba/numba/cuda/cudadrv/driver.py in safe_cuda_api_call(*args)
    292             _logger.debug('call driver api: %s', libfn.__name__)
    293             retcode = libfn(*args)
--> 294             self._check_error(fname, retcode)
    295         return safe_cuda_api_call
    296 

~/numbadev/numba/numba/cuda/cudadrv/driver.py in _check_error(self, fname, retcode)
    327                     _logger.critical(msg, _getpid(), self.pid)
    328                     raise CudaDriverError("CUDA initialized before forking")
--> 329             raise CudaAPIError(retcode, msg)
    330 
    331     def get_device(self, devnum=0):

CudaAPIError: [1] Call to cuLaunchKernel results in CUDA_ERROR_INVALID_VALUE
```

There are a couple of places where we could make an improvement:

1. Assigning the slice is implemented using a `ForAll`, which will erroneously configure and launch a kernel with 0 elements of work with 0 blocks, causing the `CUDA_ERROR_INVALID_VALUE`. Its `__call__` method could/should just return when it has 0 elements of work to do.
2. Even with the first improvement, `DeviceNDArray._do_getitem` should maybe not create the `_assign_kernel` and call `forall` on it, to avoid doing unnecessary work

I'm leaning towards doing 1 and not 2, since 2 seems a bit of a micro-/premature-optimisation - assigning zero-length slices will not be a particularly common case, and the assign kernels are cached, so calling it won't be likely to be perceptible anyway.

Resolving patch:
diff --git a/numba/cuda/compiler.py b/numba/cuda/compiler.py
--- a/numba/cuda/compiler.py
+++ b/numba/cuda/compiler.py
@@ -254,6 +254,9 @@ def __init__(self, name, sig):
 
 class ForAll(object):
     def __init__(self, kernel, ntasks, tpb, stream, sharedmem):
+        if ntasks < 0:
+            raise ValueError("Can't create ForAll with negative task count: %s"
+                             % ntasks)
         self.kernel = kernel
         self.ntasks = ntasks
         self.thread_per_block = tpb
@@ -261,6 +264,9 @@ def __init__(self, kernel, ntasks, tpb, stream, sharedmem):
         self.sharedmem = sharedmem
 
     def __call__(self, *args):
+        if self.ntasks == 0:
+            return
+
         if isinstance(self.kernel, AutoJitCUDAKernel):
             kernel = self.kernel.specialize(*args)
         else:
diff --git a/numba/cuda/simulator/cudadrv/devicearray.py b/numba/cuda/simulator/cudadrv/devicearray.py
--- a/numba/cuda/simulator/cudadrv/devicearray.py
+++ b/numba/cuda/simulator/cudadrv/devicearray.py
@@ -188,6 +188,7 @@ def check_array_compatibility(ary1, ary2):
 
 
 def to_device(ary, stream=0, copy=True, to=None):
+    ary = np.array(ary, copy=False, subok=True)
     sentry_contiguous(ary)
     if to is None:
         buffer_dtype = np.int64 if ary.dtype.char in 'Mm' else ary.dtype
diff --git a/numba/cuda/simulator/kernel.py b/numba/cuda/simulator/kernel.py
--- a/numba/cuda/simulator/kernel.py
+++ b/numba/cuda/simulator/kernel.py
@@ -115,6 +115,9 @@ def specialize(self, *args):
         return self
 
     def forall(self, ntasks, tpb=0, stream=0, sharedmem=0):
+        if ntasks < 0:
+            raise ValueError("Can't create ForAll with negative task count: %s"
+                             % ntasks)
         return self[ntasks, 1, stream, sharedmem]
 
     @property


Test patch:
diff --git a/numba/cuda/tests/cudapy/test_forall.py b/numba/cuda/tests/cudapy/test_forall.py
--- a/numba/cuda/tests/cudapy/test_forall.py
+++ b/numba/cuda/tests/cudapy/test_forall.py
@@ -7,14 +7,15 @@
 from numba.cuda.testing import SerialMixin
 
 
+@cuda.jit
+def foo(x):
+    i = cuda.grid(1)
+    if i < x.size:
+        x[i] += 1
+
+
 class TestForAll(SerialMixin, unittest.TestCase):
     def test_forall_1(self):
-        @cuda.jit
-        def foo(x):
-            i = cuda.grid(1)
-            if i < x.size:
-                x[i] += 1
-
         arr = np.arange(11)
         orig = arr.copy()
         foo.forall(arr.size)(arr)
@@ -34,6 +35,20 @@ def bar(a, x, y):
         bar.forall(y.size)(a, x, y)
         np.testing.assert_array_almost_equal(y, a * x + oldy, decimal=3)
 
+    def test_forall_no_work(self):
+        # Ensure that forall doesn't launch a kernel with no blocks when called
+        # with 0 elements. See Issue #5017.
+        arr = np.arange(11)
+        foo.forall(0)(arr)
+
+    def test_forall_negative_work(self):
+        # Ensure that forall doesn't allow the creation of a forall with a
+        # negative element count.
+        with self.assertRaises(ValueError) as raises:
+            foo.forall(-1)
+        self.assertIn("Can't create ForAll with negative task count",
+                      str(raises.exception))
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/numba/cuda/tests/cudapy/test_slicing.py b/numba/cuda/tests/cudapy/test_slicing.py
--- a/numba/cuda/tests/cudapy/test_slicing.py
+++ b/numba/cuda/tests/cudapy/test_slicing.py
@@ -25,6 +25,13 @@ def test_slice_as_arg(self):
 
         cucopy[1, 10](inp, out)
 
+    def test_assign_empty_slice(self):
+        # Issue #5017. Assigning to an empty slice should not result in a
+        # CudaAPIError.
+        N = 0
+        a = range(N)
+        arr = cuda.device_array(len(a))
+        arr[:] = cuda.to_device(a)
 
 if __name__ == '__main__':
     unittest.main()