Repository: OpenMined/PySyft

Issue:
Garbage Collection issue with in-place methods on tensors
This triggers a KeyError. I suspect unexpected garbage collection.
```
buf = torch.tensor([[1., 2], [4., 2]]).send(bob)
buf.add_(buf)
buf.get()
```


Discussion:

Resolving patch:
diff --git a/syft/frameworks/torch/hook.py b/syft/frameworks/torch/hook.py
--- a/syft/frameworks/torch/hook.py
+++ b/syft/frameworks/torch/hook.py
@@ -524,7 +524,7 @@ def overloaded_syft_method(self, *args, **kwargs):
 
         return overloaded_syft_method
 
-    def get_hooked_method(hook_self, attr):
+    def get_hooked_method(hook_self, method_name):
         """
         Hook a method in order to replace all args/kwargs syft/torch tensors with
         their child attribute if they exist
@@ -535,45 +535,47 @@ def get_hooked_method(hook_self, attr):
         :return: the hooked method
         """
 
-        @wraps(attr)
+        @wraps(method_name)
         def overloaded_native_method(self, *args, **kwargs):
             """
             Operate the hooking
             """
 
             if not hasattr(self, "child"):  # means that it's not a wrapper
-                cmd = getattr(self, f"native_{attr}")
+                method = getattr(self, f"native_{method_name}")
                 # Run the native function with the new args
 
                 try:
-
                     if isinstance(args, tuple):
-                        response = cmd(*args)
+                        response = method(*args)
                     else:
-                        response = cmd(args)
+                        response = method(args)
 
                 except BaseException as e:
                     # we can make some errors more descriptive with this method
                     raise route_method_exception(e, self, args, kwargs)
 
             else:  # means that there is a wrapper to remove
-
                 try:
                     # Replace all torch tensor with their child attribute
                     new_self, new_args = syft.frameworks.torch.hook_args.hook_method_args(
-                        attr, self, args
+                        method_name, self, args
                     )
                 except BaseException as e:
                     # we can make some errors more descriptive with this method
                     raise route_method_exception(e, self, args, kwargs)
 
                 # Send the new command to the appropriate class and get the response
-                cmd = getattr(new_self, attr)
-                response = cmd(*new_args, **kwargs)
+                method = getattr(new_self, method_name)
+                response = method(*new_args, **kwargs)
+
+                # For inplace methods, just directly return self
+                if syft.torch.is_inplace_method(method_name):
+                    return self
 
                 # Put back the wrappers where needed
                 response = syft.frameworks.torch.hook_args.hook_response(
-                    attr, response, wrap_type=type(self), new_self=self
+                    method_name, response, wrap_type=type(self), new_self=self
                 )
 
             return response
diff --git a/syft/frameworks/torch/torch_attributes.py b/syft/frameworks/torch/torch_attributes.py
--- a/syft/frameworks/torch/torch_attributes.py
+++ b/syft/frameworks/torch/torch_attributes.py
@@ -112,7 +112,6 @@ def __init__(self, torch: ModuleType, hook: ModuleType) -> None:
             "is_tensor",
             "isfinite",
             "load",
-            "zeros_like",
             "randperm",
         ]
 
@@ -159,6 +158,9 @@ def __init__(self, torch: ModuleType, hook: ModuleType) -> None:
 
         self.command_guard = self._command_guard
 
+        # Dict {method_name: <is_inplace:bool>
+        self.inplace_methods = {}
+
     def _command_guard(
         self, command: str, torch_domain: str, get_native: bool = False
     ) -> Union[Callable[..., Any], str]:
@@ -232,6 +234,19 @@ def get_native_torch_name(attr: str) -> str:
         native_func_name = ".".join(parts)
         return native_func_name
 
+    def is_inplace_method(self, method_name):
+        """
+        Says if a method is inplace or not by test if it ends by _ and is not a __xx__
+        :param method_name: the name for the method
+        :return: boolean
+        """
+        try:
+            return self.inplace_methods[method_name]
+        except KeyError:
+            is_inplace = method_name[-1] == "_" and "__" not in method_name
+            self.inplace_methods[method_name] = is_inplace
+            return is_inplace
+
     @staticmethod
     def apply_fix16922(torch):
         """
diff --git a/syft/workers/base.py b/syft/workers/base.py
--- a/syft/workers/base.py
+++ b/syft/workers/base.py
@@ -264,8 +264,11 @@ def execute_command(self, message):
         command = command.decode("utf-8")
         # Handle methods
         if _self is not None:
-
-            tensor = getattr(_self, command)(*args, **kwargs)
+            if sy.torch.is_inplace_method(command):
+                getattr(_self, command)(*args, **kwargs)
+                return
+            else:
+                tensor = getattr(_self, command)(*args, **kwargs)
         # Handle functions
         else:
             # At this point, the command is ALWAYS a path to a
@@ -304,7 +307,6 @@ def execute_command(self, message):
                 ptr_id=tensor.id,
                 garbage_collect_data=False,
             )
-
             return pointer
 
     def send_command(self, recipient, message):


Test patch:
diff --git a/test/torch/tensors/test_gc.py b/test/torch/tensors/test_gc.py
--- a/test/torch/tensors/test_gc.py
+++ b/test/torch/tensors/test_gc.py
@@ -132,6 +132,18 @@ def test_implicit_garbage_collect_double_pointer(workers):
     # assert x.id not in workers["alice"]._objects
 
 
+# TESTING IN PLACE METHODS
+
+
+def test_inplace_method_on_pointer(workers):
+    bob = workers["bob"]
+    tensor = torch.tensor([[1.0, 2], [4.0, 2]])
+    pointer = tensor.send(bob)
+    pointer.add_(pointer)
+    tensor_back = pointer.get()
+    assert (tensor * 2 == tensor_back).all()
+
+
 # TESTING LOGGING TENSORS