Repository: OpenMined/PySyft

Issue:
Fix precision on pointers Error
**Describe the bug**
Fix_precision on pointer tensor fails because of change in fix_prec method which make use of numpy ops.

**To Reproduce**
```python
#[classic imports]
x = torch.tensor([1.])
x_ptr = x.send(alice)
x_fp = x_ptr.fix_prec()
``` 

Or run Tutorial Part 10.

![image](https://user-images.githubusercontent.com/8157164/61076178-8a1cfc00-a413-11e9-8dc3-dfba2ae73261.png)



**Error**
```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/code/PySyft/syft/frameworks/torch/hook/hook_args.py in register_response(attr, response, response_ids, owner)
    656         # Load the utility function to register the response and transform tensors with pointers
--> 657         register_response_function = register_response_functions[attr_id]
    658         # Try running it

KeyError: 'numpy'

During handling of the above exception, another exception occurred:

AttributeError                            Traceback (most recent call last)
<ipython-input-1-95fc1ed4192b> in <module>
     13 x = torch.tensor([1.])
     14 x_ptr = x.send(alice)
---> 15 x_fp = x_ptr.fix_prec()

~/code/PySyft/syft/frameworks/torch/tensors/interpreters/native.py in fix_prec(self, *args, **kwargs)
    666         prec_fractional = kwargs.get("precision_fractional", 3)
    667         max_precision = _get_maximum_precision()
--> 668         if self._requires_large_precision(max_precision, base, prec_fractional):
    669             return (
    670                 syft.LargePrecisionTensor(*args, **kwargs)

~/code/PySyft/syft/frameworks/torch/tensors/interpreters/native.py in _requires_large_precision(self, max_precision, base, precision_fractional)
    691         # We need to use NumPy here as log2 is not yet implemented for LongTensor PyTorch objects
    692         return np.any(
--> 693             np.log2(np.abs(self.clone().detach().numpy()) + 1) + base_fractional > max_precision
    694         )
    695 

~/code/PySyft/syft/frameworks/torch/hook/hook.py in overloaded_native_method(self, *args, **kwargs)
    675                 # Send the new command to the appropriate class and get the response
    676                 method = getattr(new_self, method_name)
--> 677                 response = method(*new_args, **new_kwargs)
    678 
    679                 # For inplace methods, just directly return self

~/code/PySyft/syft/frameworks/torch/hook/hook.py in overloaded_pointer_method(self, *args, **kwargs)
    511             command = (attr, self, args, kwargs)
    512 
--> 513             response = owner.send_command(location, command)
    514 
    515             return response

~/code/PySyft/syft/workers/base.py in send_command(self, recipient, message, return_ids)
    425 
    426         try:
--> 427             ret_val = self.send_msg(codes.MSGTYPE.CMD, message, location=recipient)
    428         except ResponseSignatureError as e:
    429             ret_val = None

~/code/PySyft/syft/workers/base.py in send_msg(self, msg_type, message, location)
    221 
    222         # Step 2: send the message and wait for a response
--> 223         bin_response = self._send_msg(bin_message, location)
    224 
    225         # Step 3: deserialize the response

~/code/PySyft/syft/workers/virtual.py in _send_msg(self, message, location)
      8 class VirtualWorker(BaseWorker, FederatedClient):
      9     def _send_msg(self, message: bin, location: BaseWorker) -> bin:
---> 10         return location._recv_msg(message)
     11 
     12     def _recv_msg(self, message: bin) -> bin:

~/code/PySyft/syft/workers/virtual.py in _recv_msg(self, message)
     11 
     12     def _recv_msg(self, message: bin) -> bin:
---> 13         return self.recv_msg(message)
     14 
     15     @staticmethod

~/code/PySyft/syft/workers/base.py in recv_msg(self, bin_message)
    252             print(f"worker {self} received {sy.codes.code2MSGTYPE[msg_type]} {contents}")
    253         # Step 1: route message to appropriate function
--> 254         response = self._message_router[msg_type](contents)
    255 
    256         # Step 2: Serialize the message to simple python objects

~/code/PySyft/syft/workers/base.py in execute_command(self, message)
    391             try:
    392                 response = sy.frameworks.torch.hook_args.register_response(
--> 393                     command_name, response, list(return_ids), self
    394                 )
    395                 return response

~/code/PySyft/syft/frameworks/torch/hook/hook_args.py in register_response(attr, response, response_ids, owner)
    664         register_response_functions[attr_id] = register_response_function
    665         # Run it
--> 666         new_response = register_response_function(response, response_ids=response_ids, owner=owner)
    667 
    668     # Remove the artificial tuple

~/code/PySyft/syft/frameworks/torch/hook/hook_args.py in <lambda>(x, **kwargs)
    757         f = many_fold
    758 
--> 759     return lambda x, **kwargs: f(lambdas, x, **kwargs)

~/code/PySyft/syft/frameworks/torch/hook/hook_args.py in two_fold(lambdas, args, **kwargs)
    514 
    515 def two_fold(lambdas, args, **kwargs):
--> 516     return lambdas[0](args[0], **kwargs), lambdas[1](args[1], **kwargs)
    517 
    518 

~/code/PySyft/syft/frameworks/torch/hook/hook_args.py in <lambda>(i, **kwargs)
    735         if isinstance(r, (list, tuple))  # if the rule is a list or tuple.
    736         # Last if not, rule is probably == 1 so use type to return the right transformation.
--> 737         else lambda i, **kwargs: register_tensor(i, **kwargs)
    738         for a, r in zip(response, rules)  # And do this for all the responses / rules provided
    739     ]

~/code/PySyft/syft/frameworks/torch/hook/hook_args.py in register_tensor(tensor, owner, response_ids)
    706             and each id is pop out when needed.
    707     """
--> 708     tensor.owner = owner
    709     try:
    710         tensor.id = response_ids.pop(-1)

AttributeError: 'numpy.ndarray' object has no attribute 'owner'
```


Discussion:

Resolving patch:
diff --git a/syft/frameworks/torch/pointers/pointer_tensor.py b/syft/frameworks/torch/pointers/pointer_tensor.py
--- a/syft/frameworks/torch/pointers/pointer_tensor.py
+++ b/syft/frameworks/torch/pointers/pointer_tensor.py
@@ -195,6 +195,23 @@ def attr(self, attr_name):
     def dim(self) -> int:
         return len(self._shape)
 
+    def fix_prec(self, *args, **kwargs):
+        """
+        Send a command to remote worker to transform a tensor to fix_precision
+
+        Returns:
+            A pointer to an FixPrecisionTensor
+        """
+
+        # Send the command
+        command = ("fix_prec", self, args, kwargs)
+
+        response = self.owner.send_command(self.location, command)
+
+        return response
+
+    fix_precision = fix_prec
+
     def share(self, *args, **kwargs):
         """
         Send a command to remote worker to additively share a tensor
diff --git a/syft/frameworks/torch/tensors/interpreters/additive_shared.py b/syft/frameworks/torch/tensors/interpreters/additive_shared.py
--- a/syft/frameworks/torch/tensors/interpreters/additive_shared.py
+++ b/syft/frameworks/torch/tensors/interpreters/additive_shared.py
@@ -923,6 +923,10 @@ def simplify(tensor: "AdditiveSharingTensor") -> tuple:
         chain = None
         if hasattr(tensor, "child"):
             chain = sy.serde._simplify(tensor.child)
+
+        # Don't delete the remote values of the shares at simplification
+        tensor.set_garbage_collect_data(False)
+
         return (tensor.id, tensor.field, tensor.crypto_provider.id, chain)
 
     @staticmethod
diff --git a/syft/frameworks/torch/tensors/interpreters/native.py b/syft/frameworks/torch/tensors/interpreters/native.py
--- a/syft/frameworks/torch/tensors/interpreters/native.py
+++ b/syft/frameworks/torch/tensors/interpreters/native.py
@@ -662,18 +662,22 @@ def float_prec_(self):
     float_precision_ = float_prec_
 
     def fix_prec(self, *args, **kwargs):
-        base = kwargs.get("base", 10)
-        prec_fractional = kwargs.get("precision_fractional", 3)
-        max_precision = _get_maximum_precision()
-        if self._requires_large_precision(max_precision, base, prec_fractional):
-            return (
-                syft.LargePrecisionTensor(*args, **kwargs)
-                .on(self)
-                .child.fix_large_precision()
-                .wrap()
-            )
+        if self.is_wrapper:
+            self.child = self.child.fix_prec(*args, **kwargs)
+            return self
         else:
-            return syft.FixedPrecisionTensor(*args, **kwargs).on(self).enc_fix_prec().wrap()
+            base = kwargs.get("base", 10)
+            prec_fractional = kwargs.get("precision_fractional", 3)
+            max_precision = _get_maximum_precision()
+            if self._requires_large_precision(max_precision, base, prec_fractional):
+                return (
+                    syft.LargePrecisionTensor(*args, **kwargs)
+                    .on(self)
+                    .child.fix_large_precision()
+                    .wrap()
+                )
+            else:
+                return syft.FixedPrecisionTensor(*args, **kwargs).on(self).enc_fix_prec().wrap()
 
     fix_precision = fix_prec
 


Test patch:
diff --git a/test/torch/tensors/test_additive_shared.py b/test/torch/tensors/test_additive_shared.py
--- a/test/torch/tensors/test_additive_shared.py
+++ b/test/torch/tensors/test_additive_shared.py
@@ -424,6 +424,34 @@ def test_fixed_precision_and_sharing(workers):
     assert (y == (t + t)).all()
 
 
+def test_fixed_precision_and_sharing_on_pointer(workers):
+    bob, alice, james = (workers["bob"], workers["alice"], workers["james"])
+
+    t = torch.tensor([1, 2, 3, 4.0])
+    ptr = t.send(james)
+
+    x = ptr.fix_prec().share(bob, alice)
+
+    y = x + x
+
+    y = y.get().get().float_prec()
+    assert (y == (t + t)).all()
+
+
+def test_pointer_on_fixed_precision_and_sharing(workers):
+    bob, alice, james = (workers["bob"], workers["alice"], workers["james"])
+
+    t = torch.tensor([1, 2, 3, 4.0])
+
+    x = t.fix_prec().share(bob, alice)
+    x = x.send(james)
+
+    y = x + x
+
+    y = y.get().get().float_prec()
+    assert (y == (t + t)).all()
+
+
 def test_get_item(workers):
     alice, bob, james = workers["alice"], workers["bob"], workers["james"]
     x = th.tensor([[3.1, 4.3]]).fix_prec().share(alice, bob, crypto_provider=james)