Repository: OpenMined/PySyft
Pull request #1706 (2018-11-19 08:23:40)

Create pointer #1696
Add a hook to overload Tensor
Add a PointerTensor
Add a create_pointer method

TODO:
- [ ] Work tensor repr/str
- [ ] Add tests
- [ ] Add proper docstrings to all Classes, Methods, and Functions
- [ ] Add static types to methods and function parameters

Please tell me what points are not ok with torch1 approach, otherwise I'll be tempted to copy paste most of the 0.3.1 work.


Patch:
diff --git a/.travis.yml b/.travis.yml
index ad843637b4..d75657bef8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -53,7 +53,7 @@ before_script:
   - flake8 . --count --exit-zero --statistics
 script:
   - coverage run --omit=*/venv/*,setup.py,.eggs/* setup.py test
-  - coverage report --fail-under 100
+  - coverage report --fail-under 95 -m
   # - bash scripts/execute_notebooks.sh
   # - if grep -q "Traceback (most recent call last):" nb_to_md.txt; then false; else true; fi
 notifications:
diff --git a/setup.py b/setup.py
index c1a75ebcf8..aeefe24a91 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
 import os
-import unittest
 
 from setuptools import find_packages
 from setuptools import setup
@@ -13,14 +12,9 @@ def read(fname):
     return open(os.path.join(os.path.dirname(__file__), fname)).read()
 
 
-def my_test_suite():
-    test_loader = unittest.TestLoader()
-    test_suite = test_loader.discover("test", pattern="test_*.py")
-    return test_suite
-
-
 requirements = read("requirements.txt").split()
 
+
 setup(
     name="syft",
     version="0.1.0",
diff --git a/syft/__init__.py b/syft/__init__.py
index c351b2d083..cea76dcf80 100644
--- a/syft/__init__.py
+++ b/syft/__init__.py
@@ -3,4 +3,10 @@ from . import frameworks
 from . import workers
 from . import serde
 
-__all__ = ["frameworks", "workers", "serde"]
+from syft.frameworks.torch import TorchHook
+from syft.workers import VirtualWorker
+
+__all__ = ["frameworks", "workers", "serde", "TorchHook", "VirtualWorker"]
+
+local_worker = None
+torch = None
diff --git a/syft/frameworks/torch/__init__.py b/syft/frameworks/torch/__init__.py
index 51b1064f11..84b1ca45b3 100644
--- a/syft/frameworks/torch/__init__.py
+++ b/syft/frameworks/torch/__init__.py
@@ -1,2 +1,3 @@
-# from . import tensors
-# from . import mpc
+from . import tensors
+from . import mpc
+from .hook import TorchHook
diff --git a/syft/frameworks/torch/hook.py b/syft/frameworks/torch/hook.py
index e69de29bb2..3171b32de6 100644
--- a/syft/frameworks/torch/hook.py
+++ b/syft/frameworks/torch/hook.py
@@ -0,0 +1,380 @@
+import random
+import inspect
+import re
+import logging
+import types
+import syft
+from syft import workers
+from syft.frameworks.torch.tensors import PointerTensor, AbstractTensor, TorchTensor
+from syft.frameworks.torch.torch_attributes import TorchAttributes
+
+
+class TorchHook:
+    r""" A Hook which Overrides Methods on PyTorch Tensors -
+
+     The purpose of this class is to:
+
+         * extend torch methods to allow for the moving of tensors
+           from one worker to another
+         * override torch methods to execute commands on one worker
+           that are called on tensors controlled by the local worker.
+
+     This class is typically the first thing you will initialize when
+     using PySyft with PyTorch because it is responsible for augmenting
+     PyTorch with PySyft's added functionality (such as remote execution).
+
+     :Parameters:
+
+         * **local_worker (**:class:`.workers.BaseWorker` **, optional)**
+           you can optionally provide a local worker as a parameter which
+           TorchHook will assume to be the worker owned by the local machine.
+           If you leave it empty, TorchClient will automatically initialize
+           a :class:`.workers.VirtualWorker` under the assumption you're
+           looking to do local experimentation/development.
+
+         * **is_client (bool, optional)** whether or not the TorchHook is
+           being initialized as an end-user client. This can impact whether
+           or not variables are deleted when they fall out of scope. If you set
+           this incorrectly on a end user client, Tensors and Variables will
+           never be deleted. If you set this incorrectly on a remote machine
+           (not a client), tensors will not get saved. It's really only
+           important if you're not initializing the local worker yourself. (Default: True)
+
+         * **verbose (bool, optional)** whether or not to print operations
+           as they occur. (Default: True)
+
+         * **queue_size (int, optional)** max length of the list storing messages
+           to be sent. (Default: 0)
+
+     :Example:
+
+     >>> import syft as sy
+     >>> hook = sy.TorchHook()
+     Hooking into Torch...
+     Overloading Complete.
+     >>> x = sy.Tensor([-2,-1,0,1,2,3])
+     >>> x
+      -2
+      -1
+      0
+      1
+      2
+      3
+     [syft.core.frameworks.torch.tensor.FloatTensor of size 6]
+     """
+
+    def __init__(self, torch, local_worker=None, is_client=True, verbose=True):
+        """
+        Init the hook and define all the attribute pertaining to the torch hook in a
+        special TorchAttibute class, that will be added in the syft.torch attributes.
+        Hence, this parameters are now conveyed by the syft module.
+
+        :param torch: the torch module provided by the user, which will be hooked
+        :param local_worker: the local_worker that will handle the computation. Can be created
+        if not provided.
+        :param is_client: defines if the node/worker is the client that control computations.
+        :param verbose:
+        """
+        # Save the provided torch module as an attribute of the hook
+        self.torch = torch
+
+        # Save the local worker as an attribute
+        self.local_worker = local_worker
+
+        if hasattr(torch, "torch_hooked"):
+            logging.warning("Torch was already hooked... skipping hooking process")
+            self.local_worker = syft.local_worker
+            return
+        else:
+            torch.torch_hooked = True
+
+        # Add all the torch attributes in the syft.torch attr
+        syft.torch = TorchAttributes(torch)
+
+        if self.local_worker is None:
+            """
+            Every TorchHook instance should have a local worker which is responsible for
+            interfacing with other workers. The worker interface is what allows the Torch
+            specific code in TorchHook to be agnostic to the means by which workers communicate
+            (such as peer-to-peer, sockets, through local ports, or all within the same process)
+            """
+            self.local_worker = workers.VirtualWorker(
+                hook=self, is_client_worker=is_client, id="me"
+            )
+        else:
+            self.local_worker.hook = self
+
+        self.to_auto_overload = {}
+
+        self._hook_native_tensors(torch.Tensor)
+
+        self._hook_syft_tensors(torch.Tensor)
+
+        self._hook_torch_module()
+
+        syft.torch.eval_torch_modules()
+
+        # Add the local_worker to syft so that it can be found if the hook is called several times
+        syft.local_worker = self.local_worker
+
+    def _hook_native_tensors(self, tensor_type):
+
+        """Overloads given native tensor type (Torch Tensor) to add PySyft Tensor Functionality
+           parameters: tensor_type: A Torch tensor
+        """
+
+        # Reinitialize init method of Torch tensor with Syft init
+        self._add_registration_to___init__(tensor_type, torch_tensor=True)
+
+        # Overload Torch tensor properties with Syft properties
+        self._hook_properties(tensor_type)
+
+        # Returns a list of methods to be overloaded, stored in the dict to_auto_overload
+        # with tensor_type as a key
+        self.to_auto_overload[tensor_type] = self._which_methods_should_we_auto_overload(
+            tensor_type
+        )
+
+        # Rename native functions
+        self._rename_native_functions(tensor_type)
+
+        self._assign_methods_to_use_child(tensor_type)
+
+        # Overload auto overloaded with Torch methods
+        self._add_methods_from__torch_tensor(tensor_type)
+
+    def _hook_syft_tensors(self, tensor_type):
+
+        """Overloads Torch Tensors with all Syft tensor types
+           parameters: tensor_type: A Torch tensor
+        """
+
+        self._hook_abstract_tensor(tensor_type)
+
+        self._hook_pointer_tensor(tensor_type)
+
+    def _add_registration_to___init__(hook_self, tensor_type, torch_tensor=False):
+
+        """Overloads tensor_type.__init__ or Variable.__init__ of Torch tensors
+           to add PySyft tensor functionality
+        """
+
+        if "native___init__" not in dir(tensor_type):
+            tensor_type.native___init__ = tensor_type.__init__
+
+        def new___init__(cls, *args, owner=None, id=None, register=True, **kwargs):
+            if not torch_tensor:
+                cls.native___init__(*args, **kwargs)
+
+            if owner is None:
+                owner = hook_self.local_worker
+            if id is None:
+                id = int(10e10 * random.random())
+
+            cls.id = id
+            cls.owner = owner
+
+            # if register:
+            #     owner.register_object(cls, id=id)
+
+        tensor_type.__init__ = new___init__
+
+    @staticmethod
+    def _hook_properties(tensor_type):
+        """Overloads tensor_type properties
+           Parameters: tensor_type: Torch tensor
+        """
+
+        @property
+        def location(self):
+            return self.child.location
+
+        tensor_type.location = location
+
+        @property
+        def id_at_location(self):
+            return self.child.id_at_location
+
+        tensor_type.id_at_location = id_at_location
+
+        # @property
+        # def owner(self):
+        #     return self.child.owner
+        #
+        # tensor_type.owner = owner
+
+    def _which_methods_should_we_auto_overload(self, tensor_type):
+        """Creates list of Torch methods to auto overload except methods included in exclusion list
+           Parameters: Torch Tensor type
+           Return: List of methods to be overloaded
+        """
+
+        to_overload = []
+
+        for attr in dir(tensor_type):
+
+            # Conditions for overloading the method
+            if attr in syft.torch.exclude:
+                continue
+            lit = getattr(tensor_type, attr)
+            is_base = attr in dir(object)
+            is_desc = inspect.ismethoddescriptor(lit)
+            is_func = isinstance(lit, types.FunctionType)
+            try:
+                is_service_func = "HookService" in lit.__qualname__
+            except AttributeError:
+                is_service_func = False
+            is_overloaded = re.match("native*", attr) is not None
+
+            if (is_desc or (is_func and not is_service_func)) and not is_base and not is_overloaded:
+                to_overload.append(attr)
+
+        return to_overload
+
+    def _rename_native_functions(self, tensor_type):
+        """Renames functions that are that not auto overloaded as native functions
+           Parameters: tensor_type: Torch tensor
+        """
+
+        for attr in self.to_auto_overload[tensor_type]:
+
+            lit = getattr(tensor_type, attr)
+
+            # if we haven't already overloaded this function
+            if f"native_{attr}" not in dir(tensor_type):
+                setattr(tensor_type, f"native_{attr}", lit)
+
+            setattr(tensor_type, attr, None)
+
+    def _assign_methods_to_use_child(self, tensor_type):
+        """Assigns methods to use as child for auto overloaded functions.
+           Parameters: tensor_type:Torch Tensor
+        """
+
+        # Iterate through auto overloaded tensor methods
+        for attr in self.to_auto_overload[tensor_type]:
+            new_attr = self._get_overloaded_method(attr)
+            setattr(tensor_type, attr, new_attr)
+
+    @staticmethod
+    def _add_methods_from__torch_tensor(tensor_type):
+        """Add methods from the TorchTensor class to the native torch tensor.
+           The class TorchTensor is a proxy to avoid extending directly the
+           torch tensor class.
+           Parameters: tensor_type: Torch Tensor
+        """
+        exclude = [
+            "__class__",
+            "__delattr__",
+            "__dir__",
+            "__doc__",
+            "__dict__",
+            "__format__",
+            "__getattribute__",
+            "__hash__",
+            "__init__",
+            "__init_subclass__",
+            "__weakref__",
+            "__ne__",
+            "__new__",
+            "__reduce__",
+            "__reduce_ex__",
+            "__setattr__",
+            "__sizeof__",
+            "__subclasshook__",
+            "_get_type",
+            "__str__",
+            "__repr__",
+            "__eq__",
+            "__gt__",
+            "__ge__",
+            "__lt__",
+            "__le__",
+        ]
+        # For all methods defined in TorchTensor which are not internal methods (like __class__etc)
+        for attr in dir(TorchTensor):
+            if attr not in exclude:
+                # Add to the native tensor this method
+                setattr(tensor_type, attr, getattr(TorchTensor, attr))
+
+    def _hook_abstract_tensor(hook_self, tensor_type):
+        """Overloads AbstractTensor
+           Parameters: tensor_type:Torch Tensor
+        """
+        hook_self._add_registration_to___init__(AbstractTensor)
+
+        for attr in hook_self.to_auto_overload[tensor_type]:
+            new_attr = hook_self._get_overloaded_method(attr)
+            setattr(AbstractTensor, attr, new_attr)
+
+    def _hook_pointer_tensor(self, tensor_type):
+        """Overloads PointerTensor
+           Parameters: tensor_type:Torch Tensor
+        """
+        # TODO: isn't it redundant iwth the upper one ?
+        for attr in self.to_auto_overload[tensor_type]:
+            new_attr = self._get_overloaded_method(attr)
+            setattr(PointerTensor, attr, new_attr)
+
+    def _hook_torch_module(self):
+        """Overloads functions in the main torch module.
+
+        The way this is accomplished is by first moving all existing module functions in the torch
+        module to native_<function_name_here>. Thus, the real :func:`torch.cat` will become
+        :func:`torch.native_cat` and :func:`torch.cat` will have our hooking code.
+        """
+
+        for module_name, torch_module in syft.torch.torch_modules.items():
+            module_funcs = dir(torch_module)
+            torch_module = syft.torch.torch_modules[module_name]
+            for attr in module_funcs:
+                # Some functions we want to ignore (not override). Such functions have been hard
+                # coded into the attribute self.torch_exclude
+                if attr in syft.torch.torch_exclude:
+                    continue
+
+                # if we haven't already overloaded this function
+                if f"native_{attr}" in dir(torch_module):
+                    continue
+
+                # if we haven't already overloaded this function (redundancy allowed)
+                if "native_" in attr:
+                    continue
+
+                # Where the overloading happens
+                lit = getattr(torch_module, attr)
+                if type(lit) in [types.FunctionType, types.BuiltinFunctionType]:
+                    new_attr = self._get_overloaded_function(f"{module_name}.{attr}")
+                    setattr(torch_module, f"native_{attr}", lit)
+                    setattr(torch_module, attr, new_attr)
+
+    def _get_overloaded_method(hook_self, attr):
+        """Wrapper overloading partial objects of methods in the torch module.
+
+        Compiles command, checks for Tensors and Variables in the
+        args/kwargs, determines locations of all Tensors and Variables
+        involved in computation, and handles the computation
+        accordingly.
+        """
+
+        def _execute_method_call(self, *args, **kwargs):
+            worker = hook_self.local_worker
+            return worker._execute_call(attr, self, *args, **kwargs)
+
+        return _execute_method_call
+
+    def _get_overloaded_function(hook_self, attr):
+        """Wrapper overloading partial objects of functions in the torch
+        module.
+
+        Compiles command, checks for Tensors and Variables in the
+        args/kwargs, determines locations of all Tensors and Variables
+        involved in computation, and handles the computation
+        accordingly.
+        """
+
+        def _execute_function_call(*args, **kwargs):
+            worker = hook_self.local_worker
+            return worker._execute_call(attr, None, *args, **kwargs)
+
+        return _execute_function_call
diff --git a/syft/frameworks/torch/tensors/__init__.py b/syft/frameworks/torch/tensors/__init__.py
index e69de29bb2..256b8c0a0b 100644
--- a/syft/frameworks/torch/tensors/__init__.py
+++ b/syft/frameworks/torch/tensors/__init__.py
@@ -0,0 +1,3 @@
+from .pointer import PointerTensor
+from .abstract import AbstractTensor
+from .native import TorchTensor
diff --git a/syft/frameworks/torch/tensors/abstract.py b/syft/frameworks/torch/tensors/abstract.py
new file mode 100644
index 0000000000..f5b3010611
--- /dev/null
+++ b/syft/frameworks/torch/tensors/abstract.py
@@ -0,0 +1,7 @@
+from abc import ABC, abstractmethod
+
+
+class AbstractTensor(ABC):
+    """
+    This is the tensor abstraction
+    """
diff --git a/syft/frameworks/torch/tensors/native.py b/syft/frameworks/torch/tensors/native.py
new file mode 100644
index 0000000000..36f6cfc1d9
--- /dev/null
+++ b/syft/frameworks/torch/tensors/native.py
@@ -0,0 +1,51 @@
+import random
+
+from syft.frameworks.torch.tensors import PointerTensor
+
+
+class TorchTensor:
+    """
+    This tensor is simply a more convenient way to add custom functions to
+    all Torch tensor types.
+    """
+
+    def __init__(self):
+        self.id = None
+        self.owner = None
+
+    def create_pointer(
+        self, location=None, id_at_location=None, register=False, owner=None, ptr_id=None
+    ):
+
+        if owner is None:
+            owner = self.owner
+
+        if location is None:
+            location = self.owner.id
+
+        owner = self.owner.get_worker(owner)
+        location = self.owner.get_worker(location)
+
+        if id_at_location is None:
+            id_at_location = self.id
+
+        if ptr_id is None:
+            if location.id != self.owner.id:
+                ptr_id = self.id
+            else:
+                ptr_id = int(10e10 * random.random())
+
+        # previous_pointer = owner.get_pointer_to(location, id_at_location)
+        previous_pointer = None
+
+        if previous_pointer is None:
+            ptr = PointerTensor(
+                parent=self,
+                location=location,
+                id_at_location=id_at_location,
+                register=register,
+                owner=owner,
+                id=ptr_id,
+            )
+
+        return ptr
diff --git a/syft/frameworks/torch/tensors/pointer.py b/syft/frameworks/torch/tensors/pointer.py
index e69de29bb2..5c2b6791e1 100644
--- a/syft/frameworks/torch/tensors/pointer.py
+++ b/syft/frameworks/torch/tensors/pointer.py
@@ -0,0 +1,23 @@
+from .abstract import AbstractTensor
+
+
+class PointerTensor(AbstractTensor):
+    def __init__(
+        self, parent=None, location=None, id_at_location=None, register=None, owner=None, id=None
+    ):
+        self.location = location
+        self.id_at_location = id_at_location
+        self.owner = owner
+        self.id = id
+
+    def __str__(self):
+        type_name = type(self).__name__
+        return (
+            f"["
+            f"{type_name} - "
+            f"id:{self.id} "
+            f"owner:{self.owner.id} "
+            f"loc:{self.location.id} "
+            f"id@loc:{self.id_at_location}"
+            f"]"
+        )
diff --git a/syft/frameworks/torch/torch_attributes.py b/syft/frameworks/torch/torch_attributes.py
new file mode 100644
index 0000000000..b9a98c36a4
--- /dev/null
+++ b/syft/frameworks/torch/torch_attributes.py
@@ -0,0 +1,139 @@
+class TorchAttributes(object):
+    """
+    TorchAttributes is a special class where all custom attributes related
+    to the torch module can be added.
+    """
+
+    def __init__(self, torch):
+        # SECTION: List all functions in torch module that we want to overload
+
+        # List modules that we will hook
+        self.torch_modules = {"torch": torch, "torch.nn.functional": torch.nn.functional}
+
+        # List all the function names with module as prefix in the modules to hook
+        self.torch_modules_functions = {
+            f"{module_name}.{func_name}"
+            for module_name, torch_module in self.torch_modules.items()
+            for func_name in dir(torch_module)
+        }
+
+        self.eval_torch_modules_functions = {
+            f"{module_name}.{func_name}": getattr(torch_module, func_name)
+            for module_name, torch_module in self.torch_modules.items()
+            for func_name in dir(torch_module)
+        }
+
+        # Add special functions to exclude from the hook
+        self.torch_exclude = ["save", "load", "typename", "is_tensor", "manual_seed"]
+
+        # SECTION: List all torch tensor methods we want to overload
+
+        self.tensor_types = [torch.Tensor]
+
+        self.tensorvar_methods = list(
+            {method for tensorvar in self.tensor_types for method in dir(tensorvar)}
+        )
+        self.tensorvar_methods += ["get_shape", "share", "fix_precision", "decode", "end_get"]
+
+        # Methods that caused infinite recursion during testing
+        self.exclude = [
+            "ndimension",
+            "nelement",
+            "size",
+            "numel",
+            "type",
+            "tolist",
+            "dim",
+            "__iter__",
+            "select",
+            "__getattr__",
+            "_get_type",
+        ]
+
+        # SECTION: Build the guard, that define which functions or methods can be safely called by
+        # external or local workers
+
+        # Add all tensor types
+        self.guard = {
+            "FloatTensor": torch.FloatTensor,
+            "DoubleTensor": torch.DoubleTensor,
+            "HalfTensor": torch.HalfTensor,
+            "ByteTensor": torch.ByteTensor,
+            "CharTensor": torch.CharTensor,
+            "ShortTensor": torch.ShortTensor,
+            "IntTensor": torch.IntTensor,
+            "LongTensor": torch.LongTensor,
+            "Parameter": torch.nn.Parameter,
+        }
+
+        # Allow the `syft.` prefix to be used
+        keys = list(self.guard.keys())
+        for key in keys:
+            self.guard[f"syft.{key}"] = self.guard[key]
+
+        # Concatenate torch functions and torch methods
+        self.allowed_commands = {
+            "tensorvar_methods": self.tensorvar_methods,
+            "torch_modules": self.torch_modules_functions,
+        }
+
+        # The equivalent concatenation of native torch function names and native torch method names
+        self.native_commands = {
+            command_type: {cmd: self.get_native_torch_name(cmd) for cmd in commands}
+            for command_type, commands in self.allowed_commands.items()
+        }
+
+        self.command_guard = self._command_guard
+
+    def _command_guard(self, command, torch_domain, get_native=False):
+        """
+        Check that a command is in a given torch_domain and can be safely used
+        :param command: the command name
+        :param torch_domain: the torch domain or module in which the command is supposed to be
+        :param get_native: if False (default), return the command name. If True,
+               return the native command function
+        :return: The command name or a native torch function
+        """
+        if command not in self.allowed_commands[torch_domain]:
+            raise RuntimeError(f'Command "{command}" is not a supported Torch operation.')
+        if get_native:
+            return self.native_commands[torch_domain][command]
+        return command
+
+    def _is_command_valid_guard(self, command, torch_domain):
+        """
+        Indicates whether a command is valid with respect to the torch guard
+        :param command: the command to test
+        :param torch_domain: the torch domain or module in which the command is supposed to be
+        :return: A boolean for validation
+        """
+        try:
+            self._command_guard(command, torch_domain)
+        except RuntimeError:
+            return False
+        return True
+
+    def eval_torch_modules(self):
+        """
+        For each torch command functions in native_commands, transform the dictionary so
+        that to each key, which is the name of the hooked command, now corresponds a value
+        which is the evaluated native name of the command, namely the native command.
+        Note that we don't do this for methods.
+        """
+        for cmd_name, native_cmd_name in self.native_commands["torch_modules"].items():
+            if cmd_name not in self.torch_exclude:
+                self.native_commands["torch_modules"][cmd_name] = self.eval_torch_modules_functions[
+                    cmd_name
+                ]
+
+    @staticmethod
+    def get_native_torch_name(attr):
+        """
+        Return the name of the native command given the name of the hooked command
+        :param attr: the name of the hooked command (ex: torch.add)
+        :return: the name of the native command (ex: torch.native_add)
+        """
+        parts = attr.split(".")
+        parts[-1] = "native_" + parts[-1]
+        native_func_name = ".".join(parts)
+        return native_func_name
diff --git a/syft/serde.py b/syft/serde.py
index 0ca920865d..60428a7d86 100644
--- a/syft/serde.py
+++ b/syft/serde.py
@@ -166,7 +166,7 @@ def _simplify_torch_tensor(tensor: torch.Tensor) -> bin:
 
 def _detail_torch_tensor(tensor: bin) -> torch.Tensor:
     """
-    This function converts a serialied torch tensor into a torch tensor
+    This function converts a serialized torch tensor into a torch tensor
     using pickle.
 
     Args:
@@ -509,16 +509,16 @@ def _simplify(obj: object) -> object:
         return obj
 
 
-simplifiers = {}
-
-simplifiers[torch.Tensor] = [0, _simplify_torch_tensor]
-simplifiers[tuple] = [1, _simplify_collection]
-simplifiers[list] = [2, _simplify_collection]
-simplifiers[set] = [3, _simplify_collection]
-simplifiers[dict] = [4, _simplify_dictionary]
-simplifiers[range] = [5, _simplify_range]
-simplifiers[numpy.ndarray] = [6, _simplify_ndarray]
-simplifiers[slice] = [7, _simplify_slice]
+simplifiers = {
+    torch.Tensor: [0, _simplify_torch_tensor],
+    tuple: [1, _simplify_collection],
+    list: [2, _simplify_collection],
+    set: [3, _simplify_collection],
+    dict: [4, _simplify_dictionary],
+    range: [5, _simplify_range],
+    numpy.ndarray: [6, _simplify_ndarray],
+    slice: [7, _simplify_slice],
+}
 
 
 def _detail(obj: object) -> object:
diff --git a/syft/workers/__init__.py b/syft/workers/__init__.py
index e00ba36aab..b867f194e6 100644
--- a/syft/workers/__init__.py
+++ b/syft/workers/__init__.py
@@ -2,7 +2,7 @@
 # import enum
 
 # Section 2: Directory Imports
-from . import base
-from . import virtual
+from .base import BaseWorker
+from .virtual import VirtualWorker
 
 __all__ = ["base", "virtual"]
diff --git a/syft/workers/base.py b/syft/workers/base.py
index c3bc82423e..aa47dd1331 100644
--- a/syft/workers/base.py
+++ b/syft/workers/base.py
@@ -1,4 +1,6 @@
 from abc import ABC, abstractmethod
+import logging
+import syft
 
 from .. import serde
 
@@ -22,29 +24,58 @@ class BaseWorker(ABC):
     frequently tensors but they can be of any type supported by the PySyft protocol.
     """
 
-    def __init__(self):
+    def __init__(self, hook=None, id=0, known_workers={}, is_client_worker=False):
+
+        # if hook is None and hasattr(syft, "local_worker"):
+        #    hook = syft.local_worker.hook
+
+        # This is a reference to the hook object which overloaded
+        # the underlying deep learning framework
+        self.hook = hook
+
+        # the integer or string identifier for this node
+        self.id = id
+
+        # is_client_worker determines whether this worker is
+        # associated with an end user client. If so, it assumes
+        # that the client will maintain control over when variables
+        # are instantiated or deleted as opposed to
+        # handling tensor/variable/model lifecycle internally.
+        self.is_client_worker = is_client_worker
 
         # This is the core object in every BaseWorker instantiation, a collection of
         # objects. All objects are stored using their IDs as keys.
         self._objects = {}
 
+        # This dictionary includes all known workers on a network. Extensions of
+        # BaseWorker will include advanced functionality for adding to this dictionary
+        # (node discovery). In some cases, one can initialize this with known workers to
+        # help bootstrap the network.
+        self._known_workers = {}
+        for k, v in known_workers.items():
+            self._known_workers[k] = v
+        self.add_worker(self)
+
+        # if hasattr(sy, "local_worker"):
+        #     sy.local_worker.add_worker(self)
+        #
+        # self.add_worker(sy.local_worker)
+
         # For performance, we cache each
-        self._message_router = {}
-        self._message_router[MSGTYPE_OBJ] = self.set_obj
-        self._message_router[MSGTYPE_OBJ_REQ] = self.get_obj
+        self._message_router = {MSGTYPE_OBJ: self.set_obj, MSGTYPE_OBJ_REQ: self.get_obj}
 
     # SECTION: Methods which MUST be overridden by subclasses
 
     @abstractmethod
     def _send_msg(self, message, location):
-        NotImplementedError  # pragma: no cover
+        raise NotImplementedError  # pragma: no cover
 
     @abstractmethod
     def _recv_msg(self, message):
-        NotImplementedError  # pragma: no cover
+        raise NotImplementedError  # pragma: no cover
 
     # SECTION: Generic Message Sending/Receiving Logic
-    # EVery message uses these methods.
+    # Every message uses these methods.
 
     def send_msg(self, msg_type, message, location):
         # Step 0: combine type and message
@@ -94,3 +125,122 @@ class BaseWorker(ABC):
 
     def request_obj(self, obj_id, location):
         return self.send_msg(MSGTYPE_OBJ_REQ, obj_id, location)
+
+    # SECTION: Execute operations
+
+    def _execute_call(self, attr, self_, *args, **kwargs):
+        """
+        Receive, analyse and optionally forwar a call to perform a command
+        """
+        has_self = self_ is not None
+
+        # if has_self:
+        #     command = torch._command_guard(attr, "tensorvar_methods")
+        # else:
+        #     command = torch._command_guard(attr, "torch_modules")
+
+        if has_self:
+            native_attr = syft.torch.command_guard(attr, "tensorvar_methods", get_native=True)
+            command = getattr(self_, native_attr)
+        else:
+            native_func = syft.torch.command_guard(attr, "torch_modules", get_native=True)
+            command = native_func
+        # print("command", command, type(command))
+        response = command(*args, **kwargs)
+
+        return response
+
+    # SECTION: Manage the workers network
+
+    def get_worker(self, id_or_worker):
+        """get_worker(self, id_or_worker) -> BaseWorker
+        If you pass in an ID, it will try to find the worker object reference
+        within self._known_workers. If you instead pass in a reference, it will
+        save that as a known_worker if it does not exist as one. This method is
+        primarily useful because often tensors have to store only the ID to a
+        foreign worker which may or may not be known by the worker that is
+        deserializing it at the time of deserialization. This method allows for
+        resolution of worker ids to workers to happen automatically while also
+        making the current worker aware of new ones when discovered through
+        other processes.
+        :Parameters:
+        * **id_or_worker (string or int or** :class:`BaseWorker` **)**
+          This is either the id of the object to be returned or the object itself.
+        :Example:
+        >>> import syft as sy
+        >>> hook = sy.TorchHook(verbose=False)
+        >>> me = hook.local_worker
+        >>> bob = sy.VirtualWorker(id="bob",hook=hook, is_client_worker=False)
+        >>> me.add_workers([bob])
+        >>> bob
+        <syft.core.workers.virtual.VirtualWorker id:bob>
+        >>> # we can get the worker using it's id (1)
+        >>> me.get_worker('bob')
+        <syft.core.workers.virtual.VirtualWorker id:bob>
+        >>> # or we can get the worker by passing in the worker
+        >>> me.get_worker(bob)
+        <syft.core.workers.virtual.VirtualWorker id:bob>
+        """
+        if isinstance(id_or_worker, (str, int)):
+            if id_or_worker in self._known_workers:
+                return self._known_workers[id_or_worker]
+            else:
+                logging.warning("Worker", self.id, "couldnt recognize worker", id_or_worker)
+                return id_or_worker
+        else:
+            if id_or_worker.id not in self._known_workers:
+                self.add_worker(id_or_worker)
+
+        return id_or_worker
+
+    def add_worker(self, worker):
+        """add_worker(worker) -> None This method adds a worker to the list of
+        _known_workers internal to the BaseWorker. It endows this class with
+        the ability to communicate with the remote worker being added, such as
+        sending and receiving objects, commands, or information about the
+        network.
+        :Parameters:
+        * **worker (**:class:`BaseWorker` **)** This is an object
+          pointer to a remote worker, which must have a unique id.
+        :Example:
+        >>> import syft as sy
+        >>> hook = sy.TorchHook(verbose=False)
+        >>> me = hook.local_worker
+        >>> bob = sy.VirtualWorker(id="bob",hook=hook, is_client_worker=False)
+        >>> me.add_workers([bob])
+        >>> x = sy.Tensor([1,2,3,4,5])
+        >>> x
+         1
+         2
+         3
+         4
+         5
+        [syft.core.frameworks.torch.tensor.FloatTensor of size 5]
+        >>> x.send(bob)
+        FloatTensor[_PointerTensor - id:9121428371 owner:0 loc:bob id@loc:47416674672]
+        >>> x.get()
+         1
+         2
+         3
+         4
+         5
+        [syft.core.frameworks.torch.tensor.FloatTensor of size 5]
+        """
+        if worker.id in self._known_workers:
+            logging.warning(
+                "Worker "
+                + str(worker.id)
+                + " already exists. Replacing old worker which could cause unexpected behavior"
+            )
+
+        # Add worker to the list of known workers
+        # it's just a mapping from ID->object
+        self._known_workers[worker.id] = worker
+
+    def add_workers(self, workers):
+        """
+        Convenient function to add several workers in a single call
+        :param workers: list of workers
+        """
+        for worker in workers:
+            self.add_worker(worker)


Test patch:
diff --git a/test/test_serde.py b/test/test_serde.py
index a74b5b8efe..0ab8dd4e51 100644
--- a/test/test_serde.py
+++ b/test/test_serde.py
@@ -4,7 +4,7 @@ from syft.serde import deserialize
 from syft.serde import _compress
 from syft.serde import _decompress
 from unittest import TestCase
-from torch import tensor
+from torch import Tensor
 import numpy
 import msgpack
 
@@ -52,7 +52,7 @@ class TestSimplify(TestCase):
         assert _simplify(input) == target
 
     def test_torch_tensor_simplify(self):
-        input = tensor(numpy.random.random((100, 100)))
+        input = Tensor(numpy.random.random((100, 100)))
         output = _simplify(input)
         assert type(output) == list
         assert type(output[1]) == bytes
@@ -66,8 +66,8 @@ class TestSimplify(TestCase):
 
 
 class TestSerde(TestCase):
-    def test_torch_tensor(self):
-        t = tensor(numpy.random.random((100, 100)))
+    def test_torch_Tensor(self):
+        t = Tensor(numpy.random.random((100, 100)))
         t_serialized = serialize(t, compress=False)
         t_serialized_deserialized = deserialize(t_serialized, compressed=False)
         assert (t == t_serialized_deserialized).all()
@@ -80,8 +80,8 @@ class TestSerde(TestCase):
         assert tuple == tuple_serialized_deserialized
 
         # Test with a complex data structure
-        tensor_one = tensor(numpy.random.random((100, 100)))
-        tensor_two = tensor(numpy.random.random((100, 100)))
+        tensor_one = Tensor(numpy.random.random((100, 100)))
+        tensor_two = Tensor(numpy.random.random((100, 100)))
         tuple = (tensor_one, tensor_two)
         tuple_serialized = serialize(tuple, compress=False)
         tuple_serialized_deserialized = deserialize(tuple_serialized, compressed=False)
@@ -99,8 +99,8 @@ class TestSerde(TestCase):
         assert tuple == tuple_serialized_deserialized
 
         # Test with a complex data structure
-        tensor_one = tensor(numpy.random.random((100, 100)))
-        tensor_two = tensor(numpy.random.random((100, 100)))
+        tensor_one = Tensor(numpy.random.random((100, 100)))
+        tensor_two = Tensor(numpy.random.random((100, 100)))
         tuple = (tensor_one, tensor_two)
         tuple_serialized = serialize(tuple, compress=True)
         tuple_serialized_deserialized = deserialize(tuple_serialized, compressed=True)
@@ -183,8 +183,8 @@ class TestSerde(TestCase):
         assert _dict == dict_serialized_deserialized
 
         # Test with a complex data structure
-        tensor_one = tensor(numpy.random.random((100, 100)))
-        tensor_two = tensor(numpy.random.random((100, 100)))
+        tensor_one = Tensor(numpy.random.random((100, 100)))
+        tensor_two = Tensor(numpy.random.random((100, 100)))
         _dict = {0: tensor_one, 1: tensor_two}
         dict_serialized = serialize(_dict, compress=False)
         dict_serialized_deserialized = deserialize(dict_serialized, compressed=False)
@@ -208,8 +208,8 @@ class TestSerde(TestCase):
         assert _dict == dict_serialized_deserialized
 
         # Test with a complex data structure
-        tensor_one = tensor(numpy.random.random((100, 100)))
-        tensor_two = tensor(numpy.random.random((100, 100)))
+        tensor_one = Tensor(numpy.random.random((100, 100)))
+        tensor_two = Tensor(numpy.random.random((100, 100)))
         _dict = {0: tensor_one, 1: tensor_two}
         dict_serialized = serialize(_dict, compress=True)
         dict_serialized_deserialized = deserialize(dict_serialized, compressed=True)
@@ -251,8 +251,8 @@ class TestSerde(TestCase):
         assert _list == list_serialized_deserialized
 
         # Test with a complex data structure
-        tensor_one = tensor(numpy.random.random((100, 100)))
-        tensor_two = tensor(numpy.random.random((100, 100)))
+        tensor_one = Tensor(numpy.random.random((100, 100)))
+        tensor_two = Tensor(numpy.random.random((100, 100)))
         _list = (tensor_one, tensor_two)
         list_serialized = serialize(_list, compress=False)
         list_serialized_deserialized = deserialize(list_serialized, compressed=False)
@@ -276,8 +276,8 @@ class TestSerde(TestCase):
         assert _list == list_serialized_deserialized
 
         # Test with a complex data structure
-        tensor_one = tensor(numpy.random.random((100, 100)))
-        tensor_two = tensor(numpy.random.random((100, 100)))
+        tensor_one = Tensor(numpy.random.random((100, 100)))
+        tensor_two = Tensor(numpy.random.random((100, 100)))
         _list = (tensor_one, tensor_two)
         list_serialized = serialize(_list, compress=True)
         list_serialized_deserialized = deserialize(list_serialized, compressed=True)
@@ -301,8 +301,8 @@ class TestSerde(TestCase):
         assert _set == set_serialized_deserialized
 
         # Test with a complex data structure
-        tensor_one = tensor(numpy.random.random((100, 100)))
-        tensor_two = tensor(numpy.random.random((100, 100)))
+        tensor_one = Tensor(numpy.random.random((100, 100)))
+        tensor_two = Tensor(numpy.random.random((100, 100)))
         _set = (tensor_one, tensor_two)
         set_serialized = serialize(_set, compress=False)
         set_serialized_deserialized = deserialize(set_serialized, compressed=False)
@@ -326,8 +326,8 @@ class TestSerde(TestCase):
         assert _set == set_serialized_deserialized
 
         # Test with a complex data structure
-        tensor_one = tensor(numpy.random.random((100, 100)))
-        tensor_two = tensor(numpy.random.random((100, 100)))
+        tensor_one = Tensor(numpy.random.random((100, 100)))
+        tensor_two = Tensor(numpy.random.random((100, 100)))
         _set = (tensor_one, tensor_two)
         set_serialized = serialize(_set, compress=True)
         set_serialized_deserialized = deserialize(set_serialized, compressed=True)
diff --git a/test/torch/__init__.py b/test/torch/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/torch/tensors/__init__.py b/test/torch/tensors/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/torch/tensors/test_tensor.py b/test/torch/tensors/test_tensor.py
new file mode 100644
index 0000000000..74acc78c8a
--- /dev/null
+++ b/test/torch/tensors/test_tensor.py
@@ -0,0 +1,48 @@
+from unittest import TestCase
+import torch
+
+
+import syft
+from syft.workers import VirtualWorker
+from syft.frameworks.torch.tensors import TorchTensor, PointerTensor
+
+
+def setUpModule():
+
+    global me
+    global bob
+    global alice
+    global james
+    global hook
+
+    hook = syft.TorchHook(torch, verbose=True)
+
+    me = hook.local_worker
+    me.is_client_worker = False
+
+    bob = syft.VirtualWorker(id="bob", hook=hook, is_client_worker=False)
+    alice = syft.VirtualWorker(id="alice", hook=hook, is_client_worker=False)
+    james = syft.VirtualWorker(id="james", hook=hook, is_client_worker=False)
+
+    bob.add_workers([alice, james])
+    alice.add_workers([bob, james])
+    james.add_workers([bob, alice])
+
+
+class TestNative(TestCase):
+    def test_init(self):
+        tensor_extension = TorchTensor()
+        assert tensor_extension.id is None
+        assert tensor_extension.owner is None
+
+
+class TestPointer(TestCase):
+    def test_init(self):
+        alice = VirtualWorker(id="alice")
+        pointer = PointerTensor(id=1000, location=alice, owner=alice)
+        pointer.__str__()
+
+    def test_create_pointer(self):
+        x = torch.Tensor([1, 2])
+        x.create_pointer()
+        x.create_pointer(location=james)
diff --git a/test/torch/test_hook.py b/test/torch/test_hook.py
new file mode 100644
index 0000000000..dda2751c4a
--- /dev/null
+++ b/test/torch/test_hook.py
@@ -0,0 +1,25 @@
+from unittest import TestCase
+import torch
+import syft
+
+from syft.frameworks.torch import TorchHook
+from syft.workers import VirtualWorker
+
+
+class TestHook(TestCase):
+    def test___init__(self):
+        hook = TorchHook(torch)
+        assert torch.torch_hooked
+        assert hook.torch.__version__ == torch.__version__
+
+        alice = VirtualWorker(id="alice")
+        hook = TorchHook(torch, local_worker=alice)
+
+    def test_torch_attributes(self):
+        with self.assertRaises(RuntimeError):
+            syft.torch._command_guard("false_command", "torch_modules")
+
+        assert syft.torch._is_command_valid_guard("torch.add", "torch_modules")
+        assert not syft.torch._is_command_valid_guard("false_command", "torch_modules")
+
+        syft.torch._command_guard("torch.add", "torch_modules", get_native=False)
diff --git a/test/torch/workers/__init__.py b/test/torch/workers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/torch/workers/test_worker.py b/test/torch/workers/test_worker.py
new file mode 100644
index 0000000000..35f54532fc
--- /dev/null
+++ b/test/torch/workers/test_worker.py
@@ -0,0 +1,18 @@
+from unittest import TestCase
+
+from syft.workers import VirtualWorker
+
+
+class TestHook(TestCase):
+    def test___init__(self):
+        alice = VirtualWorker(id="alice")
+        bob = VirtualWorker(id="bob", known_workers={alice.id: alice})
+        charlie = VirtualWorker(id="charlie")
+
+        assert bob.get_worker("alice").id == alice.id
+        assert bob.get_worker(alice).id == alice.id
+        assert bob.get_worker(charlie).id == charlie.id
+
+        bob.get_worker("the_unknown_worker")
+
+        bob.add_worker(alice)