Repository: chainer/chainer

Issue:
`chainer.grad` does not call `backward_{pre,post}process` of function hooks
The following code can be used for reproduction. It does not print anything on `grad`.

```py
import chainer
import numpy

class MyHook(chainer.FunctionHook):
    def __init__(self, name):
        self.name = name

    def backward_preprocess(self, f, in_d, out_g):
        print(self.name, ':', 'preproc', f.label)

    def backward_postprocess(self, f, in_d, out_g):
        print(self.name, ':', 'postproc', f.label)

x = chainer.Variable(numpy.array([2.]))
y = x * x
with MyHook('backward'):
    y.backward()
with MyHook('grad'):
    chainer.grad([y], [x])
```

which shows

```
backward : preproc _ * _
backward : postproc _ * _
```

I confirmed it with Chainer 3.2.0 and 4.0.0b2.


Discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 30 days if no further activity occurs. Thank you for your contributions.

bump

Resolving patch:
diff --git a/chainer/function_node.py b/chainer/function_node.py
--- a/chainer/function_node.py
+++ b/chainer/function_node.py
@@ -891,8 +891,28 @@ def _backprop(outputs, inputs, grad_required, retain_grad, grads, loss_scale):
         gys = tuple([gy if not isinstance(gy, tuple) else
                      chainer.functions.add(*gy)
                      for gy in gys])
+
+        # Call pre-backward hooks
+        hooks = chainer.get_function_hooks()
+        if func._n_local_function_hooks != 0:
+            hooks = collections.OrderedDict(hooks)
+            hooks.update(func.local_function_hooks)
+        hooks = hooks.values()  # avoid six for performance
+
+        in_data = tuple([x.data for x in func.inputs])
+        out_grad_data = tuple(
+            [None if g is None else g.data for g in gys])
+        cuda.get_device_from_array(*in_data).use()
+
+        for hook in hooks:
+            hook.backward_preprocess(func, in_data, out_grad_data)
+
         new_gxs = func.backward_accumulate(input_indexes, gys, gxs)
 
+        # Call post-backward hooks
+        for hook in hooks:
+            hook.backward_postprocess(func, in_data, out_grad_data)
+
         # Delete output gradients that are not required to return
         for y_ref in func.outputs:
             y = y_ref()


Test patch:
diff --git a/tests/chainer_tests/test_function_hook.py b/tests/chainer_tests/test_function_hook.py
--- a/tests/chainer_tests/test_function_hook.py
+++ b/tests/chainer_tests/test_function_hook.py
@@ -1,5 +1,8 @@
 import unittest
 
+import mock
+import numpy
+
 import chainer
 from chainer import testing
 
@@ -24,5 +27,27 @@ def test_backward_preprocess(self):
     def test_backward_postprocess(self):
         self.assertTrue(hasattr(self.h, 'backward_postprocess'))
 
+    def check_hook_methods_called(self, func):
+        def check_method_called(name):
+            with mock.patch.object(self.h, name) as patched:
+                with self.h:
+                    func()
+                patched.assert_called()
+
+        check_method_called('forward_preprocess')
+        check_method_called('forward_postprocess')
+        check_method_called('backward_preprocess')
+        check_method_called('backward_postprocess')
+
+    def test_all_called_with_backward(self):
+        x = chainer.Variable(numpy.random.rand(2, 3).astype(numpy.float32))
+        y = chainer.functions.sum(x * x)
+        self.check_hook_methods_called(y.backward)
+
+    def test_all_called_with_grad(self):
+        x = chainer.Variable(numpy.random.rand(2, 3).astype(numpy.float32))
+        y = chainer.functions.sum(x * x)
+        self.check_hook_methods_called(lambda: chainer.grad([y], [x]))
+
 
 testing.run_module(__name__, __file__)