Repository: cupy/cupy

Issue:
`linalg.det` should return 0 if getrf failed
`det` of a singular matrix is 0. `getrf` failure has to be checked.

* Conditions
  - CuPy version: from a0869c60b5fe5551949f5c815de0206cf8e5759b (#2437)

* Code to reproduce

  ```
  >>> cupy.linalg.det(cupy.zeros((2, 2), cupy.float32))
  array(nan, dtype=float32)
  ```



Discussion:

Resolving patch:
diff --git a/cupy/linalg/norms.py b/cupy/linalg/norms.py
--- a/cupy/linalg/norms.py
+++ b/cupy/linalg/norms.py
@@ -236,7 +236,7 @@ def _slogdet_one(a):
     handle = device.get_cusolver_handle()
     m = len(a)
     ipiv = cupy.empty(m, dtype=numpy.int32)
-    dev_info = cupy.empty(1, dtype=numpy.int32)
+    dev_info = cupy.empty((), dtype=numpy.int32)
 
     # Need to make a copy because getrf works inplace
     a_copy = a.copy(order='F')
@@ -253,22 +253,25 @@ def _slogdet_one(a):
     getrf(handle, m, m, a_copy.data.ptr, m, workspace.data.ptr,
           ipiv.data.ptr, dev_info.data.ptr)
 
-    try:
-        cupy.linalg.util._check_cusolver_dev_info_if_synchronization_allowed(
-            getrf, dev_info)
-
-        diag = cupy.diag(a_copy)
-        # ipiv is 1-origin
-        non_zero = (cupy.count_nonzero(ipiv != cupy.arange(1, m + 1)) +
-                    cupy.count_nonzero(diag < 0))
-        # Note: sign == -1 ** (non_zero % 2)
-        sign = (non_zero % 2) * -2 + 1
-        logdet = cupy.log(abs(diag)).sum()
-    except linalg.LinAlgError:
-        sign = cupy.array(0.0, dtype=dtype)
-        logdet = cupy.array(float('-inf'), dtype)
-
-    return sign, logdet
+    # dev_info < 0 means illegal value (in dimensions, strides, and etc.) that
+    # should never happen even if the matrix contains nan or inf.
+    # TODO(kataoka): assert dev_info >= 0 if synchronization is allowed for
+    # debugging purposes.
+
+    diag = cupy.diag(a_copy)
+    # ipiv is 1-origin
+    non_zero = (cupy.count_nonzero(ipiv != cupy.arange(1, m + 1)) +
+                cupy.count_nonzero(diag < 0))
+
+    # Note: sign == -1 ** (non_zero % 2)
+    sign = (non_zero % 2) * -2 + 1
+    logdet = cupy.log(abs(diag)).sum()
+
+    singular = dev_info > 0
+    return (
+        cupy.where(singular, dtype.type(0), sign),
+        cupy.where(singular, dtype.type('-inf'), logdet),
+    )
 
 
 def trace(a, offset=0, axis1=0, axis2=1, dtype=None, out=None):


Test patch:
diff --git a/tests/cupy_tests/linalg_tests/test_norms.py b/tests/cupy_tests/linalg_tests/test_norms.py
--- a/tests/cupy_tests/linalg_tests/test_norms.py
+++ b/tests/cupy_tests/linalg_tests/test_norms.py
@@ -130,6 +130,12 @@ def test_det_zero_dim(self, xp, dtype):
         a = testing.shaped_arange((), xp, dtype)
         xp.linalg.det(a)
 
+    @testing.for_float_dtypes(no_float16=True)
+    @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4)
+    def test_det_singular(self, xp, dtype):
+        a = xp.zeros((2, 3, 3), dtype)
+        return xp.linalg.det(a)
+
 
 @testing.gpu
 class TestSlogdet(unittest.TestCase):
@@ -157,11 +163,18 @@ def test_slogdet_4(self, xp, dtype):
 
     @testing.for_float_dtypes(no_float16=True)
     @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4)
-    def test_slogdet_fail(self, xp, dtype):
+    def test_slogdet_singular(self, xp, dtype):
+        a = xp.zeros((3, 3), dtype)
+        sign, logdet = xp.linalg.slogdet(a)
+        return xp.array([sign, logdet], dtype)
+
+    @testing.for_float_dtypes(no_float16=True)
+    @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4)
+    def test_slogdet_singular_errstate(self, xp, dtype):
         a = xp.zeros((3, 3), dtype)
         with cupyx.errstate(linalg='raise'):
-            # `cupy.linalg.slogdet` internally catches a raised error from
-            # cuSOLVER, but yield a valid output to mimic NumPy.
+            # `cupy.linalg.slogdet` internally catches `dev_info < 0` from
+            # cuSOLVER, which should not affect `dev_info > 0` cases.
             sign, logdet = xp.linalg.slogdet(a)
         return xp.array([sign, logdet], dtype)