隐语的编程思想#
单控制器和多控制器模式#
隐语旨在提供隐私保护机器学习和数据分析,面临的场景天然是跨机构分布式的。面对跨机构分布式场景,如何提供高效简洁的编程模式是隐语面临的挑战之一。
目前常见的两种分布式编程思路可以主要归为两大类。
第一类我们称为多控制器模式。开发者需要为每个参与方分别编写代码,参与方之间通常通过MPI语义(比如 all_gather, all_reduce 等)互相通信。每个参与方执行各自的代码来完成合作,比如SPU背后就是这种模式。顺便说一下,多控制器模式也可以把多方代码写在一个代码文件中,前提是多方代码的逻辑基本是一样的(所谓的同构代码)。
第二类我们称之为单控制器模式。即开发者拥有全局的视角,每个参与方的代码逻辑写在同一份代码中,实际运行时只需要运行这一份代码,通过任务下发机制(比如ray的remote机制)统一管理各参与方的执行逻辑。典型的如ray,ray提供了driver视角配合remote机制,用户可以在全局视角下编写分布式代码。
两种模式各有优劣,并无绝对的好坏之分。下面是目前我们总结到的一些经验。
多控制器
优点
适合代码逻辑同构的场景。
参与方可以审核代码并决定执行与否。
缺点
对代码逻辑异构场景不友好:逻辑分散在不同代码块中,缺乏全局视角,可能导致代码理解难度变高。
单控制器
优点
同时适合代码逻辑同构和异构的场景。
缺点
依赖任务下发机制,任务下发机制可能带来安全漏洞,一方面参与方不好审核代码,另一方面恶意代码执行的防范会比较难做到
考虑到隐语是一个明密文混合编程框架,代码同构异构的需求同时存在,受到ray的启发,隐语最开始是直接采用单控制器模式来编写代码。但是如上述,单控制器模式存在安全隐患,且不好解决。所以问题来了,我们是否可以同时兼顾单控制器和多控制器模式,各取其优点?答案是可以的,这就是下面要讲的隐语编程思想——单控制器编程,多控制器执行。
多控制器执行#
原理#
多控制器执行的核心思路是每个参与方都运行同一份代码,但是仅执行代码中属于自己的部分。
假如我们把代码逻辑看成一个DAG(有向无环图),DAG的每个节点表示一段属于某个参与方的代码逻辑,边表示不同节点之间的数据流转,则多控制器执行原理可以如图所示。
为了达成此目的,DAG的每一个节点都被标记了参与方(party),代码执行时,通过party属性判断节点的归属方,并决定是否需要执行。
情形一
节点属于自己,则执行该节点;若节点的输入依赖来源于其他方,则插入recv节点等待从其他方接收。
情形二
节点不属于自己,则判断节点的输入依赖是否来源于自身;若来源于自身,则插入send节点发送给节点归属方。
多控制器执行原则上要求代码的控制流是确定性的,每一方执行同样代码时,理应生成一模一样的DAG。这是多控制器模式能够正确执行的前提。
为了确保这一点,对编程带来了一些要求,后文会详细提及。
跨机构通信#
目前跨机构通信的实现中,DAG的每一个节点都会被顺序标注序号,send/recv会根据节点的序号来确保数据的收发正确性。这么设计主要是为了简化跨架构通信,同时也引入了一些限制,比如控制流中本身不能多线程(否则跨机构分布式的序号生成会出错)。
实现#
欢迎访问rayfed阅读相关代码。
使用方法#
多控制器执行模式使用方法比较简单,只需要在secretflow.init的时候提供每个机构通信的地址,并指定当前参与方。
示例
对于参与方alice,启动方式如下。
import secretflow as sf
cluster_config = {
'parties': {
'alice': {
'address': '127.0.0.1:10001',
}
'bob': {
'address': '127.0.0.1:10002',
}
},
'self_party': 'alice'
}
sf.init(cluster_config=cluster_config)
对应的,参与方bob启动方式如下。
import secretflow as sf
cluster_config = {
'parties': {
'alice': {
'address': '127.0.0.1:10001',
}
'bob': {
'address': '127.0.0.1:10002',
}
},
'self_party': 'bob'
}
sf.init(cluster_config=cluster_config)
编程注意事项#
多控制器执行模式隐式的要求了每一方执行的DAG是相同的,这带来了一些编程上的要求。
避免控制流中出现不确定性,即控制流无论在何时何地执行,其生成的执行图应该是一致的。
不能在控制流中直接生成随机数,或者依赖某个环境因素(比如某个文件是否存在),我们需要确保控制流代码是确定性的。比如FLModel的控制流中有生成随机数当种子的做法。一种替代的方式是让某一个参与方生成随机数,然后广播给其他参与方。
单测
单测中遇到的情况会比较复杂。目前单测中使用多进程模拟多个参与方,但是由于代码是在同一台机器上执行,所以会遇到更多corner case。
driver生成临时文件,然后删除。多进程模拟时可能出现冲突 -> 解决办法:让PYU生成/删除文件。
driver生成随机ndarray -> 解决办法:让PYU生成然后返回。
SecretFlow内置了一些数据集,下载的时候会写入本地缓存文件。多进程模拟时会出现写冲突 -> 解决办法:引入跨进程的文件锁。
某些单测会构造异常case,然后assertRaiseException。假如alice执行抛异常了,则alice后续的节点不会执行,bob可能捕获不到异常。-> 待解决,rayfed暂时还不具有异常广播能力。 目前临时把这类case注释掉了(个位数)
spu提供了reset方法,会停掉已有的spu actor,然后重新拉起,主要用于某些testcase会构造spu出错,需要清理spu的状态。由于多进程模拟执行,可能某一方的spu actor停止重启后,另一方的spu actor可能还没重启,新旧spu actor连上了。 -> 待解决 (case数量小于5)
sf.remote
暂不支持装饰器模式,后续会支持。目前可以使用sf.proxy
对class进行装饰。
单控制器执行#
多控制器执行需要启动多个ray集群,debug也比单控制器模式难。为了方便用户仿真,隐语仍然支持单控制器执行模式,即用户仍然可以使用一个ray集群来模拟多参与方。
单控制器执行和原来没有变化,代码无需任何修改。
单控制器执行使用方式很简单,只需要在secretflow.init的时候,提供 parties
参数,具体使用方法可以参考 sf.init。
SF底层封装了ray和rayfed两套原语,向上统一成一种原语。