Repository: numba/numba

Issue:
Invalid np.dot() output for vector/matrix multiply of empty arrays
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [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).

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->
Looks like `np.dot` implementation doesn't handle empty input properly:

```python
In [161]: X = np.array([]).reshape(0, 2)

In [162]: Y = np.array([])

In [163]: numba.njit(lambda X, Y: np.dot(Y, X))(X, Y)
Out[163]: array([-1.36311572e+57, -1.36311572e+57])

In [164]: np.dot(Y, X)
Out[164]: array([0., 0.])
```


Discussion:
I can reproduce this - executable reproducer for ease of copy/paste/go:

```python
import numba
import numpy as np

X = np.array([]).reshape(0, 2)

Y = np.array([])

print(numba.njit(lambda X, Y: np.dot(Y, X))(X, Y))
# array([-1.36311572e+57, -1.36311572e+57])

print(np.dot(Y, X))
# array([0., 0.])
```

Resolving patch:
diff --git a/numba/np/linalg.py b/numba/np/linalg.py
--- a/numba/np/linalg.py
+++ b/numba/np/linalg.py
@@ -452,6 +452,8 @@ def dot_2_mm(context, builder, sig, args):
     def dot_impl(a, b):
         m, k = a.shape
         _k, n = b.shape
+        if k == 0:
+            return np.zeros((m, n), a.dtype)
         out = np.empty((m, n), a.dtype)
         return np.dot(a, b, out)
 
@@ -466,6 +468,8 @@ def dot_2_vm(context, builder, sig, args):
     def dot_impl(a, b):
         m, = a.shape
         _m, n = b.shape
+        if m == 0:
+            return np.zeros((n, ), a.dtype)
         out = np.empty((n, ), a.dtype)
         return np.dot(a, b, out)
 
@@ -480,6 +484,8 @@ def dot_2_mv(context, builder, sig, args):
     def dot_impl(a, b):
         m, n = a.shape
         _n, = b.shape
+        if n == 0:
+            return np.zeros((m, ), a.dtype)
         out = np.empty((m, ), a.dtype)
         return np.dot(a, b, out)
 
@@ -571,6 +577,7 @@ def dot_3_mv_check_args(a, b, out):
         raise ValueError("incompatible output array size for "
                          "np.dot(a, b, out) (matrix * vector)")
 
+
 def dot_3_vm(context, builder, sig, args):
     """
     np.dot(vector, matrix, out)
@@ -591,28 +598,38 @@ def dot_3_vm(context, builder, sig, args):
         # Asked for x * y, we will compute y.T * x
         mty = yty
         m_shapes = y_shapes
+        v_shape = x_shapes[0]
+        lda = m_shapes[1]
         do_trans = yty.layout == 'F'
         m_data, v_data = y.data, x.data
         check_args = dot_3_vm_check_args
-
-
     else:
         # Matrix * vector
         # We will compute x * y
         mty = xty
         m_shapes = x_shapes
+        v_shape = y_shapes[0]
+        lda = m_shapes[0]
         do_trans = xty.layout == 'C'
         m_data, v_data = x.data, y.data
         check_args = dot_3_mv_check_args
 
-
     context.compile_internal(builder, check_args,
                              signature(types.none, *sig.args), args)
     for val in m_shapes:
         check_c_int(context, builder, val)
 
-    call_xxgemv(context, builder, do_trans, mty, m_shapes, m_data,
-                v_data, out.data)
+    zero = context.get_constant(types.intp, 0)
+    both_empty = builder.icmp_signed('==', v_shape, zero)
+    matrix_empty = builder.icmp_signed('==', lda, zero)
+    is_empty = builder.or_(both_empty, matrix_empty)
+    with builder.if_else(is_empty, likely=False) as (empty, nonempty):
+        with empty:
+            cgutils.memset(builder, out.data,
+                           builder.mul(out.itemsize, out.nitems), 0)
+        with nonempty:
+            call_xxgemv(context, builder, do_trans, mty, m_shapes, m_data,
+                        v_data, out.data)
 
     return impl_ret_borrowed(context, builder, sig.return_type,
                              out._getvalue())
@@ -650,6 +667,7 @@ def check_args(a, b, out):
 
     context.compile_internal(builder, check_args,
                              signature(types.none, *sig.args), args)
+
     check_c_int(context, builder, m)
     check_c_int(context, builder, k)
     check_c_int(context, builder, n)
@@ -658,38 +676,49 @@ def check_args(a, b, out):
     y_data = y.data
     out_data = out.data
 
-    # Check whether any of the operands is really a 1-d vector represented
-    # as a (1, k) or (k, 1) 2-d array.  In those cases, it is pessimal
-    # to call the generic matrix * matrix product BLAS function.
-    one = ir.Constant(intp_t, 1)
-    is_left_vec = builder.icmp_signed('==', m, one)
-    is_right_vec = builder.icmp_signed('==', n, one)
-
-    with builder.if_else(is_right_vec) as (r_vec, r_mat):
-        with r_vec:
-            with builder.if_else(is_left_vec) as (v_v, m_v):
-                with v_v:
-                    # V * V
-                    call_xxdot(context, builder, False, dtype,
-                               k, x_data, y_data, out_data)
-                with m_v:
-                    # M * V
-                    do_trans = xty.layout == outty.layout
-                    call_xxgemv(context, builder, do_trans,
-                                xty, x_shapes, x_data, y_data, out_data)
-        with r_mat:
-            with builder.if_else(is_left_vec) as (v_m, m_m):
-                with v_m:
-                    # V * M
-                    do_trans = yty.layout != outty.layout
-                    call_xxgemv(context, builder, do_trans,
-                                yty, y_shapes, y_data, x_data, out_data)
-                with m_m:
-                    # M * M
-                    call_xxgemm(context, builder,
-                                xty, x_shapes, x_data,
-                                yty, y_shapes, y_data,
-                                outty, out_shapes, out_data)
+    # If eliminated dimension is zero, set all entries to zero and return
+    zero = context.get_constant(types.intp, 0)
+    both_empty = builder.icmp_signed('==', k, zero)
+    x_empty = builder.icmp_signed('==', m, zero)
+    y_empty = builder.icmp_signed('==', n, zero)
+    is_empty = builder.or_(both_empty, builder.or_(x_empty, y_empty))
+    with builder.if_else(is_empty, likely=False) as (empty, nonempty):
+        with empty:
+            cgutils.memset(builder, out.data,
+                           builder.mul(out.itemsize, out.nitems), 0)
+        with nonempty:
+            # Check if any of the operands is really a 1-d vector represented
+            # as a (1, k) or (k, 1) 2-d array.  In those cases, it is pessimal
+            # to call the generic matrix * matrix product BLAS function.
+            one = context.get_constant(types.intp, 1)
+            is_left_vec = builder.icmp_signed('==', m, one)
+            is_right_vec = builder.icmp_signed('==', n, one)
+
+            with builder.if_else(is_right_vec) as (r_vec, r_mat):
+                with r_vec:
+                    with builder.if_else(is_left_vec) as (v_v, m_v):
+                        with v_v:
+                            # V * V
+                            call_xxdot(context, builder, False, dtype,
+                                       k, x_data, y_data, out_data)
+                        with m_v:
+                            # M * V
+                            do_trans = xty.layout == outty.layout
+                            call_xxgemv(context, builder, do_trans,
+                                        xty, x_shapes, x_data, y_data, out_data)
+                with r_mat:
+                    with builder.if_else(is_left_vec) as (v_m, m_m):
+                        with v_m:
+                            # V * M
+                            do_trans = yty.layout != outty.layout
+                            call_xxgemv(context, builder, do_trans,
+                                        yty, y_shapes, y_data, x_data, out_data)
+                        with m_m:
+                            # M * M
+                            call_xxgemm(context, builder,
+                                        xty, x_shapes, x_data,
+                                        yty, y_shapes, y_data,
+                                        outty, out_shapes, out_data)
 
     return impl_ret_borrowed(context, builder, sig.return_type,
                              out._getvalue())


Test patch:
diff --git a/numba/tests/test_linalg.py b/numba/tests/test_linalg.py
--- a/numba/tests/test_linalg.py
+++ b/numba/tests/test_linalg.py
@@ -152,7 +152,6 @@ def test_vdot(self):
         self.check_dot_vv(vdot, "np.vdot()")
 
     def check_dot_vm(self, pyfunc2, pyfunc3, func_name):
-        m, n = 2, 3
 
         def samples(m, n):
             for order in 'CF':
@@ -169,16 +168,22 @@ def samples(m, n):
         cfunc2 = jit(nopython=True)(pyfunc2)
         if pyfunc3 is not None:
             cfunc3 = jit(nopython=True)(pyfunc3)
-        for a, b in samples(m, n):
-            self.check_func(pyfunc2, cfunc2, (a, b))
-            self.check_func(pyfunc2, cfunc2, (b, a.T))
-        if pyfunc3 is not None:
+
+        for m, n in [(2, 3),
+                     (3, 0),
+                     (0, 3)
+                     ]:
             for a, b in samples(m, n):
-                out = np.empty(m, dtype=a.dtype)
-                self.check_func_out(pyfunc3, cfunc3, (a, b), out)
-                self.check_func_out(pyfunc3, cfunc3, (b, a.T), out)
+                self.check_func(pyfunc2, cfunc2, (a, b))
+                self.check_func(pyfunc2, cfunc2, (b, a.T))
+            if pyfunc3 is not None:
+                for a, b in samples(m, n):
+                    out = np.empty(m, dtype=a.dtype)
+                    self.check_func_out(pyfunc3, cfunc3, (a, b), out)
+                    self.check_func_out(pyfunc3, cfunc3, (b, a.T), out)
 
         # Mismatching sizes
+        m, n = 2, 3
         a = self.sample_matrix(m, n - 1, np.float64)
         b = self.sample_vector(n, np.float64)
         self.assert_mismatching_sizes(cfunc2, (a, b))
@@ -229,10 +234,15 @@ def samples(m, n, k):
 
         # Test generic matrix * matrix as well as "degenerate" cases
         # where one of the outer dimensions is 1 (i.e. really represents
-        # a vector, which may select a different implementation)
+        # a vector, which may select a different implementation),
+        # one of the matrices is empty, or both matrices are empty.
         for m, n, k in [(2, 3, 4),  # Generic matrix * matrix
                         (1, 3, 4),  # 2d vector * matrix
                         (1, 1, 4),  # 2d vector * 2d vector
+                        (0, 3, 2),  # Empty matrix * matrix, empty output
+                        (3, 0, 2),  # Matrix * empty matrix, empty output
+                        (0, 0, 3),  # Both arguments empty, empty output
+                        (3, 2, 0),  # Both arguments empty, nonempty output
                         ]:
             for a, b in samples(m, n, k):
                 self.check_func(pyfunc2, cfunc2, (a, b))