Repository: chainer/chainer

Issue:
Stack overflow when `to_cpu`-family is overridden
### Code
```py
import chainer


class MyNet(chainer.Link):
    def to_cpu(self):
        super(MyNet, self).to_cpu()


net = MyNet()
net.to_cpu()
```

### Result
```
:
  File "/data2/work/w/repos/chainer/chainer/link.py", line 447, in device_resident_accept
    super(Link, self).device_resident_accept(visitor)
  File "/data2/work/w/repos/chainer/chainer/device_resident.py", line 31, in device_resident_accept
    visitor.visit_device_resident(self)
  File "/data2/work/w/repos/chainer/chainer/device_resident.py", line 240, in visit_device_resident
    to_method(**kwargs)
  File "/home/niboshi/t/a.py", line 6, in to_cpu
    super(MyNet, self).to_cpu()
  File "/data2/work/w/repos/chainer/chainer/device_resident.py", line 67, in to_cpu
    self.__to_device(visitor)
  File "/data2/work/w/repos/chainer/chainer/device_resident.py", line 139, in __to_device
    self.device_resident_accept(to_device_visitor)
  File "/data2/work/w/repos/chainer/chainer/link.py", line 447, in device_resident_accept
    super(Link, self).device_resident_accept(visitor)
  File "/data2/work/w/repos/chainer/chainer/device_resident.py", line 31, in device_resident_accept
    visitor.visit_device_resident(self)
RecursionError: maximum recursion depth exceeded
```


Discussion:

Resolving patch:
diff --git a/chainer/device_resident.py b/chainer/device_resident.py
--- a/chainer/device_resident.py
+++ b/chainer/device_resident.py
@@ -63,7 +63,8 @@ def to_cpu(self):
         visitor = _ToDeviceVisitor(
             backend.CpuDevice(),
             entry_method_info=('to_cpu', {}),
-            skip_between_cupy_devices=True)
+            skip_between_cupy_devices=True,
+            starting_device_resident=self)
         self.__to_device(visitor)
         return self
 
@@ -91,7 +92,8 @@ def to_gpu(
         visitor = _ToDeviceVisitor(
             device,
             entry_method_info=('to_gpu', {'device': device.device}),
-            skip_between_cupy_devices=True)
+            skip_between_cupy_devices=True,
+            starting_device_resident=self)
         self.__to_device(visitor)
         return self
 
@@ -101,7 +103,8 @@ def to_intel64(self):
         intel64.check_ideep_available()
         visitor = _ToDeviceVisitor(
             chainer.get_device(intel64.Intel64Device()),
-            entry_method_info=('to_intel64', {}))
+            entry_method_info=('to_intel64', {}),
+            starting_device_resident=self)
         self.__to_device(visitor)
         return self
 
@@ -205,7 +208,8 @@ class _ToDeviceVisitor(DeviceResidentsVisitor):
 
     def __init__(
             self, device, entry_method_info=None,
-            skip_between_cupy_devices=False):
+            skip_between_cupy_devices=False,
+            starting_device_resident=None):
 
         assert isinstance(device, chainer.backend.Device)
 
@@ -219,15 +223,29 @@ def __init__(
             assert len(entry_method_info) == 2
             assert entry_method_info[0] in ('to_cpu', 'to_gpu', 'to_intel64')
 
+        # starting_device_resident is also for backward compatibility
+        # workaround for overridden methods.
+        # It is a DeviceResident if to_xxx methods were initially called
+        # on this visitor. This is used to avoid infinite accept-visit loop
+        # that would occur by calling to_xxx methods.
+        assert (starting_device_resident is None
+                or isinstance(starting_device_resident, DeviceResident))
+
         self._device = device
         self._entry_method_info = entry_method_info
         self._skip_between_cupy_devices = skip_between_cupy_devices
+        self._starting_device_resident = starting_device_resident
 
     def visit_device_resident(self, device_resident):
         device_resident._device = self._device
 
         # Backward compatibility workaround for overridden methods
         if device_resident._overridden_to_methods:
+            # Skip this device resident, if the visitor was initially triggered
+            # from it.
+            if device_resident is self._starting_device_resident:
+                return
+
             if self._entry_method_info is not None:
                 # Deprecated method is being called: e.g. to_cpu and to_gpu.
                 method_name, kwargs = self._entry_method_info


Test patch:
diff --git a/tests/chainer_tests/test_link.py b/tests/chainer_tests/test_link.py
--- a/tests/chainer_tests/test_link.py
+++ b/tests/chainer_tests/test_link.py
@@ -2357,15 +2357,18 @@ def from_chx(self, device):
 
             elif method_name == 'to_cpu':
                 def to_cpu(self):
+                    super(ChildLink, self).to_cpu()
                     self.to_method_called += 1
 
             elif method_name == 'to_gpu':
                 def to_gpu(self, device=None):
                     assert isinstance(device, (cuda.Device, int))
+                    super(ChildLink, self).to_gpu(device)
                     self.to_method_called += 1
 
             elif method_name == 'to_intel64':
                 def to_intel64(self):
+                    super(ChildLink, self).to_intel64()
                     self.to_method_called += 1
 
             else: