Repository: OpenMined/PySyft
Pull request: Multi-worker computation graphs (SplitNN)
We add an option to send() to support backpropagation on a computation graph held by several workers.

Fix #3000

Example:
```python
hook.local_worker.is_client_worker = False

x = th.tensor([1., 2., 3, 4, 5], requires_grad=True)
y = (x + x)
me.register_obj(y) # registration on the local worker is sometimes buggy

y_ptr = y.send(alice, requires_grad=True)
z_ptr = y_ptr * 2

z_ptr2 = z_ptr.move(bob, requires_grad=True)
z = z_ptr2.sum()
z.backward()

print(x.grad)
#> tensor([4., 4., 4., 4., 4.])
```

TODO:
- [x] Comment & clean code
- [x] Add tests
- [x] Finish splitNN tutorials cleaning

Problem statement:
Make .send() differentiable
**Is your feature request related to a problem? Please describe.**
If you build a graph of computation across multiple workers, you'll use send or move to do so. But those are not differentiable and hence .backward() won't work except on the last worker where you call it

**Describe the solution you'd like**
I'd like send() to be crossable is the opposite direction (using a reference to the original value/ location) to send back a .backward() call when needed

**Additional context**
This is super useful for SplitNN

Hints from issue discussion:
Backward references would also be handy for garbage collection.

Commit message: Fix tests

Patch to review:
diff --git a/examples/tutorials/advanced/Split Neural Network/Tutorial 1 - SplitNN Introduction.ipynb b/examples/tutorials/advanced/Split Neural Network/Tutorial 1 - SplitNN Introduction.ipynb
index 5ffade3a544..c39658fe106 100644
--- a/examples/tutorials/advanced/Split Neural Network/Tutorial 1 - SplitNN Introduction.ipynb	
+++ b/examples/tutorials/advanced/Split Neural Network/Tutorial 1 - SplitNN Introduction.ipynb	
@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 1,
    "metadata": {
     "tags": [
      "parameters"
@@ -78,7 +78,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 2,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -91,7 +91,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -105,7 +105,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -150,7 +150,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 7,
    "metadata": {
     "pixiedust": {
      "displayParams": {}
@@ -169,7 +169,7 @@
     "    a = models[0](x)\n",
     "\n",
     "    #3) break the computation graph link, and send the activation signal to the next model\n",
-    "    remote_a = a.detach().move(models[1].location).requires_grad_()\n",
+    "    remote_a = a.move(models[1].location, requires_grad=True)\n",
     "\n",
     "    #4) make prediction on next model using recieved signal\n",
     "    pred = models[1](remote_a)\n",
@@ -181,11 +181,11 @@
     "    #6) figure out which weights caused us to miss\n",
     "    loss.backward()\n",
     "\n",
-    "    #7) send gradient of the recieved activation signal to the model behind\n",
-    "    grad_a = remote_a.grad.copy().move(models[0].location)\n",
+    "    # 7) send gradient of the recieved activation signal to the model behind\n",
+    "    # grad_a = remote_a.grad.copy().move(models[0].location)\n",
     "\n",
-    "    #8) backpropagate on bottom model given this gradient\n",
-    "    a.backward(grad_a)\n",
+    "    # 8) backpropagate on bottom model given this gradient\n",
+    "    # a.backward(grad_a)\n",
     "\n",
     "    #9) change the weights\n",
     "    for opt in optimizers:\n",
@@ -197,9 +197,40 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 8,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Epoch 0 - Training loss: 0.3258790969848633\n",
+      "Epoch 1 - Training loss: 0.23088108003139496\n"
+     ]
+    },
+    {
+     "ename": "KeyboardInterrupt",
+     "evalue": "",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mKeyboardInterrupt\u001b[0m                         Traceback (most recent call last)",
+      "\u001b[0;32m<ipython-input-8-69b5658edbff>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mepochs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      2\u001b[0m     \u001b[0mrunning_loss\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m     \u001b[0;32mfor\u001b[0m \u001b[0mimages\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlabels\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtrainloader\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      4\u001b[0m         \u001b[0mimages\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mimages\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0malice\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      5\u001b[0m         \u001b[0mimages\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mimages\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mview\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mimages\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/env/pysyft/lib/python3.7/site-packages/torch/utils/data/dataloader.py\u001b[0m in \u001b[0;36m__next__\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m    343\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    344\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0m__next__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 345\u001b[0;31m         \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_next_data\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    346\u001b[0m         \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_num_yielded\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    347\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_dataset_kind\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0m_DatasetKind\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mIterable\u001b[0m \u001b[0;32mand\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m\\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/env/pysyft/lib/python3.7/site-packages/torch/utils/data/dataloader.py\u001b[0m in \u001b[0;36m_next_data\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m    383\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0m_next_data\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    384\u001b[0m         \u001b[0mindex\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_next_index\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m  \u001b[0;31m# may raise StopIteration\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 385\u001b[0;31m         \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_dataset_fetcher\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfetch\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mindex\u001b[0m\u001b[0;34m)\u001b[0m  \u001b[0;31m# may raise StopIteration\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    386\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_pin_memory\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    387\u001b[0m             \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_utils\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpin_memory\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpin_memory\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/env/pysyft/lib/python3.7/site-packages/torch/utils/data/_utils/fetch.py\u001b[0m in \u001b[0;36mfetch\u001b[0;34m(self, possibly_batched_index)\u001b[0m\n\u001b[1;32m     42\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0mfetch\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpossibly_batched_index\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     43\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mauto_collation\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 44\u001b[0;31m             \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdataset\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0midx\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0midx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpossibly_batched_index\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     45\u001b[0m         \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     46\u001b[0m             \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdataset\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mpossibly_batched_index\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/env/pysyft/lib/python3.7/site-packages/torch/utils/data/_utils/fetch.py\u001b[0m in \u001b[0;36m<listcomp>\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m     42\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0mfetch\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpossibly_batched_index\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     43\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mauto_collation\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 44\u001b[0;31m             \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdataset\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0midx\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0midx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpossibly_batched_index\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     45\u001b[0m         \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     46\u001b[0m             \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdataset\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mpossibly_batched_index\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/env/pysyft/lib/python3.7/site-packages/torchvision/datasets/mnist.py\u001b[0m in \u001b[0;36m__getitem__\u001b[0;34m(self, index)\u001b[0m\n\u001b[1;32m     95\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     96\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtransform\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 97\u001b[0;31m             \u001b[0mimg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtransform\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mimg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     98\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     99\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtarget_transform\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/env/pysyft/lib/python3.7/site-packages/torchvision/transforms/transforms.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, img)\u001b[0m\n\u001b[1;32m     68\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mimg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     69\u001b[0m         \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtransforms\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 70\u001b[0;31m             \u001b[0mimg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mimg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     71\u001b[0m         \u001b[0;32mreturn\u001b[0m \u001b[0mimg\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     72\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/env/pysyft/lib/python3.7/site-packages/torchvision/transforms/transforms.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, pic)\u001b[0m\n\u001b[1;32m     99\u001b[0m             \u001b[0mTensor\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mConverted\u001b[0m \u001b[0mimage\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    100\u001b[0m         \"\"\"\n\u001b[0;32m--> 101\u001b[0;31m         \u001b[0;32mreturn\u001b[0m \u001b[0mF\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_tensor\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpic\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    102\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    103\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0m__repr__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/env/pysyft/lib/python3.7/site-packages/torchvision/transforms/functional.py\u001b[0m in \u001b[0;36mto_tensor\u001b[0;34m(pic)\u001b[0m\n\u001b[1;32m     93\u001b[0m     \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     94\u001b[0m         \u001b[0mnchannel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpic\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmode\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 95\u001b[0;31m     \u001b[0mimg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mimg\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mview\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpic\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpic\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnchannel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     96\u001b[0m     \u001b[0;31m# put it from HWC to CHW format\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     97\u001b[0m     \u001b[0;31m# yikes, this transpose takes 80% of the loading time/CPU\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/PySyft/syft/generic/frameworks/hook/trace.py\u001b[0m in \u001b[0;36mtrace_wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m     81\u001b[0m                 \u001b[0msyft\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhook\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrace\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlogs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcommand\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     82\u001b[0m             \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 83\u001b[0;31m                 \u001b[0mresponse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     84\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     85\u001b[0m             \u001b[0;32mreturn\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/PySyft/syft/generic/frameworks/hook/hook.py\u001b[0m in \u001b[0;36moverloaded_native_method\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m    436\u001b[0m                 \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    437\u001b[0m                     \u001b[0;31m# we can make some errors more descriptive with this method\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 438\u001b[0;31m                     \u001b[0;32mraise\u001b[0m \u001b[0mroute_method_exception\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0me\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    439\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    440\u001b[0m             \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m  \u001b[0;31m# means that there is a wrapper to remove\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m~/code/PySyft/syft/generic/frameworks/hook/hook.py\u001b[0m in \u001b[0;36moverloaded_native_method\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m    432\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    433\u001b[0m                 \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 434\u001b[0;31m                     \u001b[0mresponse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    435\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    436\u001b[0m                 \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
+     ]
+    }
+   ],
    "source": [
     "for i in range(epochs):\n",
     "    running_loss = 0\n",
@@ -214,6 +245,13 @@
     "    else:\n",
     "        print(\"Epoch {} - Training loss: {}\".format(i, running_loss/len(trainloader)))"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
   }
  ],
  "metadata": {
@@ -232,7 +270,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.6.9"
+   "version": "3.7.0"
   }
  },
  "nbformat": 4,
diff --git a/syft/frameworks/torch/tensors/interpreters/native.py b/syft/frameworks/torch/tensors/interpreters/native.py
index 180d15d5efa..5e871e3f3f0 100644
--- a/syft/frameworks/torch/tensors/interpreters/native.py
+++ b/syft/frameworks/torch/tensors/interpreters/native.py
@@ -58,6 +58,42 @@ class TorchTensor(AbstractTensor):
     def has_child(self):
         return hasattr(self, "child")
 
+    def trigger_origin_backward_hook(self, sender: str, origin_id: int):
+        """
+        This hook is triggered when a tensor which was received from a sender has
+        a gradient update. It will send back to this sender and his original tensor
+        this gradient value to be set remotely. Also, because this is triggered during
+        backward(), the backward command is also forwarded back.
+
+        Args:
+            sender (str): id of the worker where this tensor comes from
+            origin_id (int): what was its original id
+        """
+
+        def trigger_origin_backward(grad):
+            """
+            The function setting back the gradient and calling backward
+
+            Args:
+                grad: the gradient tensor being set
+            """
+
+            location = self.owner.get_worker(sender)
+
+            origin_ptr = syft.PointerTensor.create_pointer(
+                self, location=location, id_at_location=origin_id, garbage_collect_data=False
+            )
+            # set the gradient
+            self.owner.send_command(location, message=("set_grad", origin_ptr, (grad,), {}))
+
+            # call backward()
+            origin_ptr.backward(grad)
+
+        return trigger_origin_backward
+
+    def set_grad(self, grad):
+        self.grad = grad
+
     @property
     def tags(self):
         if self.has_child():
@@ -361,26 +397,30 @@ def send(
         self,
         *location,
         inplace: bool = False,
-        user=None,
-        local_autograd=False,
-        preinitialize_grad=False,
-        no_wrap=False,
-        garbage_collect_data=True,
+        user: object = None,
+        local_autograd: bool = False,
+        requires_grad: bool = False,
+        preinitialize_grad: bool = False,
+        no_wrap: bool = False,
+        garbage_collect_data: bool = True,
     ):
         """Gets the pointer to a new remote object.
 
-        One of the most commonly used methods in PySyft, this method serializes
-        the object upon which it is called (self), sends the object to a remote
-        worker, creates a pointer to that worker, and then returns that pointer
-        from this function.
+        One of the most commonly used methods in PySyft, this method serializes the object upon
+        which it is called (self), sends the object to a remote worker, creates a pointer to
+        that worker, and then returns that pointer from this function.
 
         Args:
-            location: The BaseWorker object which you want to send this object
-                to. Note that this is never actually the BaseWorker but instead
-                a class which instantiates the BaseWorker abstraction.
+            location: The BaseWorker object which you want to send this object to. Note that
+                this is never actually the BaseWorker but instead a class which instantiates the
+                BaseWorker abstraction.
             inplace: if true, return the same object instance, else a new wrapper
+            user (object,optional): User credentials to be verified.
             local_autograd: Use autograd system on the local machine instead of PyTorch's
                 autograd on the workers.
+            requires_grad: Default to False. If true, whenever the remote value of this tensor
+                will have its gradient updated (for example when calling .backward()), a call
+                will be made to set back the local gradient value.
             preinitialize_grad: Initialize gradient for AutogradTensors to a tensor
             no_wrap: If True, wrap() is called on the created pointer
             garbage_collect_data: argument passed down to create_pointer()
@@ -415,6 +455,7 @@ def send(
                 self,
                 location,
                 local_autograd=local_autograd,
+                requires_grad=requires_grad,
                 preinitialize_grad=preinitialize_grad,
                 garbage_collect_data=garbage_collect_data,
             )
@@ -613,13 +654,15 @@ def get_(self, *args, **kwargs):
         return self.get(*args, inplace=True, **kwargs)
 
     def allow(self, user=None) -> bool:
-        """ This function returns will return True if it isn't a PrivateTensor, otherwise it will return the result of PrivateTensor's allow method.
+        """ This function returns will return True if it isn't a PrivateTensor, otherwise it will
+        return the result of PrivateTensor's allow method.
 
             Args:
-                user (object,optional): User crendentials to be verified.
+                user (object,optional): User credentials to be verified.
 
             Returns:
-                boolean: If it is a public tensor/ allowed user, returns true, otherwise it returns false.
+                boolean: If it is a public tensor/ allowed user, returns true, otherwise it returns
+                false.
         """
         # If it is a wrapper
         if self.is_wrapper:
@@ -638,8 +681,21 @@ def allow(self, user=None) -> bool:
                 current_tensor = current_tensor.child
         return True
 
-    def move(self, location):
-        self.child = self.child.move(location)
+    def move(self, location: BaseWorker, requires_grad: bool = False):
+        """
+        Move acts on a pointer to A to move the remote value to B (=location).
+
+        Note a A will keep a copy of his value that he sent to B. This follows the
+        .send() paradigm where the local worker keeps a copy of the value he sends.
+
+        Args:
+            location: the worker where the remote value should be moved
+            requires_grad: see send() for details
+
+        Returns:
+            A pointer to the worker location
+        """
+        self.child = self.child.move(location, requires_grad)
         # We get the owner from self.child because the owner of a wrapper is
         # not reliable and sometimes end up being the syft.local_worker
         self.child.owner.register_obj(self)
diff --git a/syft/generic/pointers/pointer_tensor.py b/syft/generic/pointers/pointer_tensor.py
index 4c80a8e43cc..a76a7880d43 100644
--- a/syft/generic/pointers/pointer_tensor.py
+++ b/syft/generic/pointers/pointer_tensor.py
@@ -180,10 +180,10 @@ def get_class_attributes(self):
     @staticmethod
     def create_pointer(
         tensor,
-        location: AbstractWorker = None,
+        location: Union[AbstractWorker, str] = None,
         id_at_location: (str or int) = None,
         register: bool = False,
-        owner: AbstractWorker = None,
+        owner: Union[AbstractWorker, str] = None,
         ptr_id: (str or int) = None,
         garbage_collect_data=None,
         shape=None,
@@ -261,24 +261,47 @@ def create_pointer(
 
         return ptr
 
-    def move(self, destination):
-        kwargs = {"inplace": True, "create_pointer": False}
-        message = TensorCommandMessage.communication(
-            self.id_at_location, self.location.id, [destination.id], kwargs
-        )
-        self.owner.send_msg(message=message, location=self.location)
+    def move(self, destination: AbstractWorker, requires_grad: bool = False):
+        """
+        Will move the remove value from self.location A to destination B
 
-        # Change location of the pointer to point to the new object owner
-        self.location = destination
+        Note a A will keep a copy of his value that he sent to B. This follows the
+        .send() paradigm where the local worker keeps a copy of the value he sends.
 
-        return self
+        Args:
+            destination: the new location of the remote data
+            requires_grad: see send() for details
 
-    def remote_send(self, destination):
+        Returns:
+            A pointer to location
+        """
+        # move to local target is equivalent to doing .get()
+        if self.owner.id == destination.id:
+            return self.get()
+
+        ptr = self.remote_send(destination, requires_grad=requires_grad)
+
+        # We make the pointer point at the remote value. As the id doesn't change,
+        # we don't update ptr.id_at_location
+        # Note that you have now 2 pointers on different locations pointing to the
+        # same tensor.
+        ptr.location = destination
+
+        return ptr
+
+    def remote_send(
+        self, destination: AbstractWorker, requires_grad: bool = False,
+    ):
         """ Request the worker where the tensor being pointed to belongs to send it to destination.
         For instance, if C holds a pointer, ptr, to a tensor on A and calls ptr.remote_send(B),
         C will hold a pointer to a pointer on A which points to the tensor on B.
+
+        Args:
+            destination: where the remote value should be sent
+            requires_grad: if true updating the grad of the remote tensor on destination B will trigger
+                a message to update the gradient of the value on A.
         """
-        kwargs = {"inplace": True}
+        kwargs = {"inplace": False, "requires_grad": requires_grad}
         message = TensorCommandMessage.communication(
             self.id_at_location, self.location.id, [destination.id], kwargs
         )
diff --git a/syft/serde/msgpack/torch_serde.py b/syft/serde/msgpack/torch_serde.py
index d01917aaff7..60087649174 100644
--- a/syft/serde/msgpack/torch_serde.py
+++ b/syft/serde/msgpack/torch_serde.py
@@ -148,6 +148,9 @@ def _simplify_torch_tensor(worker: AbstractWorker, tensor: torch.Tensor) -> bin:
     # and here... leaving a reerence here so i can find it later
     # TODO fix pointer bug
 
+    sender = tensor.sender if hasattr(tensor, "sender") else None
+    origin_id = tensor.origin_id if hasattr(tensor, "origin_id") else None
+
     return (
         tensor.id,
         tensor_bin,
@@ -156,6 +159,8 @@ def _simplify_torch_tensor(worker: AbstractWorker, tensor: torch.Tensor) -> bin:
         serde._simplify(worker, tensor.tags),
         serde._simplify(worker, tensor.description),
         serde._simplify(worker, worker.serializer),
+        serde._simplify(worker, sender),
+        serde._simplify(worker, origin_id),
     )
 
 
@@ -174,7 +179,17 @@ def _detail_torch_tensor(worker: AbstractWorker, tensor_tuple: tuple) -> torch.T
         torch.Tensor: a torch tensor that was serialized
     """
 
-    tensor_id, tensor_bin, chain, grad_chain, tags, description, serializer = tensor_tuple
+    (
+        tensor_id,
+        tensor_bin,
+        chain,
+        grad_chain,
+        tags,
+        description,
+        serializer,
+        sender,
+        origin_id,
+    ) = tensor_tuple
 
     tensor = _deserialize_tensor(worker, serde._detail(worker, serializer), tensor_bin)
 
@@ -194,6 +209,8 @@ def _detail_torch_tensor(worker: AbstractWorker, tensor_tuple: tuple) -> torch.T
 
     tensor.tags = serde._detail(worker, tags)
     tensor.description = serde._detail(worker, description)
+    tensor.sender = serde._detail(worker, sender)
+    tensor.origin_id = serde._detail(worker, origin_id)
 
     return tensor
 
@@ -256,6 +273,15 @@ def _detail_torch_parameter(worker: AbstractWorker, param_tuple: tuple) -> torch
     param.grad = grad
     param.is_wrapper = isinstance(tensor, AbstractTensor) or tensor.is_wrapper
 
+    # Note: should be
+    #  param.sender = tensor.sender
+    #  param.origin_id = tensor.origin_id
+    # but the wrapper is lost at serialisation because of the way we hook parameter.data
+    # TODO: fix serialisation of parameters (check in particular .child & .data)
+    # Below is just a fix:
+    param.sender = tensor.sender if hasattr(tensor, "sender") else None
+    param.sender = tensor.sender if hasattr(tensor, "origin_id") else None
+
     return param
 
 
diff --git a/syft/workers/base.py b/syft/workers/base.py
index 2637ba1be48..0d874a85caa 100644
--- a/syft/workers/base.py
+++ b/syft/workers/base.py
@@ -332,6 +332,7 @@ def send(
         workers: "BaseWorker",
         ptr_id: Union[str, int] = None,
         garbage_collect_data=None,
+        requires_grad=False,
         create_pointer=True,
         **kwargs,
     ) -> ObjectPointer:
@@ -383,9 +384,17 @@ def send(
 
         worker = self.get_worker(worker)
 
+        if requires_grad:
+            obj.sender = self.id
+            obj.origin_id = obj.id
+
         # Send the object
         self.send_obj(obj, worker)
 
+        if requires_grad:
+            obj.sender = None
+            obj.origin_id = None
+
         # If we don't need to create the pointer
         if not create_pointer:
             return None
@@ -419,8 +428,23 @@ def handle_object_msg(self, obj_msg: ObjectMessage):
         # Plan/Protocol, etc. As Syft moves toward multi-tenancy with Grid and so forth,
         # that will probably be useful for providing security and permissioning. In that
         # future, this might look like `self.object_store.set_obj(obj_msg.object)`
-        if obj_msg.object is not None:
-            self.set_obj(obj_msg.object)
+
+        # def receive_object(self, obj: Union[FrameworkTensorType, AbstractTensor]) -> None:
+        """Receive an object from a another worker
+
+        Args:
+            obj: a Framework Tensor or a subclass of an AbstractTensor with an id
+        """
+        obj = obj_msg.object
+
+        self.set_obj(obj)
+
+        if isinstance(obj, FrameworkTensor):
+            tensor = obj
+            if tensor.requires_grad and tensor.sender is not None and tensor.origin_id is not None:
+                tensor.register_hook(
+                    tensor.trigger_origin_backward_hook(tensor.sender, tensor.origin_id)
+                )
 
     def handle_delete_object_msg(self, msg: ForceObjectDeleteMessage):
         # NOTE cannot currently be used because there is no ObjectDeleteMessage
@@ -516,17 +540,21 @@ def execute_communication_action(self, action: CommunicationAction) -> PointerTe
 
             response = hook_args.register_response("send", response, [sy.ID_PROVIDER.pop()], self)
 
-            self.rm_obj(obj_id)
+            # @lariffle: We only remove remote objects when the operations are inplace
+            # otherwise we could have stale pointers which we really want to avoid.
+            # TODO: needs more discussion
+            if kwargs.get("inplace"):
+                self.rm_obj(obj_id)
             return response
 
     def execute_worker_command(self, message: tuple):
         """Executes commands received from other workers.
 
-                Args:
-                    message: A tuple specifying the command and the args.
+        Args:
+            message: A tuple specifying the command and the args.
 
-                Returns:
-                    A pointer to the result.
+        Returns:
+            A pointer to the result.
         """
         command_name = message.command_name
         args, kwargs, return_ids = message.message
diff --git a/test/serde/serde_helpers.py b/test/serde/serde_helpers.py
index d41826ac544..10ce57f09c9 100644
--- a/test/serde/serde_helpers.py
+++ b/test/serde/serde_helpers.py
@@ -398,6 +398,8 @@ def compare(detailed, original):
                     (CODE[set], ((CODE[str], (b"tag1",)),)),  # (set of str) tags
                     (CODE[str], (b"desc",)),  # (str) description
                     (CODE[str], (b"torch",)),  # (str) framework
+                    None,  # (int) sender
+                    None,  # (int) origin_id
                 ),
             ),
             "cmp_detailed": compare,
@@ -426,6 +428,8 @@ def compare(detailed, original):
                     (CODE[set], ((CODE[str], (b"tag1",)),)),  # (set of str) tags
                     (CODE[str], (b"desc",)),  # (str) description
                     (CODE[str], (b"all",)),  # (str) framework
+                    None,  # (int) sender
+                    None,  # (int) origin_id
                 ),
             ),
             "cmp_detailed": compare,
diff --git a/test/torch/pointers/test_pointer_tensor.py b/test/torch/pointers/test_pointer_tensor.py
index c350359d589..eeca0658441 100644
--- a/test/torch/pointers/test_pointer_tensor.py
+++ b/test/torch/pointers/test_pointer_tensor.py
@@ -280,7 +280,7 @@ def test_grad_pointer(workers):
 
 
 def test_move(workers):
-    alice, bob, james = workers["alice"], workers["bob"], workers["james"]
+    alice, bob, james, me = workers["alice"], workers["bob"], workers["james"], workers["me"]
 
     x = torch.tensor([1, 2, 3, 4, 5]).send(bob)
 
@@ -289,7 +289,7 @@ def test_move(workers):
 
     x.move(alice)
 
-    assert x.id_at_location not in bob._objects
+    assert x.id_at_location in bob._objects
     assert x.id_at_location in alice._objects
 
     x = torch.tensor([1.0, 2, 3, 4, 5], requires_grad=True).send(bob)
@@ -299,7 +299,7 @@ def test_move(workers):
 
     x.move(alice)
 
-    assert x.id_at_location not in bob._objects
+    assert x.id_at_location in bob._objects
     assert x.id_at_location in alice._objects
 
     alice.clear_objects()
@@ -319,6 +319,15 @@ def test_move(workers):
     remote_ptr2 = remote_ptr.move(alice)
     assert remote_ptr2.id in james._objects.keys()
 
+    # Test .move back to myself
+
+    alice.clear_objects()
+    bob.clear_objects()
+    x = torch.tensor([1.0, 2, 3, 4, 5]).send(bob)
+    y = x.move(alice)
+    z = y.move(me)
+    assert (z == x).all()
+
 
 def test_combine_pointers(workers):
     """
@@ -493,3 +502,49 @@ def test_registration_of_action_on_pointer_of_pointer(workers):
 
     assert len(alice._objects) == 2
     assert len(bob._objects) == 2
+
+
+def test_setting_back_grad_to_origin_after_send(workers):
+    """
+    Calling .backward() on a tensor sent using `.send(..., requires_grad=True)`
+    should update the origin tensor gradient
+    """
+    me = workers["me"]
+    alice = workers["alice"]
+
+    with me.registration_enabled():
+        x = th.tensor([1.0, 2.0, 3, 4, 5], requires_grad=True)
+        y = x + x
+        me.register_obj(y)  # registration on the local worker is sometimes buggy
+
+        y_ptr = y.send(alice, requires_grad=True)
+        z_ptr = y_ptr * 2
+
+        z = z_ptr.sum()
+        z.backward()
+
+        assert (x.grad == th.tensor([4.0, 4.0, 4.0, 4.0, 4.0])).all()
+
+
+def test_setting_back_grad_to_origin_after_move(workers):
+    """
+    Calling .backward() on a tensor moved using `.move(..., requires_grad=True)`
+    should update the origin tensor gradient
+    """
+    me = workers["me"]
+    bob = workers["bob"]
+    alice = workers["alice"]
+
+    with me.registration_enabled():
+        x = th.tensor([1.0, 2.0, 3, 4, 5], requires_grad=True)
+        y = x + x
+        me.register_obj(y)  # registration on the local worker is sometimes buggy
+
+        y_ptr = y.send(alice, requires_grad=True)
+        z_ptr = y_ptr * 2
+
+        z_ptr2 = z_ptr.move(bob, requires_grad=True)
+        z = z_ptr2.sum()
+        z.backward()
+
+        assert (x.grad == th.tensor([4.0, 4.0, 4.0, 4.0, 4.0])).all()


Code review comments:
diff_hunk: @@ -58,6 +58,42 @@ class TorchTensor(AbstractTensor):
     def has_child(self):
         return hasattr(self, "child")
 
+    def trigger_origin_backward_hook(self, sender: str, origin_id: int):
+        """
+        This hook is triggered when a tensor which was received from a sender has
+        a gradient update. It will send back to this sender and his original tensor
+        this gradient value to be set remotely. Also, because this is triggered during
+        backward(), the backward command is also forwarded back.
+
+        Args:
+            sender (str): id of the worker where this tensor comes from
+            origin_id (int): what was its original id
+        """
+
+        def trigger_origin_backward(grad):
+            """
+            The function setting back the gradient and calling backward
+
+            Args:
+                grad: the gradient tensor being set
+            """
+
+            location = self.owner.get_worker(sender)
+
+            origin_ptr = syft.PointerTensor.create_pointer(
path: syft/frameworks/torch/tensors/interpreters/native.py
text: @user1:
Not sure I followed...
You're creating a pointer from sender to receiver? And then set grad on this new pointer?

@author:
I create a pointer from the receiver to the sender (so in the reverse order), back to the origin the tensor came from, and I ask to set back there a gradient

@user1:
Oh no, ok, the pointer points back to the origin tensor (thanks to id_at_location=origin_id).
So basically, you just want to send a message to sender to tell him to set_grad(something) on the tensor with id origin_id. Don't you think it would be better to use messages and actions directly instead of going through a pointer (which is less intuitive, I think)

@author:
I'm made a proposition using your brand new TensorCommandMessage.computation :) 

@user1:
Much sexier 😍

diff_hunk: @@ -197,9 +197,40 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 8,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Epoch 0 - Training loss: 0.3258790969848633\n",
+      "Epoch 1 - Training loss: 0.23088108003139496\n"
+     ]
+    },
+    {
+     "ename": "KeyboardInterrupt",
+     "evalue": "",
+     "output_type": "error",
+     "traceback": [
path: examples/tutorials/advanced/Split Neural Network/Tutorial 1 - SplitNN Introduction.ipynb
text: @user1:
Beware, the notebook fails here

diff_hunk: @@ -148,6 +148,9 @@ def _simplify_torch_tensor(worker: AbstractWorker, tensor: torch.Tensor) -> bin:
     # and here... leaving a reerence here so i can find it later
     # TODO fix pointer bug
 
+    sender = tensor.sender if hasattr(tensor, "sender") else None
path: syft/serde/msgpack/torch_serde.py
text: @user1:
To be sure I understand, you added a sender and an origin_id attribute on an object when it's sent?
I didn't think as much as you about it but maybe it would be cleaner to add this information in the message and not add it on the object?

@author:
Haha it was in the message but based on Karl suggestions I moved it to the object, where it seems somewhat cleaner :) 

@user1:
Hum, I still don't think dynamically adding attributes to objects is clean but if you agreed on it, it's ok for now :)

@user2:
I'm not a huge fan of dynamically adding attributes either. I'd rather see them set via the constructor (though I'm not sure how feasible that is here.)

In terms of where to put the attributes, my thinking was that we're essentially trying to build up a cross-worker tensor chain, and tensors already contain the information required to handle normal tensor chains, so that might be a reasonable place to keep this information too. It does seem like there's some overlap between pointers, messages/actions, and tensor chaining that's pretty fuzzy right now.

I'm not sure `sender` is actually the right name for this attribute if it's on a tensor, because that sort of implies a messaging context. Maybe `origin`? 🤔 

@author:
I'll go for `origin` and  set them in the "constructor" (or kindof) :)