Repository: numba/numba

Issue:
cmath linking issues in CUDA
From the mailing list:
https://groups.google.com/a/continuum.io/forum/#!topic/numba-users/oBcaFcsD-x4
This:
```python
from numba import cuda
import cmath
import numpy as np

@cuda.jit(device = True)
def quarticRoots(a, b, c, d, e):

	del_0 = c**2 - 3.*b*d + 12.*a*e
	del_1 = 2.*c**3 - 9.*b*c*d + 27*e*b**2 + 27*a*d**2 - 72*a*c*e

	p = (8.*a*c - 3.*b**2)/(8.*a**2)
	q = (b**3 - 4.*a*b*c + 8.*d*a**2)/(8.*a**3)

	Q = ((del_1 + cmath.sqrt(del_1**2 - 4.*del_0**3))/2.)**(1./3.)#**(1./3.) is the problem.
	S = 0.5*cmath.sqrt(-2./3.*p + 1/(3.*a)*(Q + del_0/Q))

	x1 = -b/(4.*a) - S + 0.5*cmath.sqrt(-4.*S**2 - 2.*p + q/S)
	x2 = -b/(4.*a) - S - 0.5*cmath.sqrt(-4.*S**2 - 2.*p + q/S)
	x3 = -b/(4.*a) + S + 0.5*cmath.sqrt(-4.*S**2 - 2.*p - q/S)
	x4 = -b/(4.*a) + S - 0.5*cmath.sqrt(-4.*S**2 - 2.*p - q/S)

	return (x1, x2, x3, x4)

@cuda.jit
def launcher(d_array):
	output = quarticRoots(2.,4.,2.,1.,1.)

	d_array[0] = output[0]
	d_array[1] = output[1]
	d_array[2] = output[2]
	d_array[3] = output[3]

def main():
	d_array = cuda.device_array(4, dtype = np.complex64)
	launcher[1,1](d_array)
	output = d_array.copy_to_host()
	print output

main()
```
yields:
```python
Traceback (most recent call last):
  File "<path>numba/numba/cuda/cudadrv/driver.py", line 1432, in complete
    driver.cuLinkComplete(self.handle, byref(cubin), byref(size))
  File "<path>numba/numba/cuda/cudadrv/driver.py", line 288, in safe_cuda_api_call
    self._check_error(fname, retcode)
  File "<path>numba/numba/cuda/cudadrv/driver.py", line 323, in _check_error
    raise CudaAPIError(retcode, msg)
numba.cuda.cudadrv.driver.CudaAPIError: [999] Call to cuLinkComplete results in CUDA_ERROR_UNKNOWN

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "issue_13.py", line 39, in <module>
    main()
  File "issue_13.py", line 35, in main
    launcher[1,1](d_array)
  File "<path>numba/numba/cuda/compiler.py", line 701, in __call__
    kernel = self.specialize(*args)
  File "<path>numba/numba/cuda/compiler.py", line 712, in specialize
    kernel = self.compile(argtypes)
  File "<path>numba/numba/cuda/compiler.py", line 730, in compile
    kernel.bind()
  File "<path>numba/numba/cuda/compiler.py", line 489, in bind
    self._func.get()
  File "<path>numba/numba/cuda/compiler.py", line 377, in get
    cubin, _size = linker.complete()
  File "<path>numba/numba/cuda/cudadrv/driver.py", line 1434, in complete
    raise LinkerError("%s\n%s" % (e, self.error_log))
numba.cuda.cudadrv.driver.LinkerError: [999] Call to cuLinkComplete results in CUDA_ERROR_UNKNOWN
error   : Undefined reference to 'numba_cpow' in '<cudapy-ptx>'
```
the `cmath.sqrt()` creates a complex domain result with is then raised to the power `1/3`, this ends up as a call to `numba_cpow` which is fine on the CPU but doesn't have an impl in CUDA, as a result there's a linking error.

Two issues:
1. Can the additional cmath function impls for the CPU be done for CUDA.
2. If not can this problem be caught earlier in the complication chain and a better message produced such that is doesn't appear as a link error.



Discussion:

Resolving patch:
diff --git a/numba/cuda/libdevice.py b/numba/cuda/libdevice.py
--- a/numba/cuda/libdevice.py
+++ b/numba/cuda/libdevice.py
@@ -1,4 +1,5 @@
 import math
+import operator
 from llvmlite.llvmpy.core import Type
 from numba.core import types, cgutils
 from numba.core.imputils import Registry
@@ -164,3 +165,41 @@ def core(context, builder, sig, args):
 for (ty, intrin) in ((types.float64, '__nv_modf',),
                      (types.float32, '__nv_modff',)):
     lower(math.modf, ty)(modf_implement(intrin, ty))
+
+
+# Complex power implementations - translations of _Py_c_pow from CPython
+# https://github.com/python/cpython/blob/a755410e054e1e2390de5830befc08fe80706c66/Objects/complexobject.c#L123-L151
+#
+# The complex64 variant casts all constants and some variables to ensure that
+# as much computation is done in single precision as possible. A small number
+# of operations are still done in 64-bit, but these come from libdevice code.
+
+def cpow_implement(fty, cty):
+    def core(context, builder, sig, args):
+        def cpow_internal(a, b):
+
+            if b.real == fty(0.0) and b.imag == fty(0.0):
+                return cty(1.0) + cty(0.0j)
+            elif a.real == fty(0.0) and b.real == fty(0.0):
+                return cty(0.0) + cty(0.0j)
+
+            vabs = math.hypot(a.real, a.imag)
+            len = math.pow(vabs, b.real)
+            at = math.atan2(a.imag, a.real)
+            phase = at * b.real
+            if b.imag != fty(0.0):
+                len /= math.exp(at * b.imag)
+                phase += b.imag * math.log(vabs)
+
+            return len * (cty(math.cos(phase)) +
+                          cty(math.sin(phase) * cty(1.0j)))
+
+        return context.compile_internal(builder, cpow_internal, sig, args)
+
+    lower(operator.pow, cty, cty)(core)
+    lower(operator.ipow, cty, cty)(core)
+    lower(pow, cty, cty)(core)
+
+
+cpow_implement(types.float32, types.complex64)
+cpow_implement(types.float64, types.complex128)


Test patch:
diff --git a/numba/cuda/tests/cudapy/test_powi.py b/numba/cuda/tests/cudapy/test_powi.py
--- a/numba/cuda/tests/cudapy/test_powi.py
+++ b/numba/cuda/tests/cudapy/test_powi.py
@@ -24,6 +24,32 @@ def cu_mat_power_binop(A, power, power_A):
     power_A[y, x] = A[y, x] ** power
 
 
+def vec_pow(r, x, y):
+    i = cuda.grid(1)
+
+    if i < len(r):
+        r[i] = pow(x[i], y[i])
+
+
+def vec_pow_binop(r, x, y):
+    i = cuda.grid(1)
+
+    if i < len(r):
+        r[i] = x[i] ** y[i]
+
+
+def vec_pow_inplace_binop(r, x):
+    i = cuda.grid(1)
+
+    if i < len(r):
+        r[i] **= x[i]
+
+
+def random_complex(N):
+    np.random.seed(123)
+    return (np.random.random(1) + np.random.random(1) * 1j)
+
+
 class TestCudaPowi(CUDATestCase):
     def test_powi(self):
         dec = cuda.jit(argtypes=[float64[:, :], int8, float64[:, :]])
@@ -45,6 +71,54 @@ def test_powi_binop(self):
         kernel[1, A.shape](A, power, Aout)
         self.assertTrue(np.allclose(Aout, A ** power))
 
+    # Relative tolerance kwarg is provided because 1.0e-7 (the default for
+    # assert_allclose) is a bit tight for single precision.
+    def _test_cpow(self, dtype, func, rtol=1.0e-7):
+        N = 32
+        x = random_complex(N).astype(dtype)
+        y = random_complex(N).astype(dtype)
+        r = np.zeros_like(x)
+
+        cfunc = cuda.jit(func)
+        cfunc[1, N](r, x, y)
+        np.testing.assert_allclose(r, x ** y, rtol=rtol)
+
+        # Checks special cases
+        x = np.asarray([0.0j, 1.0j], dtype=dtype)
+        y = np.asarray([0.0j, 1.0], dtype=dtype)
+        r = np.zeros_like(x)
+
+        cfunc[1, 2](r, x, y)
+        np.testing.assert_allclose(r, x ** y, rtol=rtol)
+
+    def test_cpow_complex64_pow(self):
+        self._test_cpow(np.complex64, vec_pow, rtol=3.0e-7)
+
+    def test_cpow_complex64_binop(self):
+        self._test_cpow(np.complex64, vec_pow_binop, rtol=3.0e-7)
+
+    def test_cpow_complex128_pow(self):
+        self._test_cpow(np.complex128, vec_pow)
+
+    def test_cpow_complex128_binop(self):
+        self._test_cpow(np.complex128, vec_pow_binop)
+
+    def _test_cpow_inplace_binop(self, dtype, rtol=1.0e-7):
+        N = 32
+        x = random_complex(N).astype(dtype)
+        y = random_complex(N).astype(dtype)
+        r = x ** y
+
+        cfunc = cuda.jit(vec_pow_inplace_binop)
+        cfunc[1, N](x, y)
+        np.testing.assert_allclose(x, r, rtol=rtol)
+
+    def test_cpow_complex64_inplace_binop(self):
+        self._test_cpow_inplace_binop(np.complex64, rtol=3.0e-7)
+
+    def test_cpow_complex128_inplace_binop(self):
+        self._test_cpow_inplace_binop(np.complex128, rtol=3.0e-7)
+
 
 if __name__ == '__main__':
     unittest.main()