Repository: chainer/chainer

Issue:
F.inv behaves differently between CPU/GPU
Currently, F.inv ignores error flags (info) from cuBLAS.

https://github.com/chainer/chainer/blob/v3.0.0/chainer/functions/math/inv.py#L56

As a result, it behaves differently between CPU/GPU.

When CPU mode:

```py
chainer.functions.inv(np.zeros(4, dtype=np.float32).reshape((2,2)))
```

raises exception (`numpy.linalg.linalg.LinAlgError: Singular matrix`), while GPU mode:

```py
chainer.functions.inv(cp.zeros(4, dtype=np.float32).reshape((2,2)))
```

no exception occurs and

```py
variable([[ nan,  nan],
          [ nan,  inf]])
```

is returned.

I think it is better to change GPU mode behavior to raise Exception, so that users can notice the error immediately.


Discussion:

Resolving patch:
diff --git a/chainer/functions/math/inv.py b/chainer/functions/math/inv.py
--- a/chainer/functions/math/inv.py
+++ b/chainer/functions/math/inv.py
@@ -1,5 +1,6 @@
 import numpy.linalg
 
+import chainer
 from chainer import cuda
 from chainer import function_node
 import chainer.functions
@@ -54,7 +55,11 @@ def forward_cpu(self, x):
     def forward_gpu(self, x):
         self.retain_outputs((0,))
         shape = x[0].shape
-        invx = _inv_gpu(x[0].reshape(1, *shape))[0].reshape(shape)
+        invx, info = _inv_gpu(x[0].reshape(1, *shape))
+        if chainer.is_debug():
+            if cuda.cupy.any(info != 0):
+                raise ValueError('Input has singular matrices.')
+        invx = invx.reshape(shape)
         return invx,
 
     def backward(self, x, gy):
@@ -85,7 +90,10 @@ def forward_cpu(self, x):
 
     def forward_gpu(self, x):
         self.retain_outputs((0,))
-        invx, _ = _inv_gpu(x[0])
+        invx, info = _inv_gpu(x[0])
+        if chainer.is_debug():
+            if cuda.cupy.any(info != 0):
+                raise ValueError('Input has singular matrices.')
         return invx,
 
     def backward(self, x, gy):


Test patch:
diff --git a/tests/chainer_tests/functions_tests/math_tests/test_inv.py b/tests/chainer_tests/functions_tests/math_tests/test_inv.py
--- a/tests/chainer_tests/functions_tests/math_tests/test_inv.py
+++ b/tests/chainer_tests/functions_tests/math_tests/test_inv.py
@@ -158,6 +158,24 @@ def test_invalid_shape(self):
         with self.assertRaises(type_check.InvalidType):
             functions.inv(x)
 
+    def test_singular_cpu(self):
+        x = chainer.Variable(numpy.zeros((2, 2), dtype=numpy.float32))
+        with self.assertRaises(numpy.linalg.LinAlgError):
+            functions.inv(x)
+
+    @attr.gpu
+    def test_singular_gpu(self):
+        x = chainer.Variable(
+            cuda.to_gpu(numpy.zeros((2, 2), dtype=numpy.float32)))
+
+        # Should raise exception only when debug mode.
+        with chainer.using_config('debug', False):
+            functions.inv(x)
+
+        with chainer.using_config('debug', True):
+            with self.assertRaises(ValueError):
+                functions.inv(x)
+
 
 class BatchInvFunctionRaiseTest(unittest.TestCase):
 
@@ -171,5 +189,23 @@ def test_invalid_shape(self):
         with self.assertRaises(type_check.InvalidType):
             functions.batch_inv(x)
 
+    def test_singular_cpu(self):
+        x = chainer.Variable(numpy.zeros((1, 2, 2), dtype=numpy.float32))
+        with self.assertRaises(numpy.linalg.LinAlgError):
+            functions.batch_inv(x)
+
+    @attr.gpu
+    def test_singular_gpu(self):
+        x = chainer.Variable(
+            cuda.to_gpu(numpy.zeros((1, 2, 2), dtype=numpy.float32)))
+
+        # Should raise exception only when debug mode.
+        with chainer.using_config('debug', False):
+            functions.batch_inv(x)
+
+        with chainer.using_config('debug', True):
+            with self.assertRaises(ValueError):
+                functions.batch_inv(x)
+
 
 testing.run_module(__name__, __file__)