Repository: numba/numba

Issue:
CUDA functions can be compiled but not called with Tuple and UniTuple arguments
As the title suggests, functions with `Tuple` or  `UniTuple` arguments can be compiled just fine, but are unable to actually be called without raising an exception. Here's an example using `UniTuple`:

```python
import numpy as np
from numba import cuda, types

n_array = 5
@cuda.jit(types.void(types.UniTuple(types.float32[:], n_array), types.float32[:]))
def test(arrays, out):
	i = cuda.threadIdx.x
	tmp = 0
	for arr in arrays:
		tmp += arr[i]
	out[i] = tmp

def main():
	n_elements = 3
	arrays_in = [k * np.ones(n_elements, np.float32) for k in range(n_array)]
	array_out = np.empty(n_elements, np.float32)
	test[1, n_elements](arrays_in, array_out)
	print("output:", array_out)

if __name__=="__main__": main()
```
I have no real knowledge about the internals of Numba,
but I found adding the following lines to `_prepare_args` of the `CUDAKernel` class:


```python
...
elif isinstance(ty, types.UniTuple) or isinstance(ty, types.Tuple):
    assert len(ty)==len(val)
    for t, v in zip(ty, val):
       self._prepare_args(t, v, stream, retr, kernelargs)
...
```

seems to fix the minimal example I provided.
I am using the latest version of Numba. 





Discussion:
Thanks for the report, I can reproduce. Seems like this could work, one of the core devs will take a look.
Would be wonderful if this was fixed.
@gmarkall : Can you take a look at this? (in your backlog of other CUDA things you are checking out...)  It does seem like a straightforward thing to fix since we unpack tuples, so there is no memory allocated data structure to worry about on the GPU.
Whoops, just noticed this mention - assigning to myself.
This fix is a lifesaver. 
- It enables otherwise impossible patterns for variable number of arguments (including but not limited to @heyer2's example)
- It makes code significantly more readable and modular

Supporting `namedtuple` is also possible with this approach
```python
elif isinstance(ty, (types.UniTuple, types.Tuple, types.NamedUniTuple, types.NamedTuple)):
    ...
```

Resolving patch:
diff --git a/numba/cuda/compiler.py b/numba/cuda/compiler.py
--- a/numba/cuda/compiler.py
+++ b/numba/cuda/compiler.py
@@ -756,6 +756,11 @@ def _prepare_args(self, ty, val, stream, retr, kernelargs):
             devrec = wrap_arg(val).to_device(retr, stream)
             kernelargs.append(devrec)
 
+        elif isinstance(ty, types.BaseTuple):
+            assert len(ty) == len(val)
+            for t, v in zip(ty, val):
+                self._prepare_args(t, v, stream, retr, kernelargs)
+
         else:
             raise NotImplementedError(ty, val)
 


Test patch:
diff --git a/numba/cuda/tests/cudapy/test_array_args.py b/numba/cuda/tests/cudapy/test_array_args.py
--- a/numba/cuda/tests/cudapy/test_array_args.py
+++ b/numba/cuda/tests/cudapy/test_array_args.py
@@ -1,4 +1,5 @@
 import numpy as np
+from collections import namedtuple
 
 from numba import cuda
 from numba.cuda.testing import unittest, CUDATestCase
@@ -21,6 +22,180 @@ def kernel(x, y):
         kernel[10, 1](x, y)
         self.assertTrue(np.all(x == y))
 
+    def test_unituple(self):
+        @cuda.jit
+        def f(r, x):
+            r[0] = x[0]
+            r[1] = x[1]
+            r[2] = x[2]
+
+        x = (1, 2, 3)
+        r = np.zeros(len(x), dtype=np.int64)
+        f[1, 1](r, x)
+
+        for i in range(len(x)):
+            self.assertEqual(r[i], x[i])
+
+    def test_tuple(self):
+        @cuda.jit
+        def f(r1, r2, x):
+            r1[0] = x[0]
+            r1[1] = x[1]
+            r1[2] = x[2]
+            r2[0] = x[3]
+            r2[1] = x[4]
+            r2[2] = x[5]
+
+        x = (1, 2, 3, 4.5, 5.5, 6.5)
+        r1 = np.zeros(len(x) // 2, dtype=np.int64)
+        r2 = np.zeros(len(x) // 2, dtype=np.float64)
+        f[1, 1](r1, r2, x)
+
+        for i in range(len(r1)):
+            self.assertEqual(r1[i], x[i])
+
+        for i in range(len(r2)):
+            self.assertEqual(r2[i], x[i + len(r1)])
+
+    def test_namedunituple(self):
+        @cuda.jit
+        def f(r, x):
+            r[0] = x.x
+            r[1] = x.y
+
+        Point = namedtuple('Point', ('x', 'y'))
+        x = Point(1, 2)
+        r = np.zeros(len(x), dtype=np.int64)
+        f[1, 1](r, x)
+
+        self.assertEqual(r[0], x.x)
+        self.assertEqual(r[1], x.y)
+
+    def test_namedtuple(self):
+        @cuda.jit
+        def f(r1, r2, x):
+            r1[0] = x.x
+            r1[1] = x.y
+            r2[0] = x.r
+
+        Point = namedtuple('Point', ('x', 'y', 'r'))
+        x = Point(1, 2, 2.236)
+        r1 = np.zeros(2, dtype=np.int64)
+        r2 = np.zeros(1, dtype=np.float64)
+        f[1, 1](r1, r2, x)
+
+        self.assertEqual(r1[0], x.x)
+        self.assertEqual(r1[1], x.y)
+        self.assertEqual(r2[0], x.r)
+
+    def test_empty_tuple(self):
+        @cuda.jit
+        def f(r, x):
+            r[0] = len(x)
+
+        x = tuple()
+        r = np.ones(1, dtype=np.int64)
+        f[1, 1](r, x)
+
+        self.assertEqual(r[0], 0)
+
+    def test_tuple_of_empty_tuples(self):
+        @cuda.jit
+        def f(r, x):
+            r[0] = len(x)
+            r[1] = len(x[0])
+
+        x = ((), (), ())
+        r = np.ones(2, dtype=np.int64)
+        f[1, 1](r, x)
+
+        self.assertEqual(r[0], 3)
+        self.assertEqual(r[1], 0)
+
+    def test_tuple_of_tuples(self):
+        @cuda.jit
+        def f(r, x):
+            r[0] = len(x)
+            r[1] = len(x[0])
+            r[2] = len(x[1])
+            r[3] = len(x[2])
+            r[4] = x[1][0]
+            r[5] = x[1][1]
+            r[6] = x[2][0]
+            r[7] = x[2][1]
+            r[8] = x[2][2]
+
+        x = ((), (5, 6), (8, 9, 10))
+        r = np.ones(9, dtype=np.int64)
+        f[1, 1](r, x)
+
+        self.assertEqual(r[0], 3)
+        self.assertEqual(r[1], 0)
+        self.assertEqual(r[2], 2)
+        self.assertEqual(r[3], 3)
+        self.assertEqual(r[4], 5)
+        self.assertEqual(r[5], 6)
+        self.assertEqual(r[6], 8)
+        self.assertEqual(r[7], 9)
+        self.assertEqual(r[8], 10)
+
+    def test_tuple_of_tuples_and_scalars(self):
+        @cuda.jit
+        def f(r, x):
+            r[0] = len(x)
+            r[1] = len(x[0])
+            r[2] = x[0][0]
+            r[3] = x[0][1]
+            r[4] = x[0][2]
+            r[5] = x[1]
+
+        x = ((6, 5, 4), 7)
+        r = np.ones(9, dtype=np.int64)
+        f[1, 1](r, x)
+
+        self.assertEqual(r[0], 2)
+        self.assertEqual(r[1], 3)
+        self.assertEqual(r[2], 6)
+        self.assertEqual(r[3], 5)
+        self.assertEqual(r[4], 4)
+        self.assertEqual(r[5], 7)
+
+    def test_tuple_of_arrays(self):
+        @cuda.jit
+        def f(x):
+            i = cuda.grid(1)
+            if i < len(x[0]):
+                x[0][i] = x[1][i] + x[2][i]
+
+        N = 10
+        x0 = np.zeros(N)
+        x1 = np.ones_like(x0)
+        x2 = x1 * 3
+        x = (x0, x1, x2)
+        f[1, N](x)
+
+        np.testing.assert_equal(x0, x1 + x2)
+
+    def test_tuple_of_array_scalar_tuple(self):
+        @cuda.jit
+        def f(r, x):
+            r[0] = x[0][0]
+            r[1] = x[0][1]
+            r[2] = x[1]
+            r[3] = x[2][0]
+            r[4] = x[2][1]
+
+        z = np.arange(2, dtype=np.int64)
+        x = (2 * z, 10, (4, 3))
+        r = np.zeros(5, dtype=np.int64)
+        f[1, 1](r, x)
+
+        self.assertEqual(r[0], 0)
+        self.assertEqual(r[1], 2)
+        self.assertEqual(r[2], 10)
+        self.assertEqual(r[3], 4)
+        self.assertEqual(r[4], 3)
+
 
 if __name__ == '__main__':
     unittest.main()