Repository: chainer/chainer

Issue:
No error or warning with enable_double_backprop=True and chainer.Function
There is no errors or warnings when `Variable.backward` or `chainer.grad` is called with `enable_double_backprop=True` for a computational graph that contains `chainer.Function`. Backprop from the obtained gradients results in wrong gradients.

I think Chainer should warn or raise an error for such cases.


Discussion:
The issue arises if the second backprop is done by `chainer.grad`, while `Variable.backward` is aware of old style `chainer.Function.backward`.
```
In [4]: x = chainer.Variable(np.arange(3).astype('f') + 1)

In [5]: y = F.prod(x)

In [6]: y.data
Out[6]: array(6., dtype=float32)

In [7]: y.backward(enable_double_backprop=True)

In [8]: chainer.grad([x.grad_var], [y.grad_var])
Out[8]: [None]

In [9]: x.grad_var.backward()
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-9-654280b0771f> in <module>()
----> 1 x.grad_var.backward()

~/Documents/chainer/chainer/variable.py in backward(self, retain_grad, enable_double_backprop, loss_scale)
    933         """
    934         with chainer.using_config('enable_backprop', enable_double_backprop):
--> 935             self._backward_main(retain_grad, loss_scale)
    936
    937     def _backward_main(self, retain_grad, loss_scale):

~/Documents/chainer/chainer/variable.py in _backward_main(self, retain_grad, loss_scale)
    936
    937     def _backward_main(self, retain_grad, loss_scale):
--> 938         self._node._check_old_style_gradient()
    939         if self.creator_node is None:
    940             return

~/Documents/chainer/chainer/variable.py in _check_old_style_gradient(self)
    401             raise RuntimeError(
    402                 'cannot twice-differentiate an old style Function "%s"' %
--> 403                 self._old_style_grad_generator)
    404
    405

RuntimeError: cannot twice-differentiate an old style Function "Prod"
```
(I run this in the master branch: https://github.com/chainer/chainer/commit/d4b7f95acb57a6cae1bbffc4b13de6055a2ce686)

The `FunctionAdapter` leaves `creator=None` of outputs.  This design eventually forces to check `creator` and `_old_style_grad_generator`, which I'm not very happy with.

Resolving patch:
diff --git a/chainer/function_node.py b/chainer/function_node.py
--- a/chainer/function_node.py
+++ b/chainer/function_node.py
@@ -728,6 +728,12 @@ def grad(outputs, inputs, grad_outputs=None, grad_inputs=None, set_grad=False,
         raise TypeError(
             'grad_inputs must be a tuple or a list or None, not {}.'.format(
                 type(grad_inputs)))
+
+    for v in outputs:
+        # Raise error here if v is created by Function.backward.
+        # In such case, we don't know exact inputs of the creator.
+        v.node._check_old_style_gradient()
+
     # The implementation consists of three steps.
 
     # 1. Backward enumeration: all the nodes reachable backward from the output
@@ -744,6 +750,10 @@ def grad(outputs, inputs, grad_outputs=None, grad_inputs=None, set_grad=False,
             continue
         visited_funcs.add(func)
         for x in func.inputs:
+            # Raise error here if x is created by Function.backward.
+            # In such case, we don't know exact inputs of the creator.
+            x._check_old_style_gradient()
+
             if not x.requires_grad:
                 continue
             forward_graph[x].append(func)


Test patch:
diff --git a/tests/chainer_tests/test_variable.py b/tests/chainer_tests/test_variable.py
--- a/tests/chainer_tests/test_variable.py
+++ b/tests/chainer_tests/test_variable.py
@@ -1550,6 +1550,21 @@ def test_raise_double_backprop_2(self):
         with self.assertRaises(RuntimeError):
             x.grad_var.backward()
 
+    def test_grad_raise_double_backprop(self):
+        x = chainer.Variable(np.empty(1, np.float32))
+        y = IdentityFunction()(x)
+        y.backward(enable_double_backprop=True)
+        with self.assertRaises(RuntimeError):
+            chainer.grad([x.grad_var], [y.grad_var])
+
+    def test_grad_raise_double_backprop_2(self):
+        x = chainer.Variable(np.empty(1, np.float32))
+        z = F.identity(x)  # new style
+        y = IdentityFunction()(z)  # old style
+        y.backward(enable_double_backprop=True)
+        with self.assertRaises(RuntimeError):
+            chainer.grad([x.grad_var], [y.grad_var])
+
 
 class TestAsVariable(unittest.TestCase):