Repository: numba/numba

Issue:
@guvectorize with target='cuda' ?
Dear all, 

I've just started using numba, and investigating how to run code on my GPU using it.

The following minimal example runs successfully:

    import numpy as np
    from numba import vectorize, guvectorize, int32
    
    @vectorize([int32(int32)])
    def f(x):
        return x + 2
    
    vec = np.array([1, 2, 3, 4], dtype=np.int32)
    print(f(vec))
    [3 4 5 6]

I also found out about @guvectorize - one equivalent way to implement is then:

    @guvectorize(['void(int32[:],int32[:],int32[:])'], '(n),()->(n)')
    def g(x,y,res):
        for i in range(x.shape[0]):
            res[i] = x[i] + y[0]
    
    res = np.zeros(4, dtype=np.int32)
    a = np.array([1, 2, 3, 4], dtype=np.int32)
    b = np.array([2], dtype=np.int32)
    g(a,b,res)
    print(res)
    [3 4 5 6]

Now, if I try to use `target='cuda'` in both `@vectorize` and `@guvectorize`, the former succeeds, and the latter fails (even for such a trivial case) :

    [...]
    TypingError: Failed at nopython (nopython frontend)
    Invalid usage of Function(<DeviceFunction py_func=<function g [...]

Is this to be expected? 
I'm very interested in the flexibility offered by @guvectorize (e.g. tensor operations with spec such as `(m,n),(p,m,n)->(p)`) - even more if cuda-powered :) 


Discussion:
There's definitely a bug here in the cuda target.  The scalar input (arg `y`) is mishandled.

Resolving patch:
diff --git a/numba/npyufunc/deviceufunc.py b/numba/npyufunc/deviceufunc.py
--- a/numba/npyufunc/deviceufunc.py
+++ b/numba/npyufunc/deviceufunc.py
@@ -462,17 +462,17 @@ def add(self, sig=None, argtypes=None, restype=None):
 
         indims = [len(x) for x in self.inputsig]
         outdims = [len(x) for x in self.outputsig]
+        args, return_type = sigutils.normalize_signature(sig)
 
         funcname = self.py_func.__name__
         src = expand_gufunc_template(self._kernel_template, indims,
-                                     outdims, funcname)
+                                     outdims, funcname, args)
 
         glbls = self._get_globals(sig)
 
         _exec(src, glbls)
         fnobj = glbls['__gufunc_{name}'.format(name=funcname)]
 
-        args, return_type = sigutils.normalize_signature(sig)
         outertys = list(_determine_gufunc_outer_types(args, indims + outdims))
         kernel = self._compile_kernel(fnobj, sig=tuple(outertys))
 
@@ -496,17 +496,18 @@ def _determine_gufunc_outer_types(argtys, dims):
             yield types.Array(dtype=at, ndim=1, layout='A')
 
 
-def expand_gufunc_template(template, indims, outdims, funcname):
+def expand_gufunc_template(template, indims, outdims, funcname, argtypes):
     """Expand gufunc source template
     """
     argdims = indims + outdims
     argnames = ["arg{0}".format(i) for i in range(len(argdims))]
     checkedarg = "min({0})".format(', '.join(["{0}.shape[0]".format(a)
                                               for a in argnames]))
-    inputs = [_gen_src_for_indexing(aref, adims, _gen_src_for_input_indexing)
-              for aref, adims in zip(argnames, indims)]
-    outputs = [_gen_src_for_indexing(aref, adims, _gen_src_for_output_indexing)
-               for aref, adims in zip(argnames[len(indims):], outdims)]
+    inputs = [_gen_src_for_indexing(aref, adims, atype)
+              for aref, adims, atype in zip(argnames, indims, argtypes)]
+    outputs = [_gen_src_for_indexing(aref, adims, atype)
+               for aref, adims, atype in zip(argnames[len(indims):], outdims,
+                                             argtypes)]
     argitems = inputs + outputs
     src = template.format(name=funcname, args=', '.join(argnames),
                           checkedarg=checkedarg,
@@ -514,28 +515,23 @@ def expand_gufunc_template(template, indims, outdims, funcname):
     return src
 
 
-def _gen_src_for_indexing(aref, adims, gen_sliced):
-    return "{aref}[{sliced}]".format(aref=aref, sliced=gen_sliced(adims))
+def _gen_src_for_indexing(aref, adims, atype):
+    return "{aref}[{sliced}]".format(aref=aref,
+                                     sliced=_gen_src_index(adims, atype))
 
 
-def _gen_src_for_input_indexing(adims):
+def _gen_src_index(adims, atype):
     if adims > 0:
-        return _gen_src_for_array_indexing(adims)
+        return ','.join(['__tid__'] + [':'] * adims)
+    elif isinstance(atype, types.Array) and atype.ndim - 1 == adims:
+        # Special case for 0-nd in shape-signature but
+        # 1d array in type signature.
+        # Slice it so that the result has the same dimension.
+        return '__tid__:(__tid__ + 1)'
     else:
         return '__tid__'
 
 
-def _gen_src_for_output_indexing(adims):
-    if adims > 0:
-        return _gen_src_for_array_indexing(adims)
-    else:
-        return '__tid__:(__tid__ + 1)'
-
-
-def _gen_src_for_array_indexing(adims):
-    return ','.join(['__tid__'] + [':'] * adims)
-
-
 class GUFuncEngine(object):
     '''Determine how to broadcast and execute a gufunc
     base on input shape and signature
@@ -641,7 +637,8 @@ def __init__(self, kernelmap, engine):
         assert self.engine.nout == 1, "only support single output"
 
     def __call__(self, *args, **kws):
-        callsteps = self._call_steps(args, kws)
+        callsteps = self._call_steps(self.engine.nin, self.engine.nout,
+                                     args, kws)
         callsteps.prepare_inputs()
         indtypes, schedule, outdtype, kernel = self._schedule(
             callsteps.norm_inputs, callsteps.output)
@@ -741,7 +738,9 @@ class GUFuncCallSteps(object):
         '_need_device_conversion',
     ]
 
-    def __init__(self, args, kwargs):
+    def __init__(self, nin, nout, args, kwargs):
+        if nout > 1:
+            raise ValueError('multiple output is not supported')
         self.args = args
         self.kwargs = kwargs
 
@@ -755,8 +754,16 @@ def __init__(self, args, kwargs):
             if isdev:
                 inputs.append(a)
             else:
-                inputs.append(np.array(a))
-        self.norm_inputs = inputs
+                inputs.append(np.asarray(a))
+        self.norm_inputs = inputs[:nin]
+        # Check if there are extra arguments for outputs.
+        unused_inputs = inputs[nin:]
+        if unused_inputs:
+            if self.output is not None:
+                raise ValueError("cannot specify 'out' as both a positional "
+                                 "and keyword argument")
+            else:
+                [self.output] = unused_inputs
 
     def adjust_input_types(self, indtypes):
         """


Test patch:
diff --git a/numba/cuda/tests/cudapy/test_gufunc.py b/numba/cuda/tests/cudapy/test_gufunc.py
--- a/numba/cuda/tests/cudapy/test_gufunc.py
+++ b/numba/cuda/tests/cudapy/test_gufunc.py
@@ -287,6 +287,17 @@ def foo(A, B):
         items = [i.strip("'\" ") for i in items]
         self.assertEqual(set(['what1', 'ever2']), set(items))
 
+    def test_duplicated_output(self):
+        @guvectorize([void(float32[:], float32[:])], '(x)->(x)', target='cuda')
+        def foo(inp, out):
+            pass  # intentionally empty; never executed
+
+        inp = out = np.zeros(10, dtype=np.float32)
+        with self.assertRaises(ValueError) as raises:
+            foo(inp, out, out=out)
+        self.assertEqual(str(raises.exception),
+            "cannot specify 'out' as both a positional and keyword argument")
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/numba/cuda/tests/cudapy/test_gufunc_scalar.py b/numba/cuda/tests/cudapy/test_gufunc_scalar.py
--- a/numba/cuda/tests/cudapy/test_gufunc_scalar.py
+++ b/numba/cuda/tests/cudapy/test_gufunc_scalar.py
@@ -110,7 +110,40 @@ def foo(a, b, out):
 
         self.assertIn("does not support .astype()", str(raises.exception))
 
+    def test_gufunc_old_style_scalar_as_array(self):
+        # Example from issue #2579
+        @guvectorize(['void(int32[:],int32[:],int32[:])'], '(n),()->(n)',
+                     target='cuda')
+        def gufunc(x, y, res):
+            for i in range(x.shape[0]):
+                res[i] = x[i] + y[0]
+
+        # Case 1
+        a = np.array([1, 2, 3, 4], dtype=np.int32)
+        b = np.array([2], dtype=np.int32)
+
+        res = np.zeros(4, dtype=np.int32)
+
+        expected = res.copy()
+        expected = a + b
+
+        gufunc(a, b, out=res)
+
+        np.testing.assert_almost_equal(expected, res)
+
+        # Case 2
+        a = np.array([1, 2, 3, 4] * 2, dtype=np.int32).reshape(2, 4)
+        b = np.array([2, 10], dtype=np.int32)
+
+        res = np.zeros((2, 4), dtype=np.int32)
+
+        expected = res.copy()
+        expected[0] = a[0] + b[0]
+        expected[1] = a[1] + b[1]
+
+        gufunc(a, b, res)
 
+        np.testing.assert_almost_equal(expected, res)
 
 
 if __name__ == '__main__':