MindSpore云云联邦cross silo链接:https://www.mindspore.cn/federated
MindSpore Model Zoo fasterrcnn链接:https://gitee.com/mindspore/models/tree/master/official/cv/faster_rcnn
MindSpore Armour 差分隐私优化器DPSGD:https://www.mindspore.cn/mindarmour
cross silo云云联邦是指本地拥有较多数据的较少数据参与方联合训练的一种模式,联邦技术是出于保护隐私的目的而提出的,参与方数据不上云,仅仅上传本地训练之后的模型或梯度update。然而仅上传梯度就很好的保护了参与方的隐私了吗?针对仅上传的梯度,有许多工作对训练数据进行了攻击,端云联邦一般有deep leakage等,云云联邦有成员推理攻击。从信息论的角度来看,训练是把数据经过加工,提取其中信息变为知识,这是熵减的过程,熵越少,模型可用性越高;而隐私保护与其相反,隐藏混淆有用的信息,将明文域转变为另一个空间的数据,这是熵增的过程,熵越大,可用性越低。联邦学习的参与方本地训练的隐私泄露与过拟合有关,模型梯度记住了该参与方与众不同的样本。隐私保护不会限制学习的性能,因为本质上并不冲突:隐私保护了个体,学习挖掘了整体。
目前MindSpore关于云云联邦提供的隐私保护方案仅有安全聚合,缺点是交互多,计算开销大,云云联邦一般会联合训练较大的模型,也在尝试提供轻量级的隐私保护方案。MindArmour实现了DPSGD,但场景是单机。该方案不同于LDP数据发布,LDP是本地训练结束之后在待上传的数据上加噪扰动,而DP是在训练过程中加噪,更具体的,在一个batch中细分若干micro batch,得到micro batch的整体梯度后进行裁剪,加噪,更新梯度的操作。裁剪技术本身是一种避免过拟合的技术,和加噪同样可保护隐私。
无论是MindSpore官网案例还是一些学术界论文中都喜欢使用LeNet网络作为实验对象,但之前的一些实验过程中发现在小模型上发挥较好的方案在迁移到大模型之后,方法不再适用,而且小模型在实际场景中很难使用到,因此我们要在大模型上进行实验来为未来实际场景的适配打基础。本次使用的是目标检测任务中出名的Fasterrcnn模型,该模型维度量级为4000万。
联邦学习案例使用的是model zoo中的代码,不同的是model zoo使用的是coco原始数据集并用Python的pycocotools库进行处理,而联邦案例使用的数据集是封装好的MindRecord,官网中描述“由于COCO数据集已开源,请根据其官网自行下载好数据集,并作好数据集切分”,再将切分的json数据集转化为MindRecord。
model zoo提供了eval方法来计算目标检测的IOU指标,但它是基于coco原始数据集,使用MindRecord无法直接调用,所以联邦官网也是没有输出IOU结果,只是在最后显示了loss变化。
Fasterrcnn模型比较复杂,调用net.trainable_params()
输出模型参数各层信息,其中第156、157信息如下:
params[156]
Parameter (name=learning_rate, shape=(), dtype=Float32, requires_grad=True)
params[157]
Parameter (name=momentum, shape=(), dtype=Float32, requires_grad=True)
注意到其shape是(),代表的是标量,并不是空。
micro batch使得数据被联合处理,联合梯度被批量裁剪扰动,会加速训练。该方案的核心公式为:
g ~ t ← 1 L ( ∑ i g ˉ t ( x i ) + N ( 0 , σ 2 C 2 I ) ) (1) \tilde{g}_t \gets \frac{1}{L}\left( \sum_i \bar{g}_t(x_i) + \mathcal{N}(0, \sigma^2 C^2 I)\right) \tag{1} g~t←L1(i∑gˉt(xi)+N(0,σ2C2I))(1)
如果 L L L越大,信噪比就会变大,结果越准确。论文中没有体现出micro batch, L L L是指论文lot的大小也就是实现代码中batch的大小,论文中的batch对应代码的micro batch。(个人理解)micro batch主要影响的是裁剪操作,micro size分别取1和L,前者是每个样本计算裁剪再求和,后者是micro batch平均梯度裁剪,后者信息损失较少。
MindArmour中实现的DPSGD是针对分类任务实现的,欲与Fasterrcnn结合使用需要重写TrainOneStep类,由bound box和label等信息来计算loss和grad,复制DPModel源码,更改TrainOneStep部分代码:
record_datas = self._split(data)
record_img_shapes = self._split(img_shape)
record_gt_bboxes = self._split(gt_bboxe)
record_gt_labels = self._split(gt_label)
record_gt_nums = self._split(gt_num)
loss = self.network(record_datas[0], record_img_shapes[0], record_gt_bboxes[0], record_gt_labels[0], record_gt_nums[0])
sens = P.Fill()(P.DType()(loss), P.Shape()(loss), self.sens)
record_grad = self.grad(self.network, weights)(record_datas[0], record_img_shapes[0], record_gt_bboxes[0], record_gt_labels[0], record_gt_nums[0], sens)
for循环体也修改对应部分,其中data、ima_shape等皆为MindRecord一个batch迭代器,根据micro_batch
参数来对其进行split拆分操作,拆分的数据块进行loss、grad计算。
模型定义和DPModel中各个模块作用和执行的顺序:
net = Faster_Rcnn_Resnet(config=config)# 实例化前向网络
net = net.set_train()# 设置网络为训练模式
loss = LossNet()
opt = SGD(...)# 设定优化器
net_with_loss = WithLossCell(net, loss)# 设定损失函数并连接前向网络与损失函数
net = TrainOneStepCell(net_with_loss, opt, sens=config.loss_scale)# 定义训练网络
class DPModel(Model):
def _build_train_network(self):
network = self._amp_build_train_network()
def _amp_build_train_network(self, ...)
network = _TrainOneStepCell()
class _TrainOneStepCell(Cell):
def __init__(self, network, optimizer, norm_bound=1.0, sens=1.0, micro_batches=None, noise_mech=None, clip_mech=None):
def construct(self, data, label)
其中_TrainOneStepCell
的init参数如下:
其中construct
的参数如下:
顺序为:
_build_train_network
_amp_build_train_network
_TrainOneStepCell
_ClipGradients
start model train
之后反复调用
_TrainOneStepCell Construct
_ClipGradients construct
DPSGD的micro_batches
参数表示一个原始batch中含有micro_batch
的个数,则该值必须整除batch容量,micro_batch
最大等于batch,表示一个micro_batch
中只有一个样本;最小值是1,表示一个batch等于micro_batch
,该值最好取1,实验过程中测试如果取较大的话,会报栈深度超过1000的错误,使用context.set_context(max_call_depth=1000, save_graphs=True, save_graphs_path='./l')
可发现在图编译阶段server端报错导致联邦结束,如果把server端调用的代码改为non-dp,训练过程中报错,所以建议是micro_batch
取1。
有许多针对DPSGD加速的论文,但他们并不是产生大规模随机数是比较耗时的操作,而是micro batch的出现导致不能充分利用GPU.
The reason for this slowdown is a crucial privacy-related step called “per-example gradient clipping” whose naive implementation undoes the benefifits of batch training with GPUs.
fasterrcnn+DPSGD训练时间特别长,是non-DP的100倍,主要原因是使用mindspore.ops.StandardNormal生成大量随机数,环境是GPU。aicpu的c++源码,通过输入shape,首先计算shape中数的乘积。调用StandardNormal产生各种shape随机数的耗时如下:
output = stdnormal((200000,200)) # 255s
output = stdnormal((40000000,)) # 254s
for i in range(200000):
output = stdnormal((200,)) # 74s
for i in range(200):
output = stdnormal((200000,)) # 6s
for i, shape in enumerate(shape_list[:156]):
output = stdnormal(shape) # 150s
for i, shape in enumerate(shape_list):
output = stdnormal(shape) # 350s
使用g++编译c++源码,在linux本地直接产生随机数耗时却只有7.84秒,本地测试产生四千万随机数c++代码如下:
std::mt19937 gen(0);
clock_t start = clock();
for(i = 0; i < 40000000; ++i) {
std::normal_distribution norm(0.0, 1.0);
sample = norm(gen);
}
GPU生成随机数有另外的实现,DPSGD产生大规模随机数慢的问题,在MindSpore r1.5中修复。链接:https://gitee.com/mindspore/mindspore/blob/master/mindspore/ccsrc/backend/kernel_compiler/gpu/cuda_impl/random_op_impl.cu 实现方式:
#include "random_op_impl.cuh"
template <typename T>
__global__ void NormalKernel(int seed, curandState *globalState, T *output, size_t count) {
for (size_t i = blockIdx.x * blockDim.x + threadIdx.x; i < (count); i += blockDim.x * gridDim.x) {
curand_init(seed, i, 0, &globalState<i>);
output<i> = (T)curand_normal(&globalState<i>);
}
return;
}
gpu随机数生成,以前用的是朴素的采样算法,现在改用的是cudnn里面n家自己写的高效采样,提升速度效果显著,需要pull下来最新的mindspore源码,通过patch修改,再编译安装。
cuda上的c++实现,不知道如何单独测。该改动进主仓估计还要一段时间 。
shape=()的情况不知道DPSGD还有其他什么地方会遇到,NoiseGaussianRandom中并没有针对shape检查处理,直接将shape输入到normal函数中去产生随机数:
shape = P.Shape()(gradients)
stddev = P.Mul()(self._norm_bound, self._initial_noise_multiplier)
noise = normal(shape, self._mean, stddev, self._seed)
--worker_num=6
--start_fl_job_threshold=6
阈值最好设置成和worker一样的数目,如果woker=6但阈值设置为2则会报错中断训练。使用之前要配置环境:
在/usr/local/目录下找到cuda-{version}的目录名
export LD_LIBRARY_PATH=/usr/local/cuda-{version}/extras/CUPTI/lib64:$LD_LIBRARY_PATH
执行代码必须要以root身份登录,再用conda激活user下的环境,开启和关闭监视的命令分别为:
mindinsight start --port 9001 --workspace /home/tangcong/workspace --summary-base-dir ./
mindinsight stop --port 9001
其中--workspace
指的是存放日志的路径,不重要,--summary-base-dir
指的是结果数据的上一级目录,该目录下允许存在多个profiler结果,默认在--summary-base-dir
下生成data文件夹,可将其重命名进行多次检测 。
由于服务器没有图形化界面,无法通过浏览器访问网站,需要在win环境下访问服务器的ip:port,官网上MindInsight并没有说明。
监视名称出错,MindInsight显示StandardNormal算子占时92%,定位具体位置是AdaClipMech,该机制主要作用是随着训练的进行,模型趋于收敛,梯度变化小,此时也应该同步的缩减norm,进而加较少的噪声,该过程仅产生1个随机噪声,且每个step调用157次。结合net.trainable_params()
的信息和直觉,应该是在对grad的每一层产生对应shape大小的噪声最为耗时。总结是MindInsight定位算子耗时占比是准确的,但在定位具体哪个过程调用的这个算子时会有偏差。
分类 | worker_0 | worker_1 | worker_2 | worker_3 | worker_4 | worker_5 |
---|---|---|---|---|---|---|
dp non-cross | 0.39668 | 0.73668 | 0.46442 | 0.29315 | 0.46717 | 0.78076 |
dp cross | 0.55900 | 1.15937 | 0.96693 | 0.72568 | 0.78534 | 1.05122 |
non-dp cross | 0.38603 | 0.80037 | 0.42345 | 0.33930 | 0.56676 | 0.63010 |
non-dp non-cross | 0.14752 | 0.41411 | 0.26456 | 0.31825 | 0.20308 | 0.35767 |
epoch=1, round=80,累积eps=3007.254762358554,batch=4,micro batch=1,每个云参与方本地大约120张图,则每个epoch大约30个step。
其中dp cross速度太慢,跑了两天共43个epoch,挑选loss最小的填表。
loss是在训练集上测试的,太低有可能说明训练效果好,也有可能是过拟合,联邦loss反而大的情况。
加速之后的性能:使用r1.5分支,从原来的120s加速到2秒一个step, 和non-dp差距不大了。