SPU Basics#

The following codes are demos only. It’s NOT for production due to system security concerns, please DO NOT use it directly in production.

SPU devices are responsible for performing MPC computation in SecretFlow.

This tutorial would help you:

  • be familiar with SPU device and SPU Object

  • learn how to transfer a Python Object / PYU Object from/to SPU Object.

  • run MPC computation with SPU device.

Create a SPU Device#

Create SecretFlow Parties#

Parties are basic nodes in SecretFlow nodes. We are going to create four parties - alice, bob, carol and dave.

Based on four parties, we will set up three devices:

  • a PYU device based on alice

  • a PYU device based on dave

  • a SPU device based on alice, bob and carol

spu_basics_devices.png

[1]:
import secretflow as sf

# In case you have a running secretflow runtime already.
sf.shutdown()

sf.init(['alice', 'bob', 'carol', 'dave'], num_cpus=8, log_to_driver=True)

2022-08-30 18:34:37.024687: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst

Create a 3PC ABY3 SPU device#

After that, let’s create a SPU device with ABY3 protocol.

sf.utils.testing.cluster_def is a helper method to create a config by finding unused ports.

[2]:
aby3_config = sf.utils.testing.cluster_def(parties=['alice', 'bob', 'carol'])

aby3_config

[2]:
{'nodes': [{'party': 'alice', 'id': 'local:0', 'address': '127.0.0.1:23669'},
  {'party': 'bob', 'id': 'local:1', 'address': '127.0.0.1:54219'},
  {'party': 'carol', 'id': 'local:2', 'address': '127.0.0.1:27519'}],
 'runtime_config': {'protocol': 3, 'field': 3}}

Then let’s use aby3_config to create a SPU device and check its cluster_def.

[3]:
spu_device = sf.SPU(aby3_config)

spu_device.cluster_def

[3]:
{'nodes': [{'party': 'alice', 'id': 'local:0', 'address': '127.0.0.1:23669'},
  {'party': 'bob', 'id': 'local:1', 'address': '127.0.0.1:54219'},
  {'party': 'carol', 'id': 'local:2', 'address': '127.0.0.1:27519'}],
 'runtime_config': {'protocol': 3, 'field': 3}}

Lastly, let’s create two PYU devices.

[4]:
alice, dave = sf.PYU('alice'), sf.PYU('dave')

Pass Values to SPU device#

Before talking about computation with SPU device, let’s understand how to pass a Python object / PYUObject to SPU device.

Pass a Python Object from Host to SPU#

Let’s pass a dict from Host to SPU device.

[5]:
bank_account = [{'id': 12345, 'deposit': 1000.25}, {'id': 12345, 'deposit': 100000.25}]

bank_account_spu = sf.to(spu_device, bank_account)

WARNING:absl:No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)

bank_account_spu is a SPUObject. A SPUObject represents a Python Object which could be consumed by a SPU device.

[6]:
type(bank_account_spu)

[6]:
secretflow.device.device.spu.SPUObject

Each SPUObject has two fields:

  • meta

  • shares

At this moment, since we are creating a SPU object from Host. We could check these two fields freely.

Let’s check meta first.

[7]:
bank_account_spu.meta

[7]:
[{'deposit': SPUValueMeta(shape=(), dtype=dtype('float32'), vtype=1),
  'id': SPUValueMeta(shape=(), dtype=dtype('int32'), vtype=1)},
 {'deposit': SPUValueMeta(shape=(), dtype=dtype('float32'), vtype=1),
  'id': SPUValueMeta(shape=(), dtype=dtype('int32'), vtype=1)}]

I guess you could find meta preserves the structure of origin data and replaces the digits/arrays with SPUValueMeta.

Then let’s check shares of bank_account_spu. Since we are passing data to a 3PC SPU device. We would have three pieces of shares, and we are going to check the first piece.

[8]:
assert len(bank_account_spu.shares) == 3

bank_account_spu.shares[0]

[8]:
[{'deposit': data_type: DT_FXP
  visibility: VIS_SECRET
  storage_type: "aby3.AShr<FM128>"
  content: "\361\177j\273\313\270\3253Y\034\370WM&K\r\221\360y\223\371m\272L\037<\233W\2271\221c",
  'id': data_type: DT_I32
  visibility: VIS_SECRET
  storage_type: "aby3.AShr<FM128>"
  content: "\227\357\204\032\363\201\307\234 f\272\361\216\305\265\373\332\301\314!\366\360\241x\245T\231\267\320d]\202"},
 {'deposit': data_type: DT_FXP
  visibility: VIS_SECRET
  storage_type: "aby3.AShr<FM128>"
  content: "j\240s\364=\365\243j\315:\214\036:\233xYrK\304\201\245G\350ER\360\007\274\2365M\376",
  'id': data_type: DT_I32
  visibility: VIS_SECRET
  storage_type: "aby3.AShr<FM128>"
  content: ";!\317\t\302\227\270n\324}\003\327\365\2747\215\362BC8U:I\232B<\226\213\235\241$x"}]

You should find a piece of shares of SPU Object is very similar to meta and origin data. It still preserves the structure of origin data while digits are replaced by a struct consisting of:

  • data_type, indicates whether the value is integer or fixed points.

  • visibility, indicates whether the value is a secret or a public content.

  • storage_type, indicates attributeds of value, e.g. MPC protocol(ABY3 in our case), field size(128 bits in our case), etc

  • content, encoded secret (try to guess the origin data if you would like to).

Pass a PYU Object from PYU to SPU#

Then let’s try another path. First, we create a PYU object with a PYU device.

[9]:
def debit_amount():
    return 10


debit_amount_pyu = alice(debit_amount)()
debit_amount_pyu

[9]:
<secretflow.device.device.pyu.PYUObject at 0x7f32f0bc4370>

Then let’s pass debit_amount_pyu from PYU to SPU. We will get a SPU object as result.

[10]:
debit_amount_spu = debit_amount_pyu.to(spu_device)

debit_amount_spu

[10]:
<secretflow.device.device.spu.SPUObject at 0x7f32f04322b0>

Let’s check meta of debit_amount_spu.

[11]:
debit_amount_spu.meta

[11]:
ObjectRef(4ee449587774c1f0ffffffffffffffffffffffff0100000001000000)

Oh no, it’s a Ray ObjectRef located at alice part. So how about shares of debit_amount_spu?

[12]:
debit_amount_spu.shares

(SPURuntime pid=922821) I0830 18:34:44.759606 922821 external/com_github_brpc_brpc/src/brpc/server.cpp:1066] Server[yasl::link::internal::ReceiverServiceImpl] is serving on port=23669.
(SPURuntime pid=922821) I0830 18:34:44.759674 922821 external/com_github_brpc_brpc/src/brpc/server.cpp:1069] Check out http://k69b13338.eu95sqa:23669 in web browser.
(SPURuntime pid=922814) I0830 18:34:44.692403 922814 external/com_github_brpc_brpc/src/brpc/server.cpp:1066] Server[yasl::link::internal::ReceiverServiceImpl] is serving on port=27519.
(SPURuntime pid=922814) I0830 18:34:44.692477 922814 external/com_github_brpc_brpc/src/brpc/server.cpp:1069] Check out http://k69b13338.eu95sqa:27519 in web browser.
(SPURuntime pid=922820) I0830 18:34:44.748833 922820 external/com_github_brpc_brpc/src/brpc/server.cpp:1066] Server[yasl::link::internal::ReceiverServiceImpl] is serving on port=54219.
(SPURuntime pid=922820) I0830 18:34:44.748898 922820 external/com_github_brpc_brpc/src/brpc/server.cpp:1069] Check out http://k69b13338.eu95sqa:54219 in web browser.
[12]:
[ObjectRef(4ee449587774c1f0ffffffffffffffffffffffff0100000002000000),
 ObjectRef(4ee449587774c1f0ffffffffffffffffffffffff0100000003000000),
 ObjectRef(4ee449587774c1f0ffffffffffffffffffffffff0100000004000000)]

So you get a list of ObjectRef! Since it’s located at alice part, we couldn’t check the value at host.

But if you are really curious, we could use sf.reveal to check the origin value. Be careful to use sf.reveal in production!

[13]:
sf.reveal(debit_amount_spu)

(_run pid=922818) 2022-08-30 18:34:45.224087: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
[13]:
array(10, dtype=int32)

This is the first part of Data Flow with SPU device, at this moment, you should be aware of the following facts.

  • A Python Object/PYU Object could be transferred to a SPU Object.

  • A SPU Object consists of meta and shares.

  • You could only check meta and shares when SPU Object is located at host. Otherwise, you have to call sf.reveal

  • Just converting to SPU Object won’t trigger data flow from host / PYU to SPU. e.g. When you transferred a PYU object to a SPU object. All the field of SPU objects including meta and shares are still located at owners(Host / PYU device). The shares would only be sent to parties of SPU device when computation do happenes. In short, data flow is lazy.

Computation with SPU Device#

Since we have two SPU objects - bank_account_spu and debit_amount_spu as inputs. Let’s try to do some computation with SPU device.

[14]:
def deduce_from_account(bank_account, amount):
    new_bank_account = []

    for account in bank_account:
        account['deposit'] = account['deposit'] - amount
        new_bank_account.append(account)

    return new_bank_account


new_bank_account_spu = spu_device(deduce_from_account)(
    bank_account_spu, debit_amount_spu
)

new_bank_account_spu

(_run pid=922818) 2022-08-30 18:34:46,912,912 WARNING [xla_bridge.py:backends:265] No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
[14]:
<secretflow.device.device.spu.SPUObject at 0x7f32f0522af0>

new_bank_account_spu is also a SPUObject. But it’s a bit different from bank_account_spu and debit_amount_spu!

  • bank_account_spu is located at host, so you could check value from host directly.

  • debit_amount_spu is located at alice, so only alice could check value.

  • new_bank_account_spu is located at spu, each party of spu have a piece of shares. And you couldn’t check the value directly without sf.reveal.

Well, but what happened behind computation of SPU device?

The Python function (deduce_from_account in our case) and metas of all inputs (bank_account_spu and debit_amount_spu) would be sent to one party of SPU device. Then SPU compiler would be used to compile them to SPU Executable.

spu_basics_compiler.png

Each party of SPU device would get:

  • one copy of SPU Executable

  • one piece of each SPU Object share

spu_basics_distribute.png

Then each party of SPU device would execute SPU Executation.

In the end, each party of SPU device would own a piece of output SPU Objects and a copy of meta.

Then SecretFlow framework would use them to assembly SPU Objects.

Get Value from SPU Device#

But in the end, we need to get value from spu, we couldn’t always keep SPUObject as secret!

Most common way of handling SPUObject is pass the secret to one party. This party is not necessarily one of parties consisting of SPU device.

[15]:
new_bank_account_pyu = new_bank_account_spu.to(dave)

new_bank_account_pyu

[15]:
<secretflow.device.device.pyu.PYUObject at 0x7f32f043f160>

We just pass new_bank_account_spu to pyu, then it becomes a PYUObject! And it’s owned by dave. Let’s check the value of new_bank_account_pyu.

[16]:
sf.reveal(new_bank_account_pyu)

[16]:
[{'deposit': array(990.25, dtype=float32), 'id': array(12345, dtype=int32)},
 {'deposit': array(99990.25, dtype=float32), 'id': array(12345, dtype=int32)}]

We could also pass SPUObject to host directly. The magic is sf.reveal. And again, be careful in production!

[17]:
sf.reveal(new_bank_account_spu)

[17]:
[{'deposit': array(990.25, dtype=float32), 'id': array(12345, dtype=int32)},
 {'deposit': array(99990.25, dtype=float32), 'id': array(12345, dtype=int32)}]

Advanced Topic: Use Different MPC Protocol#

At this moment, SPU device supports multiple MPC protocol besides ABY3. It’s easy to use different MPC protocol - just set the proper field in cluster def.

For instance, if someone would like to use 2PC protocol - Cheetah, You should prepare another cluster def:

[18]:
import spu

import secretflow as sf

# In case you have a running secretflow runtime already.
sf.shutdown()

sf.init(['alice', 'bob', 'carol', 'dave'], num_cpus=8, log_to_driver=True)

cheetah_config = sf.utils.testing.cluster_def(
    parties=['alice', 'bob'],
    runtime_config={
        'protocol': spu.spu_pb2.CHEETAH,
        'field': spu.spu_pb2.FM64,
    },
)

Then you could create a SPU device with cheetah_config.

[19]:
spu_device2 = sf.SPU(cheetah_config)

Let’s check the cluster_def of spu_device2.

[20]:
spu_device2.cluster_def

[20]:
{'nodes': [{'party': 'alice', 'id': 'local:0', 'address': '127.0.0.1:56917'},
  {'party': 'bob', 'id': 'local:1', 'address': '127.0.0.1:27783'}],
 'runtime_config': {'protocol': 4, 'field': 2}}

We could use spu_device2 to check famous Yao’s Millionaires’ problem.

[21]:
def get_carol_assets():
    return 1000000


def get_dave_assets():
    return 1000002


carol, dave = sf.PYU('carol'), sf.PYU('dave')

carol_assets = carol(get_carol_assets)()
dave_assets = dave(get_dave_assets)()

We use spu_device2 to check if carol is richer.

[22]:
def get_winner(carol, dave):
    return carol > dave


winner = spu_device2(get_winner)(carol_assets, dave_assets)

sf.reveal(winner)

(pid=924219) 2022-08-30 18:34:54.138914: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
(pid=924216) 2022-08-30 18:34:54.138916: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
(_run pid=924214) 2022-08-30 18:34:54.837911: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
(_run pid=924220) 2022-08-30 18:34:54.830053: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
(pid=924217) 2022-08-30 18:34:54.790930: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
(pid=924213) 2022-08-30 18:34:54.790928: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
(pid=924218) 2022-08-30 18:34:54.790927: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
(pid=924215) 2022-08-30 18:34:54.790941: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /opt/rh/rh-ruby25/root/usr/local/lib64:/opt/rh/rh-ruby25/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:/opt/rh/devtoolset-11/root/usr/lib64/dyninst:/opt/rh/devtoolset-11/root/usr/lib/dyninst
(SPURuntime pid=924219) I0830 18:34:55.906383 924219 external/com_github_brpc_brpc/src/brpc/server.cpp:1066] Server[yasl::link::internal::ReceiverServiceImpl] is serving on port=56917.
(SPURuntime pid=924219) I0830 18:34:55.906445 924219 external/com_github_brpc_brpc/src/brpc/server.cpp:1069] Check out http://k69b13338.eu95sqa:56917 in web browser.
(SPURuntime pid=924216) I0830 18:34:55.919610 924216 external/com_github_brpc_brpc/src/brpc/server.cpp:1066] Server[yasl::link::internal::ReceiverServiceImpl] is serving on port=27783.
(SPURuntime pid=924216) I0830 18:34:55.919660 924216 external/com_github_brpc_brpc/src/brpc/server.cpp:1069] Check out http://k69b13338.eu95sqa:27783 in web browser.
(_run pid=924214) 2022-08-30 18:34:56,578,578 WARNING [xla_bridge.py:backends:265] No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
(_run pid=924220) 2022-08-30 18:34:56,654,654 WARNING [xla_bridge.py:backends:265] No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
[22]:
array(False)

Advanced Topic: Multiple Returns from SPU Computation#

In most cases, we have multiple returns from the function executed by SPU device.

For instance,

[23]:
def get_multiple_outputs(x, y):
    return x + y, x - y

There are multiple options to handle this.

Option 1: Treat All Returns as Single#

This is the default behavior of SPU. Let’s see.

[24]:
single_output = spu_device2(get_multiple_outputs)(carol_assets, dave_assets)

single_output

[24]:
<secretflow.device.device.spu.SPUObject at 0x7f32e57c51f0>

We could see we only get a single SPUObject. Let’s reveal it.

[25]:
sf.reveal(single_output)

[25]:
(array(2000002, dtype=int32), array(-2, dtype=int32))

So single_output itself actually represents a tuple.

Option 2: Decide Return Nums on the Fly#

We can also instruct SPU to decide return numbers for us.

[26]:
from secretflow.device.device.spu import SPUCompilerNumReturnsPolicy

multiple_outputs = spu_device2(
    get_multiple_outputs, num_returns_policy=SPUCompilerNumReturnsPolicy.FROM_COMPILER
)(carol_assets, dave_assets)

multiple_outputs

[26]:
(<secretflow.device.device.spu.SPUObject at 0x7f32e57c0190>,
 <secretflow.device.device.spu.SPUObject at 0x7f32e57c0490>)

let’s check two outputs respectively.

[27]:
print(sf.reveal(multiple_outputs[0]))
print(sf.reveal(multiple_outputs[1]))

2000002
-2

Option 3: Decide Return Nums Manually#

If possible, you could also set return nums manually.

[28]:
user_multiple_outputs = spu_device2(
    get_multiple_outputs,
    num_returns_policy=SPUCompilerNumReturnsPolicy.FROM_USER,
    user_specified_num_returns=2,
)(carol_assets, dave_assets)

user_multiple_outputs

[28]:
[<secretflow.device.device.spu.SPUObject at 0x7f32e57c61f0>,
 <secretflow.device.device.spu.SPUObject at 0x7f32e57c6280>]

let’s also check two outputs respectively.

[29]:
print(sf.reveal(multiple_outputs[0]))
print(sf.reveal(multiple_outputs[1]))
2000002
-2

Let’s summarize what we have:

  • Be default, SPU treats all the returns as a single return

  • Since SPU compiler generates the SPU executable, it can figure out return nums. However, the options results some latency since we have to make compilation blocked.

  • If you want to avoid latency, we can provide return nums manually. But you have to make sure you provide the right nums, otherwise, the program would complain!

What’s Next?#

After learning basics of SPU, you may check some advanced tutorials with SPU: