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, carol 和 dave。
基于这四个party,我们将会建立3个设备。
一个基于 alice 的PYU设备
一个基于 dave 的PYU设备
一个基于 alice , bob 和 carol 的SPU设备
[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_spu 和 debit_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_spu 和 debit_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_spu 和 debit_amount_spu)将被发送到 SPU 设备。然后使用 SPU 编译器将它们编译为 SPU Executable。
SPU 设备的每一方将获得:
一份 SPU Executable
每个 SPU object一份share
然后 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 高级教程: