This repo is a coordinated effort to build a Rust alternative for PySyft, with native host language bindings.
This README.md is formatted with remark: mrmlnc.vscode-remark
The folder structure looks like this:
├── platforms
│ └── python <- Syft Python Host Code
├── protos <- Shared Proto definitions
└── syft <- Syft Core Rust Code
├── src
└── target
- python 3.7+ - https://www.python.org/
- rustup - https://rustup.rs/
- bloomrpc - https://github.com/uw-labs/bloomrpc
- protoc - https://github.com/protocolbuffers/protobuf
- vscode - https://github.com/microsoft/vscode
Python
$ brew install python
rustup
$ brew install rustup
$ rustup-init
bloomrpc
$ brew cask install bloomrpc
protoc
$ brew install protobuf
We are currently using nightly due to some rust dependencies.
$ rustup toolchain install nightly
$ rustup default nightly
Rust comes with an opinionated formatter and linter so we will mandate that these are used.
Install Rust Format:
$ rustup component add rustfmt
Install Rust Language Server:
$ rustup component add rls
Install Rust Linting:
$ rustup component add clippy
While VSCode is not required it is highly recommended.
Install Rust VSCode Extension: https://marketplace.visualstudio.com/items?itemName=rust-lang.rust
$ code --install-extension rust-lang.rust
Install Even Better TOML Extension: https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml
$ code --install-extension tamasfe.even-better-toml
Add to settings:
{
"evenBetterToml.formatter.reorderKeys": false,
"evenBetterToml.formatter.alignEntries": true
}
Make sure you have python3.7+
We use a virtual environment to isolate the syft core python wheel development and build process.
We include support for Pipenv, Conda and pip with virtualenv.
To keep code clean and bug free we mandate all code inside syft core python, uses an agreed upon set of linting and formatting standards.
- black - https://github.com/psf/black
- isort - https://github.com/timothycrosley/isort
- mypy - http://mypy-lang.org/
$ pip install black isort mypy
Add these to your settings.json, making sure to update the paths as necessary to your platform.
{
"python.linting.enabled": true,
"python.linting.mypyEnabled": true,
"python.formatting.provider": "black",
"python.linting.mypyPath": "/usr/local/bin/mypy",
"python.formatting.blackPath": "/usr/local/bin/black"
}
Upgrade pip:
$ pip install --upgrade pip
Install pipenv:
$ pip install pipenv
Enter virtualenv:
$ cd platforms/python
$ pipenv shell
Install packages:
$ pipenv install --dev --skip-lock
Create your conda environment, navigate to the /platforms/python directory:
$ conda create --name syft --file requirements.txt
Create a virtualenv in the /platforms/python folder and install the packages inside requirements.txt
Make sure to enable your virtualenv from Pipenv, conda or other virtualenv system when doing any commands relating to maturin or python.
If you are using pipenv:
$ cd platforms/python
$ pipenv shell
If you are using conda:
$ cd platforms/python
$ conda activate syft
You can compile and install the python library from the virtualenv in one command:
$ maturin develop
We are using pytest which is listed in the Pipfile / requirements.txt.
Run tests from the platforms/python directory inside your virtualenv:
$ cd platforms/python
$ maturin develop; pytest
The rust crate pyo3 allows us to mix compiled Rust code as a CPython module and vanilla
python code in the same wheel. The vanilla python code must go into a folder named the
same as the module and must contain at least a single __init__.py
file in that folder.
That is why you will see this inside /platforms/python:
├── src <--- Rust Code
│ └── ffi
├── syft <--- Python Code
│ ├── message
│ ├── node
│ └── protos
├── target
│ ├── debug
│ ├── rls
│ └── wheels
└── tests <--- Python Tests
To allow for a nice consistent import interface there is some code inside the vanilla python source which acts to convert the awkward issues with CPython module names and the way the Rust pyo3 submodules are defined.
Without this fix, importing a Rust module defined as syft.message might look like:
from syft.syft import message
Importing further nested items results in an error:
from syft.syft.message import run_class_method_message
>>> ModuleNotFoundError: No module named 'syft.syft.message';
However by re-exporting the modules using a matching directory structure and
__init__.py
files we can provide a clean interface:
# import from rust in platforms/python/src
from syft.message import run_class_method_message
# import from vanilla python in platforms/python/syft
During this step:
- The syft core rust library is built
- The synthesized python interface to the protos are compiled with protoc
- The python platform ffi code in platforms/python/src is compiled for your system arch
- The vanilla python code inside platforms/python/syft is added
- Code inside the
__init__.py
files allows for a consistent module import syntax - A wheel is created with both these mixed source files
Build wheel and install wheel:
$ maturin build -i python
$ pip install `find -L ./target/wheels -name "*.whl"`
$ pipenv shell
$ maturin develop
$ python -i examples/worker.py
You should see:
Starting node on [::1]:50051
Tokio thread started
Capability registered: hello
Capability registered: sum
Capability registered: sum_np
>>>
$ pipenv shell
$ maturin develop
$ python -i examples/client.py
You should see:
Tokio thread started
Capabilities returned: ["sum", "hello", "sum_np"]
Node at: http://[::1]:50051 has capabilities: ['sum', 'hello', 'sum_np']
Hello: Client 1
6
6
>>>
Try issuing a command like:
>>> execute_capability(target_addr, "sum", [i for i in range(0, 100)])
4950
>>>
You can run the Hello World demo with jupyter by opening the two notebooks.
$ pipenv shell
$ maturin develop
$ jupyter notebook
Make sure to initialize the worker and register capability functions before sending requests from the client.
If you want to test this between computers on different networks over the internet try ngrok.
Install ngrok:
$ brew cask install ngrok
Start your worker and pick a port then run ngrok like so:
$ ngrok tcp 50051
You should see output like:
Forwarding tcp://0.tcp.ngrok.io:12345 -> localhost:50051
In your client use the address like so:
target_addr = "tcp://0.tcp.ngrok.io:12345"
execute_capability(target_addr, "sum", [i for i in range(0, 10)])
45
>>>