Repository: numba/numba

Issue:
Round segfaults in cuda jitted function if passed ndigits optional parameter
## Reporting a bug

- [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).

Reproducer:

```python
import numpy as np
from numba import cuda

@cuda.jit
def gpu_round(in_array, out_array, digits=1):
    i = cuda.grid(1)
    if i < in_array.size:
        out_array[i] = round(in_array[i], digits)

test_in = cuda.to_device(np.array([0.123, 0.4567, 0.78, 0.9]))
test_out = cuda.to_device(np.array([0., 0., 0., 0.]))

gpu_round(test_in, test_out, 2)
print(test_out.copy_to_host())
```

<details>
<summary>cuda-gdb backtrace:</summary>

```
(cuda-gdb) bt
#0  0x00007fff29bb0d85 in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#1  0x00007fff29bb8b84 in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#2  0x00007fff298a69ce in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#3  0x00007fff2985ba70 in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#4  0x00007fff2985d03b in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#5  0x00007fff2986774b in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#6  0x00007fff2986a81f in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#7  0x00007fff2977dbc5 in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#8  0x00007fff29780014 in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#9  0x00007fff29781550 in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#10 0x00007fff29a95ae8 in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#11 0x00007fff29a95cdb in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#12 0x00007fff29a94f6f in nvvmVerifyProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#13 0x00007fff295e5202 in ?? () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#14 0x00007fff295f7b1e in nvvmCompileProgram () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/libnvvm.so.3.3.0
#15 0x00007ffff7e4d630 in ffi_call_unix64 () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/python3.6/lib-dynload/../../libffi.so.6
#16 0x00007ffff7e4cfed in ffi_call () from /home/nfs/kkraus/miniconda3/envs/cudf_dev/lib/python3.6/lib-dynload/../../libffi.so.6
#17 0x00007fff2e9da00e in _call_function_pointer (argcount=3, resmem=0x7fffffffc310, restype=<optimized out>, atypes=0x7fffffffc2b0,
    avalues=0x7fffffffc2e0, pProc=0x7fff295f6dd0 <nvvmCompileProgram>, flags=4353)
    at /usr/local/src/conda/python-3.6.7/Modules/_ctypes/callproc.c:809
#18 _ctypes_callproc () at /usr/local/src/conda/python-3.6.7/Modules/_ctypes/callproc.c:1166
#19 0x00007fff2e9daa45 in PyCFuncPtr_call () at /usr/local/src/conda/python-3.6.7/Modules/_ctypes/_ctypes.c:3962
#20 0x00005555556677bb in _PyObject_FastCallDict ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2331
#21 0x00005555556f75be in call_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4861
#22 0x000055555571a73a in _PyEval_EvalFrameDefault ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3335
#23 0x00005555556c2056 in _PyEval_EvalCodeWithName ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4166
#24 0x00005555556c376a in _PyFunction_FastCallDict ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:5070
#25 0x000055555566799f in _PyObject_FastCallDict ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2310
#26 0x000055555566c613 in _PyObject_Call_Prepend ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2373
#27 0x00005555556673de in PyObject_Call () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2261
#28 0x000055555571be56 in do_call_core (kwdict=0x7ffff7f18510, callargs=0x7ffff7fa0048, func=0x7ffff7f92988)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:5106
#29 _PyEval_EvalFrameDefault () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3404
#30 0x00005555556c6749 in _PyEval_EvalCodeWithName (qualname=0x0, name=<optimized out>, closure=0x0, kwdefs=0x0, defcount=0, defs=0x0,
    kwstep=2, kwcount=<optimized out>, kwargs=0x7fffef42fae8, kwnames=0x7fffef42fae0, argcount=<optimized out>, args=0x555556f197b0,
    locals=0x0, globals=<optimized out>, _co=0x7ffff39355d0)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4166
#31 PyEval_EvalCodeEx () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4187
#32 0x00005555556c7676 in function_call ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/funcobject.c:604
#33 0x00005555556673de in PyObject_Call () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2261
#34 0x000055555571be56 in do_call_core (kwdict=0x7fffee6fc510, callargs=0x7fffee6d0e10, func=0x7ffff3947e18)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:5106
#35 _PyEval_EvalFrameDefault () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3404
#36 0x00005555556c2e4b in _PyFunction_FastCall (globals=<optimized out>, nargs=1, args=<optimized out>, co=<optimized out>)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4919
#37 fast_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4954
#38 0x00005555556f7545 in call_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4858
#39 0x000055555571a73a in _PyEval_EvalFrameDefault ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3335
#40 0x00005555556c2e4b in _PyFunction_FastCall (globals=<optimized out>, nargs=1, args=<optimized out>, co=<optimized out>)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4919
#41 fast_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4954
#42 0x00005555556f7545 in call_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4858
#43 0x000055555571a73a in _PyEval_EvalFrameDefault ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3335
#44 0x00005555556c2e4b in _PyFunction_FastCall (globals=<optimized out>, nargs=1, args=<optimized out>, co=<optimized out>)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4919
#45 fast_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4954
#46 0x00005555556f7545 in call_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4858
#47 0x000055555571a73a in _PyEval_EvalFrameDefault ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3335
#48 0x00005555556c2e4b in _PyFunction_FastCall (globals=<optimized out>, nargs=2, args=<optimized out>, co=<optimized out>)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4919
#49 fast_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4954
#50 0x00005555556f7545 in call_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4858
#51 0x000055555571a73a in _PyEval_EvalFrameDefault ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3335
#52 0x00005555556c248e in _PyEval_EvalCodeWithName ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4166
#53 0x00005555556c354c in _PyFunction_FastCallDict ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:5070
#54 0x000055555566799f in _PyObject_FastCallDict ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2310
#55 0x000055555566c613 in _PyObject_Call_Prepend ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2373
#56 0x00005555556673de in PyObject_Call () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2261
#57 0x000055555571be56 in do_call_core (kwdict=0x0, callargs=0x7ffff34c5708, func=0x7ffff7ee0d08)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:5106
#58 _PyEval_EvalFrameDefault () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3404
#59 0x00005555556c2056 in _PyEval_EvalCodeWithName ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4166
#60 0x00005555556c354c in _PyFunction_FastCallDict ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:5070
#61 0x000055555566799f in _PyObject_FastCallDict ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2310
#62 0x000055555566c613 in _PyObject_Call_Prepend ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2373
#63 0x00005555556673de in PyObject_Call () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2261
#64 0x00005555556cd397 in slot_tp_call ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/typeobject.c:6207
#65 0x00005555556677bb in _PyObject_FastCallDict ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Objects/abstract.c:2331
#66 0x00005555556f75be in call_function () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4861
#67 0x000055555571a73a in _PyEval_EvalFrameDefault ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:3335
#68 0x00005555556c6749 in _PyEval_EvalCodeWithName (qualname=0x0, name=<optimized out>, closure=0x0, kwdefs=0x0, defcount=0, defs=0x0,
    kwstep=2, kwcount=<optimized out>, kwargs=0x0, kwnames=0x0, argcount=0, args=0x0, locals=0x7ffff7f581f8, globals=0x7ffff7f581f8,
    _co=0x7ffff65c2540) at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4166
#69 PyEval_EvalCodeEx () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:4187
#70 0x00005555556c74ec in PyEval_EvalCode (co=co@entry=0x7ffff65c2540, globals=globals@entry=0x7ffff7f581f8,
    locals=locals@entry=0x7ffff7f581f8) at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/ceval.c:731
#71 0x000055555576d584 in run_mod () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/pythonrun.c:1025
#72 0x000055555576d981 in PyRun_FileExFlags ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/pythonrun.c:978
#73 0x000055555576db83 in PyRun_SimpleFileExFlags ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/pythonrun.c:419
#74 0x000055555576dc8d in PyRun_AnyFileExFlags ()
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Python/pythonrun.c:81
#75 0x00005555557716f7 in run_file (p_cf=0x7fffffffdf2c, filename=0x5555558ac6e0 L"test.py", fp=0x555555900e40)
    at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Modules/main.c:340
#76 Py_Main () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Modules/main.c:810
#77 0x000055555563832e in main () at /home/conda/feedstock_root/build_artifacts/python_1562031814771/work/Programs/python.c:69
```

</details>


Discussion:
Thanks for the report. I suspect this is because `round` was implemented in https://github.com/numba/numba/pull/4137 but the variant which takes `digits` was not, hence the LLVM IR ends up with e.g. `  %".259" = call double @"llvm.rint.f64"(double %".258")` and `llvm.rint` is unsupported by nvvm https://github.com/numba/numba/issues/2435#issuecomment-380170511. This is a bug, as a minimum it ought to be caught ahead of compiling something that segfaults.
Thanks @stuartarchibald, I think one of the challenges also is that it doesn't handle `NaN` values and looks to return either `min(int64)` or has undefined behavior, whereas passing with any number for `ndigits` will maintain it being a `float` in Python land typically.

Resolving patch:
diff --git a/numba/cuda/cudaimpl.py b/numba/cuda/cudaimpl.py
--- a/numba/cuda/cudaimpl.py
+++ b/numba/cuda/cudaimpl.py
@@ -468,6 +468,49 @@ def ptx_round(context, builder, sig, args):
     ])
 
 
+# This rounding implementation follows the algorithm used in the "fallback
+# version" of double_round in CPython.
+# https://github.com/python/cpython/blob/a755410e054e1e2390de5830befc08fe80706c66/Objects/floatobject.c#L964-L1007
+
+@lower(round, types.f4, types.Integer)
+@lower(round, types.f8, types.Integer)
+def round_to_impl(context, builder, sig, args):
+    def round_ndigits(x, ndigits):
+        if math.isinf(x) or math.isnan(x):
+            return x
+
+        if ndigits >= 0:
+            if ndigits > 22:
+                # pow1 and pow2 are each safe from overflow, but
+                # pow1*pow2 ~= pow(10.0, ndigits) might overflow.
+                pow1 = 10.0 ** (ndigits - 22)
+                pow2 = 1e22
+            else:
+                pow1 = 10.0 ** ndigits
+                pow2 = 1.0
+            y = (x * pow1) * pow2
+            if math.isinf(y):
+                return x
+
+        else:
+            pow1 = 10.0 ** (-ndigits)
+            y = x / pow1
+
+        z = round(y)
+        if (math.fabs(y - z) == 0.5):
+            # halfway between two integers; use round-half-even
+            z = 2.0 * round(y / 2.0)
+
+        if ndigits >= 0:
+            z = (z / pow2) / pow1
+        else:
+            z *= pow1
+
+        return z
+
+    return context.compile_internal(builder, round_ndigits, sig, args, )
+
+
 @lower(math.isinf, types.Integer)
 @lower(math.isnan, types.Integer)
 def math_isinf_isnan_int(context, builder, sig, args):


Test patch:
diff --git a/numba/cuda/tests/cudapy/test_intrinsics.py b/numba/cuda/tests/cudapy/test_intrinsics.py
--- a/numba/cuda/tests/cudapy/test_intrinsics.py
+++ b/numba/cuda/tests/cudapy/test_intrinsics.py
@@ -1,3 +1,4 @@
+import itertools
 import numpy as np
 import re
 from numba import cuda
@@ -82,6 +83,10 @@ def simple_round(ary, c):
     ary[0] = round(c)
 
 
+def simple_round_to(ary, c, ndigits):
+    ary[0] = round(c, ndigits)
+
+
 def branching_with_ifs(a, b, c):
     i = cuda.grid(1)
 
@@ -396,6 +401,101 @@ def test_round_f8(self):
             compiled[1, 1](ary, i)
             self.assertEquals(ary[0], round(i))
 
+    def test_round_to_f4(self):
+        compiled = cuda.jit("void(float32[:], float32, int32)")(simple_round_to)
+        ary = np.zeros(1, dtype=np.float32)
+        vals = np.random.random(32).astype(np.float32)
+        np.concatenate((vals, np.array([np.inf, -np.inf, np.nan])))
+        digits = (
+            # Common case branch of round_to_impl
+            -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5,
+            # The algorithm currently implemented can only round to 13 digits
+            # with single precision. Note that this doesn't trigger the
+            # "overflow safe" branch of the implementation, which can only be
+            # hit when using double precision.
+            13
+        )
+        for val, ndigits in itertools.product(vals, digits):
+            with self.subTest(val=val, ndigits=ndigits):
+                compiled[1, 1](ary, val, ndigits)
+                self.assertPreciseEqual(ary[0], round(val, ndigits),
+                                        prec='single')
+
+    # CPython on most platforms uses rounding based on dtoa.c, whereas the CUDA
+    # round-to implementation uses CPython's fallback implementation, which has
+    # slightly different behavior at the edges of the domain. Since the CUDA
+    # simulator executes using CPython, we need to skip this test when the
+    # simulator is active.
+    @skip_on_cudasim('Overflow behavior differs on CPython')
+    def test_round_to_f4_overflow(self):
+        # Test that the input value is returned when y in round_ndigits
+        # overflows.
+        compiled = cuda.jit("void(float32[:], float32, int32)")(simple_round_to)
+        ary = np.zeros(1, dtype=np.float32)
+        val = np.finfo(np.float32).max
+        # An unusually large number of digits is required to hit the "y
+        # overflows" branch of the implementation because the typing results in
+        # the computation of y as float64.
+        ndigits = 300
+        compiled[1, 1](ary, val, ndigits)
+        self.assertEqual(ary[0], val)
+
+    def test_round_to_f4_halfway(self):
+        compiled = cuda.jit("void(float32[:], float32, int32)")(simple_round_to)
+        ary = np.zeros(1, dtype=np.float32)
+        # Value chosen to trigger the "round to even" branch of the
+        # implementation
+        val = 0.3425
+        ndigits = 3
+        compiled[1, 1](ary, val, ndigits)
+        self.assertPreciseEqual(ary[0], round(val, ndigits), prec='single')
+
+    def test_round_to_f8(self):
+        compiled = cuda.jit("void(float64[:], float64, int32)")(simple_round_to)
+        ary = np.zeros(1, dtype=np.float64)
+        vals = np.random.random(32)
+        np.concatenate((vals, np.array([np.inf, -np.inf, np.nan])))
+        digits = (-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5)
+
+        for val, ndigits in itertools.product(vals, digits):
+            with self.subTest(val=val, ndigits=ndigits):
+                compiled[1, 1](ary, val, ndigits)
+                self.assertPreciseEqual(ary[0], round(val, ndigits),
+                                        prec='exact')
+
+        # Trigger the "overflow safe" branch of the implementation
+        val = 0.12345678987654321 * 10e-15
+        ndigits = 23
+        with self.subTest(val=val, ndigits=ndigits):
+            compiled[1, 1](ary, val, ndigits)
+            self.assertPreciseEqual(ary[0], round(val, ndigits),
+                                    prec='double')
+
+    # Skipped on cudasim for the same reasons as test_round_to_f4 above.
+    @skip_on_cudasim('Overflow behavior differs on CPython')
+    def test_round_to_f8_overflow(self):
+        # Test that the input value is returned when y in round_ndigits
+        # overflows.
+        compiled = cuda.jit("void(float64[:], float64, int32)")(simple_round_to)
+        ary = np.zeros(1, dtype=np.float64)
+        val = np.finfo(np.float64).max
+        # Unlike test_round_to_f4_overflow, a reasonable number of digits can
+        # be used for this test to overflow y in round_ndigits.
+        ndigits = 12
+        compiled[1, 1](ary, val, ndigits)
+        self.assertEqual(ary[0], val)
+
+    def test_round_to_f8_halfway(self):
+        compiled = cuda.jit("void(float64[:], float64, int32)")(simple_round_to)
+        ary = np.zeros(1, dtype=np.float64)
+        # Value chosen to trigger the "round to even" branch of the
+        # implementation, with a value that is not exactly representable with a
+        # float32, but only a float64.
+        val = 0.5425
+        ndigits = 3
+        compiled[1, 1](ary, val, ndigits)
+        self.assertPreciseEqual(ary[0], round(val, ndigits), prec='double')
+
 
 if __name__ == '__main__':
     unittest.main()