Repository: numba/numba

Issue:
incorrect result with parfor and -=
Pardon my sloppy numpy. All we are doing is computing `0 - 1 - 2 - 3 - ... - 9`.

```py
import numpy as np
from numba import njit, prange

@njit(parallel=True)
def two_d_array_reduction_prod_parallel(n):
    shp = (10, 10)
    result1 = np.zeros(shp, np.int_)
    tmp_shp = (n, 10, 10)
    tmp = np.ones(tmp_shp, np.int_)

    for i in range(n):
        tmp[i] = i

    for i in prange(n):
        result1 -= tmp[i]

    return result1

def two_d_array_reduction_prod_sequential(n):
    shp = (10, 10)
    result1 = np.zeros(shp, np.int_)
    tmp_shp = (n, 10, 10)
    tmp = np.ones(tmp_shp, np.int_)

    for i in range(n):
        tmp[i] = i

    for i in range(n):
        result1 -= tmp[i]

    return result1

print(two_d_array_reduction_prod_sequential(10))
print(njit(two_d_array_reduction_prod_sequential)(10))
print(two_d_array_reduction_prod_parallel(10))
```

```py
$ python test2.py
[[-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]]
[[-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]
 [-45 -45 -45 -45 -45 -45 -45 -45 -45 -45]]
[[45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]
 [45 45 45 45 45 45 45 45 45 45]]
```

I think what might be happening is the reduction, which has intermediate arrays like ([-1, -1, -1...], [-2, -2, -2, ...], ...) is subtracting them instead of adding them, resulting in a double negation. When I set `NUMBA_DEBUG_ARRAY_OPT_RUNTIME=1` the `one_res_print` intermediate reduction arrays are negative integers. (note, you'll want to replace the 10 in the function calls with something less than your number of cores to easily debug this). So either the intermediate arrays should be positive, or the reduction should add instead of subtracting. 

CC @DrTodd13 


Discussion:
There's the same problem with division

```py
import numpy as np
from numba import njit, prange

@njit(parallel=True)
def two_d_array_reduction_prod_parallel(n):
    shp = (10, 10)
    result1 = np.ones(shp, np.float_)
    tmp_shp = (n, 10, 10)
    tmp = np.ones(tmp_shp, np.float_)

    for i in range(n):
        tmp[i] = i + 1

    for i in prange(n):
        result1 /= tmp[i]

    return result1

def two_d_array_reduction_prod_sequential(n):
    shp = (10, 10)
    result1 = np.ones(shp, np.float_)
    tmp_shp = (n, 10, 10)
    tmp = np.ones(tmp_shp, np.float_)

    for i in range(n):
        tmp[i] = i+1

    for i in range(n):
        result1 /= tmp[i]

    return result1

print(two_d_array_reduction_prod_sequential(10))
print(njit(two_d_array_reduction_prod_sequential)(10))
print(two_d_array_reduction_prod_parallel(10))
```

```
$python test2.py 
[[2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]]
[[2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]
 [2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07 2.75573192e-07 2.75573192e-07
  2.75573192e-07 2.75573192e-07]]
[[3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]
 [3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800. 3628800.
  3628800. 3628800.]]
```

It ends up computing 10! instead of 1/10!, presumably because it does a double division of the intermediate arrays (which are 1/1., 1/2., 1/3., ...).
@asmeurer thanks for reporting this! And yes, cc @DrTodd13 is the right call. 😄 

Resolving patch:
diff --git a/numba/npyufunc/parfor.py b/numba/npyufunc/parfor.py
--- a/numba/npyufunc/parfor.py
+++ b/numba/npyufunc/parfor.py
@@ -28,6 +28,7 @@
 from numba.six import exec_
 from numba.parfor import print_wrapped, ensure_parallel_support
 import types as pytypes
+import operator
 
 import warnings
 from ..errors import NumbaParallelSafetyWarning
@@ -366,8 +367,18 @@ def _lower_parfor_parallel(lowerer, parfor):
                     if isinstance(inst, ir.Assign):
                         rhs = inst.value
                         # We probably need to generalize this since it only does substitutions in
-                        # inplace_ginops.
+                        # inplace_binops.
                         if isinstance(rhs, ir.Expr) and rhs.op == 'inplace_binop' and rhs.rhs.name == init_var.name:
+                            if config.DEBUG_ARRAY_OPT:
+                                print("Adding call to reduction", rhs)
+                            if rhs.fn == operator.isub:
+                                rhs.fn = operator.iadd
+                                rhs.immutable_fn = operator.add
+                            if rhs.fn == operator.itruediv or rhs.fn == operator.ifloordiv:
+                                rhs.fn = operator.imul
+                                rhs.immutable_fn = operator.mul
+                            if config.DEBUG_ARRAY_OPT:
+                                print("After changing sub to add or div to mul", rhs)
                             # Get calltype of rhs.
                             ct = lowerer.fndesc.calltypes[rhs]
                             assert(len(ct.args) == 2)
@@ -375,7 +386,7 @@ def _lower_parfor_parallel(lowerer, parfor):
                             ctargs = (ct.args[0], redvar_typ)
                             # Update the signature of the call.
                             ct = ct.replace(args=ctargs)
-                            # Remove so we can re-insrt since calltypes is unique dict.
+                            # Remove so we can re-insert since calltypes is unique dict.
                             lowerer.fndesc.calltypes.pop(rhs)
                             # Add calltype back in for the expr with updated signature.
                             lowerer.fndesc.calltypes[rhs] = ct
diff --git a/numba/parfor.py b/numba/parfor.py
--- a/numba/parfor.py
+++ b/numba/parfor.py
@@ -2969,8 +2969,13 @@ def get_parfor_reductions(parfor, parfor_params, calltypes, reductions=None,
             reduce_varnames.append(param)
             param_nodes[param].reverse()
             reduce_nodes = get_reduce_nodes(param, param_nodes[param])
-            init_val = guard(get_reduction_init, reduce_nodes)
-            reductions[param] = (init_val, reduce_nodes)
+            gri_out = guard(get_reduction_init, reduce_nodes)
+            if gri_out is not None:
+                init_val, redop = gri_out
+            else:
+                init_val = None
+                redop = None
+            reductions[param] = (init_val, reduce_nodes, redop)
     return reduce_varnames, reductions
 
 def get_reduction_init(nodes):
@@ -2984,11 +2989,13 @@ def get_reduction_init(nodes):
     require(nodes[-2].target.name == nodes[-1].value.name)
     acc_expr = nodes[-2].value
     require(isinstance(acc_expr, ir.Expr) and acc_expr.op=='inplace_binop')
-    if acc_expr.fn == operator.iadd:
-        return 0
-    if acc_expr.fn == operator.imul:
-        return 1
-    return None
+    if acc_expr.fn == operator.iadd or acc_expr.fn == operator.isub:
+        return 0, acc_expr.fn
+    if (  acc_expr.fn == operator.imul
+       or acc_expr.fn == operator.itruediv
+       or acc_expr.fn == operator.ifloordiv ):
+        return 1, acc_expr.fn
+    return None, None
 
 def get_reduce_nodes(name, nodes):
     """


Test patch:
diff --git a/numba/tests/test_parfors.py b/numba/tests/test_parfors.py
--- a/numba/tests/test_parfors.py
+++ b/numba/tests/test_parfors.py
@@ -1693,13 +1693,43 @@ def test_impl():
     @skip_unsupported
     def test_prange03(self):
         def test_impl():
-            s = 0
+            s = 10
             for i in range(10):
                 s += 2
             return s
         self.prange_tester(test_impl, scheduler_type='unsigned',
                            check_fastmath=True)
 
+    @skip_unsupported
+    def test_prange03mul(self):
+        def test_impl():
+            s = 3
+            for i in range(10):
+                s *= 2
+            return s
+        self.prange_tester(test_impl, scheduler_type='unsigned',
+                           check_fastmath=True)
+
+    @skip_unsupported
+    def test_prange03sub(self):
+        def test_impl():
+            s = 100
+            for i in range(10):
+                s -= 2
+            return s
+        self.prange_tester(test_impl, scheduler_type='unsigned',
+                           check_fastmath=True)
+
+    @skip_unsupported
+    def test_prange03div(self):
+        def test_impl():
+            s = 10
+            for i in range(10):
+                s /= 2
+            return s
+        self.prange_tester(test_impl, scheduler_type='unsigned',
+                           check_fastmath=True)
+
     @skip_unsupported
     def test_prange04(self):
         def test_impl():