SPU基础#

以下代码仅作为示例,请勿在生产环境直接使用。

SPU设备在SecretFlow中负责执行MPC计算。

本教程将帮助你:

  • 熟悉SPU设备和SPU Object

  • 学习如何在Python Object/PYU Object和SPU Object之间相互转化。

  • 利用SPU设备执行MPC计算

创建一个SPU设备#

创建SecretFlow Parties#

SecretFlow Parties是在SecretFlow的基本节点,我们将会创建4个party - alice, bob, caroldave

基于这四个party,我们将会建立3个设备。

  • 一个基于 alice 的PYU设备

  • 一个基于 dave 的PYU设备

  • 一个基于 alice , bobcarol 的SPU设备

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'], address='local')

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

创建一个基于三方ABY3协议的SPU设备#

之后,我们创建一个基于 ABY3 协议的SPU设备。

sf.utils.testing.cluster_def 是一个helper通过寻找未占用的端口来创建一个设置。

[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}}

随后我们用 aby3_config 来创建一个SPU设备并检查其 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}}

最后,我们创建两个PYU设备。

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

向SPU设备传值#

在讨论利用SPU设备计算之前,我们需要理解如何将一个 Python object / PYUObject 传给一个SPU设备。

将一个Python Object从Host传到SPU#

让我们将一个字典从HOST传给SPU设备。

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

bank_account_spu = sf.to(alice, bank_account).to(spu_device)

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 是一个 SPUObject。一个 SPUObject 可以代表一个可以被一个SPU设备接受的 Python Object。

[6]:
type(bank_account_spu)

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

每一个SPUObject包含两个成员:

  • meta

  • shares

此时,由于我们是在 Host 创建一个SPU object。我们可以自由地查看这两个成员。

我们首先查看meta。

[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)}]

我猜你已经发现meta保留了原始数据的结构,只是将数字和数列替换为 SPUValueMeta

随后我们检查bank_account_spu的*shares* 。 由于我们将数据传递至一个3PC的SPU设备。我们可以有三个分片,我们将会检查第一个分片。

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

bank_account_spu.shares_name[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"}]

你应该发现一个SPU Object的分片非常类似于meta和原始数据。它保留了原始数据的结构但是被一个机构体替换:

  • data_type, 代表了值是整数还是定点数。

  • visibility,代表了值是密文还是明文。

  • storage_type,代表了值的属性,比如MPC协议(这里是ABY3),field size(我们这里是128位),等等。

  • content,编码之后的秘密(不妨尝试猜一下原文)。

将一个PYU Object从PYU传给SPU#

然后,我们尝试另一条路。首先,我们用一个PYU设备创建一个PYU object。

[9]:
def debit_amount():
    return 10


debit_amount_pyu = alice(debit_amount)()
debit_amount_pyu

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

然后我们将debit_amount_pyu从PYU传到SPU,我们将会得到一个SPU object作为结果。

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

debit_amount_spu

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

我们检查一下debit_amount_spu的meta。

[11]:
debit_amount_spu.meta

[11]:
ObjectRef(4ee449587774c1f0ffffffffffffffffffffffff0100000001000000)

不,它是一个在alice一边的Ray ObjectRef。debit_amount_spu的shares是怎样的呢?

[12]:
debit_amount_spu.shares_name

(SPURuntime pid=922821) I0830 18:34:44.759606 922821 external/com_github_brpc_brpc/src/brpc/server.cpp:1066] Server[yacl::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[yacl::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[yacl::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)]

你会得到一个ObjectRef列表。因为它在alice这一侧,我们无法在host检查它的值。

如果你非常好奇,我们可以用 sf.reveal 检查原始值。在生产环境中,请谨慎使用 sf.reveal

[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)

这是使用 SPU 设备的数据流的第一部分,此时,您应该了解以下事实。

  • Python Object/PYU Object可以转化到 SPU Object。

  • 一个SPU Object 包含了meta 和 shares。

  • 你只能在SPU Object位于host的时候检查 meta 和 shares。否则,你只能使用 sf.reveal

  • 仅仅转化为SPU Object不会触发从host / PYU至SPU的数据流动,比如当你将一个PYU Object转化为SPU Object时。SPU object所有内容包括meta和shares仍然位于所有方(Host / PYU设备)。shares只会在计算发生的时候将会被发送到SPU设备的各方。简单的来说,数据流动是lazy的。

使用 SPU 设备进行计算#

因为我们有两个 SPU Object - bank_account_spudebit_amount_spu 作为输入。 让我们尝试使用 SPU 设备进行一些计算。

[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>

newbankaccountspu 也是一个 SPU Object 。 但它与 bank_account_spudebit_amount_spu 有点不同!

  • bank_account_spu 位于host,因此您可以直接从host检查值。

  • debit_amount_spu 位于 alice,因此只有 alice 可以检查值。

  • newbankaccount_spu 位于spu,spu的每一方都有一份shares。 如果没有 sf.reveal ,您将无法直接检查该值。

好吧,但是 SPU 设备的计算背后发生了什么?

Python 函数(在我们的例子中是 deduce_from_account)和所有输入的元数据(bank_account_spudebit_amount_spu)将被发送到 SPU 设备。然后使用 SPU 编译器将它们编译为 SPU Executable

spu_basics_compiler.png

SPU 设备的每一方将获得:

  • 一份 SPU Executable

  • 每个 SPU object一份share

spu_basics_distribute.png

然后 SPU 设备的每一方将执行 SPU Executation。

最后,SPU 设备的每一方都将拥有一个输出 SPU Object的一份share和一个meta。

然后 SecretFlow 框架将使用它们来组装 SPU Object。

从 SPU 设备中获取值#

但最后,我们需要从 spu 中获取值,我们不能总是将 SPUObject 当作密文!

处理 SPUObject 的最常见方法是将秘密传递给一方。 该方不一定是由 SPU 设备组成的各方之一。

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

new_bank_account_pyu

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

我们只是将 new_bankaccountspu 传递给 pyu ,然后它就变成了 PYUObject ! 它归dave所有。 让我们检查 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)}]

我们也可以直接将 SPUObject 传递给host。 利用神奇的*sf.reveal*。 再次提醒在生产环境中要小心使用*sf.reveal*!

[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)}]

进阶主题:使用不同的 MPC 协议#

目前SPU设备支持ABY3之外的多种MPC协议。 使用不同的 MPC 协议很容易 - 只需在 cluster def 中设置适当的字段。

例如,如果有人想使用 2PC 协议 - Cheetah,你应该准备另一个集群 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'], address='local')

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

然后你可以用 cheetah_config 创建一个 SPU 设备。

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

让我们检查一下 spu_device2 的 cluster_def

[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}}

我们可以使用 spu_device2 来检查著名的姚氏百万富翁问题。

[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)()

我们使用 spu_device2 来检查 carol 是否更富有。

[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[yacl::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[yacl::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)

进阶主题:从SPU 计算得到多个返回值#

在大多数情况下,我们从 SPU 设备执行的函数中获得多个返回值。

例如,

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

有多种选择可以处理这个问题。

选项 1:将所有返回值视为单一返回值#

这是 SPU 的默认行为。 让我们来看看。

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

single_output

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

我们可以看到我们只得到一个*SPUObject*。 让我们揭示它。

[25]:
sf.reveal(single_output)

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

所以 single_output 本身实际上代表一个元组。

选项 2:即时决定返回值数量#

我们还可以指示 SPU 为我们决定返回值数量。

[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>)

让我们分别检查两个输出。

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

2000002
-2

选项 3:手动确定返回值数量#

如果可能,您还可以手动设置返回值数量。

[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>]

让我们分别检查两个输出。

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

让我们总结一下我们所拥有的结论:

  • 默认情况下,SPU 将所有返回值视为单个返回值

  • 由于 SPU 编译器生成 SPU 可执行文件,它可以计算出返回值数量。 但是,这个选项会导致一些延迟,因为我们必须使编译工作阻塞。

  • 如果您想避免延迟,我们可以手动提供返回值数量。 但是你必须确保你提供了正确的数字,否则程序会报错!

下一步是什么#

在学习了 SPU 的基础知识后,您可以查看一些 SPU 高级教程: