Repository: cupy/cupy

Issue:
Support 0×0 matrices in linalg routines
### `cupy.linalg.solve`, `cupy.linalg.inv`
```
cupy_backends.cuda.libs.cusolver.CUSOLVERError: CUSOLVER_STATUS_INVALID_VALUE
```

<details>

```
>>> for xp in [numpy, cupy]:
...     print(xp.__name__)
...     a = xp.empty((0, 0))
...     b = xp.empty((0, 3))
...     print(repr(xp.linalg.solve(a, b)))
...
numpy
array([], shape=(0, 3), dtype=float64)
cupy
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
  File ".../cupy/linalg/_solve.py", line 62, in solve
    return cupyx.lapack.gesv(a, b)
  File ".../cupyx/lapack.py", line 64, in gesv
    getrf(handle, n, n, a.data.ptr, n, dwork.data.ptr, dipiv.data.ptr,
  File "cupy_backends/cuda/libs/cusolver.pyx", line 1402, in cupy_backends.cuda.libs.cusolver.dgetrf
  File "cupy_backends/cuda/libs/cusolver.pyx", line 1409, in cupy_backends.cuda.libs.cusolver.dgetrf
  File "cupy_backends/cuda/libs/cusolver.pyx", line 1049, in cupy_backends.cuda.libs.cusolver.check_status
cupy_backends.cuda.libs.cusolver.CUSOLVERError: CUSOLVER_STATUS_INVALID_VALUE
```

</details>


### `cupy.linalg.lstsq`
```
ValueError: zero-size array to reduction operation cupy_max which has no identity
```

<details>

```
>>> cupy.linalg.lstsq(a, b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../cupy/linalg/_solve.py", line 235, in lstsq
    cutoff = rcond * s.max()
  File "cupy/core/core.pyx", line 760, in cupy.core.core.ndarray.max
  File "cupy/core/core.pyx", line 768, in cupy.core.core.ndarray.max
  File "cupy/core/_routines_statistics.pyx", line 41, in cupy.core._routines_statistics._ndarray_max
  File "cupy/core/_reduction.pyx", line 560, in cupy.core._reduction._SimpleReductionKernel.__call__
  File "cupy/core/_reduction.pyx", line 328, in cupy.core._reduction._AbstractReductionKernel._call
ValueError: zero-size array to reduction operation cupy_max which has no identity
```

</details>



Discussion:
`linalg.pinv` is fixed in #4686.

Resolving patch:
diff --git a/cupy/linalg/_solve.py b/cupy/linalg/_solve.py
--- a/cupy/linalg/_solve.py
+++ b/cupy/linalg/_solve.py
@@ -261,11 +261,13 @@ def lstsq(a, b, rcond='warn'):
         rcond = numpy.finfo(s.dtype).eps
 
     # number of singular values and matrix rank
-    cutoff = rcond * s.max()
     s1 = 1 / s
-    sing_vals = s <= cutoff
-    s1[sing_vals] = 0
-    rank = s.size - sing_vals.sum(dtype=numpy.int32)
+    rank = cupy.array(s.size, numpy.int32)
+    if s.size > 0:
+        cutoff = rcond * s.max()
+        sing_vals = s <= cutoff
+        s1[sing_vals] = 0
+        rank -= sing_vals.sum(dtype=numpy.int32)
 
     # Solve the least-squares solution
     # x = vh.T.conj() @ diag(s1) @ u.T.conj() @ b


Test patch:
diff --git a/tests/cupy_tests/linalg_tests/test_solve.py b/tests/cupy_tests/linalg_tests/test_solve.py
--- a/tests/cupy_tests/linalg_tests/test_solve.py
+++ b/tests/cupy_tests/linalg_tests/test_solve.py
@@ -191,7 +191,7 @@ def test_pinv_size_0(self):
 
 
 @testing.gpu
-class TestLstsq(unittest.TestCase):
+class TestLstsq:
 
     @testing.for_dtypes('ifdFD')
     @testing.numpy_cupy_allclose(atol=1e-3)
@@ -220,7 +220,7 @@ def check_lstsq_solution(self, a_shape, b_shape, seed, rcond, xp, dtype,
     def check_invalid_shapes(self, a_shape, b_shape):
         a = cupy.random.rand(*a_shape)
         b = cupy.random.rand(*b_shape)
-        with self.assertRaises(numpy.linalg.LinAlgError):
+        with pytest.raises(numpy.linalg.LinAlgError):
             cupy.linalg.lstsq(a, b, rcond=None)
 
     def test_lstsq_solutions(self):
@@ -248,6 +248,18 @@ def test_lstsq_solutions(self):
                 self.check_lstsq_solution((i, j), (i, ), seed+1, rcond=1e-6,
                                           singular=True)
 
+    @pytest.mark.parametrize("rcond", [-1, None, 0.5])
+    @pytest.mark.parametrize("k", [None, 0, 1, 4])
+    @pytest.mark.parametrize(("i", "j"), [(0, 0), (3, 0), (0, 7)])
+    @testing.for_dtypes('ifdFD')
+    @testing.numpy_cupy_allclose(rtol=1e-6)
+    def test_lstsq_empty_matrix(self, xp, dtype, i, j, k, rcond):
+        a = xp.empty((i, j), dtype)
+        assert a.size == 0
+        b_shape = (i,) if k is None else (i, k)
+        b = testing.shaped_random(b_shape, xp, dtype)
+        return xp.linalg.lstsq(a, b, rcond)
+
     def test_invalid_shapes(self):
         self.check_invalid_shapes((4, 3), (3, ))
         self.check_invalid_shapes((3, 3, 3), (2, 2))