作者 | FesianXu
导读
咱们在进行比照学习训练时候,常常须要设置大的batch size,而显卡的显存大小是限度batch size大小的最次要因素,在实际过程中咱们常常采纳recompute机制,通过用计算换空间的形式,缩小模型的内存耗费。然,在动态图训练时候,recompute机制须要进行手动的进行同步和梯度交融,本文纪录下这个问题。
全文7095字,预计浏览工夫18分钟。
在比照学习场景,或者其余须要大batch size的场景中,因为显卡显存的限度,常常会受限batch size的进一步增大,此时能够采纳“以计算换空间”的形式缩小模型的显存占用,得而进一步增大batch size。目前支流框架都对这个机制提供了反对,个别称之为recompute或者checkpoint机制,比方pytorch提供在[1],paddle(动态图)提供在[2],tensorflow(动态图)提供在[3];而在动态图框架中,比方tensorflow(动态图)提供在[4],而paddle(动态图)的这个能力由fleet-x提供[5]。为了了解recompute机制在分布式场景会导致的问题和解决方案,咱们首先须要理解recompute机制,咱们先简略介绍下。
一般来说深度学习网络的一次训练由三局部形成:
前向计算(forward):在该阶段会对模型的算子进行前向计算,对算子的输出计算失去输入,并传给下一层作为输出,直至计算失去最初一层的后果地位(通常是损失)。
反向计算(backward):在该阶段,会通过反向求导和链式法则对每一层的参数的梯度进行计算。
梯度更新(优化,optimization):在该阶段,通过反向计算失去的梯度对参数进行更新,也称之为学习,参数优化。
在之前反向求导公式的推导过程中[6],咱们晓得进行反向求导链式传递的时候,须要前一层的激活输入\(\sigma^{\prime}(z_{j}^{l})\)作为输出参加本层的梯度计算,如式子(1-1)所示(既是[6]中的公式(4.1)):
△(1-1)
公式看起来让人头大,咱们以代码为例子。在个别深度学习框架中,提供对自定义层的梯度定义,如博文[7]中介绍的。个别这类型的自定义都会提供两种输出,op和grad,如下代码:
#应用润饰器,建设梯度反向流传函数。其中op.input蕴含输出值、输入值,grad蕴含下层传来的梯度@tf.RegisterGradient("QuantizeGrad")def sign_grad(op, grad): input = op.inputs[0] # 取出以后的输出 cond = (input>=-1)&(input<=1) # 大于1或者小于-1的值的地位 zeros = tf.zeros_like(grad) # 定义出0矩阵用于掩膜 return tf.where(cond, grad, zeros) # 将大于1或者小于-1的上一层的梯度置为0
其中的op示意以后的算子操作符,而op.inputs即是该算子的输出列表,当然如果该算子是中间层算子,那么其输出就是上一层的输入了,而grad就是累积的梯度,个别咱们都会对op和grad进行操作,以计算以后层的梯度。绝对应的一些代码例子,读者有趣味可移步到[8],笔者实现了一个很简略的主动梯度求导试验例子。
如同有点跑题了,然而笔者以这个例子次要是想通知诸位读者,在模型的训练过程中为了反向梯度计算的不便会贮存很多两头变量,比方前向计算过程中的激活输入值,梯度值等等。有些两头值会被框架主动回收,比方非叶子节点的梯度值是会被主动回收的,见[9],然而有些两头变量不会,比方此时的中间层的输入值,这些两头变量占据了整个训练过程的大量内存。对于这些两头变量,如果心愿采纳更大的batch size进行训练,那么就须要缩小这些两头变量以换取更大的内存两头,recompute就是依据这个思路设计的。
recompute将深度网络切分为若干个局部(segment),对于每个局部而言,前向计算的时候,除了小局部必须贮存的变量外,其余两头变量都将被删除;在反向计算的时候,首先从新计算一遍前向算子,以取得须要的两头后果,再失常地运行反向算子。因而,recompute比照惯例的网络迭代而言,多计算了一遍前向计算,是典型的以计算换空间的“斗争”技术。整个过程如Fig 1.所示。
△Fig 1. 前向计算,反向计算和重计算的图示,其中重计算会将除了checkpoints之外的非必要两头变量删除,在进行反向梯度计算时候再从新进行前向计算失去。
通常会把切分网络的变量称之为checkpoints,有大量学者在钻研如何抉择适合的checkpoints能力更好地平衡计算性能和内存,通常以ERNIE,BERT等为例子,在其每个Transformer模块的两头变量作为切分就比拟适合。留神到无论在动态图还是在动态图中,都须要对checkpoints进行定义,比方paddle fleet中的recompute应用如下所示:
dist_strategy = fleet.DistributedStrategy()# 应用Recompute,并设置checkpointsdist_strategy.recompute = Truedist_strategy.recompute_configs = {"checkpoints": model.checkpoints}# 定义checkpoints作为切分点optimizer = fluid.optimizer.Adam(learning_rate=configs.lr)optimizer = fleet.distributed_optimizer(optimizer, dist_strategy) # 设置分布式优化器optimizer.minimize(model.loss)
然而,问题来了。在动态图中应用分布式的recompute机制可能并不会有问题,因为动态图的分布式应用暗藏了一些细节,然而在动态图中应用recompute机制时候(以paddle为例子),则会产生报错如Fig 2.所示,类似的报错信息同样在pytorch上也会遇到,见[10]。
△Fig 2. 在paddle动态图分布式场景中,采纳recompute机制将会产生这个报错。
在了解这个报错之前,咱们须要了解数据分布式并行(Data Distributed Parallel,DDP)的逻辑。数据并行指的是将数据程度划分,并给不同的节点(不同的过程或者卡,甚至是分布式节点)进行计算,而后将各个节点的梯度更新后果进行汇总后更新(这个步骤称之为规约,reduce),使得最终每个节点的梯度更新后果是保持一致的。一般来说DDP能够分为几个步骤[12]:
1、构建:DDP会将rank 0节点上的本地模型参数state\_dict()播送到其余节点上,以确保每个节点都是有着同样的模型正本进行初始化的。而后,每个节点上的DDP过程将会创立一个本地规约器(reducer),这个规约器用于负责后续反向流传过程中的多节点梯度同步。为了进步通信效率,通常会将多个梯度打包到一个“桶(bucket)”中,并且对整个桶进行规约,由此缩小通信老本,如Fig 3.所示。如果某个桶的某些梯度因为某些起因还没有筹备好,那么就须要期待这个梯度准备就绪能力同步,这通常都会影响训练效率。除了装桶外,规约器还须要在构建过程中对每个参数进行主动求导钩子函数(hook)的注册。在反向求导阶段,这些钩子函数在梯度就绪的时候将会被触发。
△Fig 3. 梯度同步以桶为单位进行。
2、前向流传:DDP拿到输出后就传递给本地模型,如果find\_unused\_parameters 设置为True,那么就会持续分析模型的输入。这个模式容许对模型的子图进行反向计算,DDP会遍历模型的主动求导图,从中找出参加反向计算的参数,并且将所有未应用的参数(也即是不须要加入规约的参数)标识为ready状态。在反向过程中,规约器只会期待unready状态的参数进行同步,然而规约器同样会规约所有参数,而仅是不会期待这些未应用的参数而已。
3、反向流传:反向的backward()函数间接蕴含在损失Tensor中,而这脱离了DDP的管制,因而DDP利用在构建阶段注册好的主动梯度钩子进行梯度同步的触发。当一个梯度ready后,其对应的DDP钩子函数会被触发,DDP因而会将其参数的梯度标识为ready状态,意味着曾经筹备好被规约了。当一个桶中所有梯度都曾经就绪后,规约器就对该桶触发allreduce操作,对所有节点该桶的值进行汇总求均匀。
4、优化阶段:对于优化器而言,它优化的是本地的模型。因为每个节点的初始状态和参数更新都是统一的,因而最初的多节点的模型参数也是统一的。
让咱们回到原来的问题上,理解了DDP的运行逻辑后,咱们就能读懂这个报错信息了。
Error happened, when parameter[385] [xxxxx@GRAD] has been ready before. Please set fine\_unused\_parameters=True to traverse backward graph in each step to prepare reduce in advance. If you have set , xxxx
当采纳了recompute机制后,将会有K个Transformer模块的checkpoints重叠在一起,在进行loss.backward()的时候,将会对同样的模型触发产生K个前向-反向过程,这意味着对于同一个参数将会有K个主动求导钩子函数进行绑定,一旦某一个钩子函数将参数设置为ready后,其余钩子函数就会导致这个报错。因而报错中会显示某个GRAD梯度参数曾经被标识为ready了,让你关上fine\_unused\_parameters = True以遍历反向图进行提前规约,然而你即使设置了同样也会报这个错的,因为实质起因在于recompute导致了参数被多个钩子函数所绑定了。[11]
那么怎么解决这个问题呢?一个简略的办法就是将DDP的前向-反向过程用no\_sync()上下文进行包裹,此时能够避免DDP进行多节点的梯度规约,并且在本地会集所有的本地模型梯度。在退出了no\_sync()上下文后,手动触发DDP的前向-反向,进行梯度规约。这个no\_sync上下文曾经在pytorch和paddle中实现了,咱们以paddle为例子(pytorch也是一样的,和paddle差异极其小,留神须要paddle 2.2以上才反对no\_sync上下文):
# required: distributed import numpy import paddle import paddle.distributed as dist from paddle.autograd import PyLayer from paddle.distributed.fleet.utils.hybrid_parallel_util import fused_allreduce_gradients class cus_tanh(PyLayer): @staticmethod def forward(ctx, x): y = paddle.tanh(x) ctx.save_for_backward(y) return y @staticmethod def backward(ctx, dy): y, = ctx.saved_tensor() grad = dy * (1 - paddle.square(y)) return grad class SimpleNet(paddle.nn.Layer): def __init__(self): super(SimpleNet, self).__init__() self.linear = paddle.nn.Linear(2, 2) def forward(self, inputs): inputs = cus_tanh.apply(inputs) return self.linear(inputs) if __name__ == '__main__': dist.init_parallel_env() model = SimpleNet() model = paddle.DataParallel(model) opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters()) for step in range(10): x_data = numpy.random.randn(2, 2).astype(numpy.float32) x = paddle.to_tensor(x_data) x.stop_gradient = False # step 1 : skip gradient synchronization by 'no_sync' with model.no_sync(): y_pred = model(x) loss = y_pred.mean() loss.backward() # step 2 : fuse + allreduce manually before optimization fused_allreduce_gradients(list(model.parameters()), None) opt.step() opt.clear_grad()
代码中的model.no\_sync()进入no\_sync上下文,在进行本地梯度计算完后,采纳fused\_allreduce\_gradients进行多节点的手动梯度规约。当然,将梯度规约全副放到了模型梯度计算完后,这样显然会比一边计算一边同时装桶进行多节点梯度规约来的慢,因为后者能够暗藏一些通信工夫,而前者则齐全是串行的过程。不过这也没方法,目前没有其余解决办法,权且先对付吧。
——END——
参考资料:
[1] https://pytorch.org/docs/stable/checkpoint.html, TORCH.UTILS.CHECKPOINT
[2]https://www.paddlepaddle.org.cn/documentation/docs/zh/2.2/api...\_cn.html#recompute, paddle recompute
[3]https://www.tensorflow.org/api\_docs/python/tf/recompute\_grad, tf.recompute\_grad
[4]https://www.tensorflow.org/versions/r1.15/api\_docs/python/tf/contrib, Module: tf.contrib
[5]https://fleet-x.readthedocs.io/en/stable/paddle\_fleet\_rst/fleet\_large\_batch\_training\_techniques\_cn.html#forward-recomputation-backpropagation, Forward Recomputation Backpropagation
[6]https://blog.csdn.net/LoseInVain/article/details/78092613, 《深度学习系列》反向流传算法的公式推导
[7]https://blog.csdn.net/LoseInVain/article/details/83108001, 在TensorFlow中自定义梯度的两种办法
[8]https://github.com/FesianXu/ToyAutoDiff, Toy Automatic Differentiation on computation graph
[9]https://blog.csdn.net/LoseInVain/article/details/99172594,在pytorch中对非叶节点的变量计算梯度
[10]https://github.com/pytorch/pytorch/issues/24005, Using torch.utils.checkpoint.checkpoint\_sequential and torch.autograd.grad breaks when used in combination with DistributedDataParallel
[11]https://github.com/pytorch/pytorch/issues/24005#issuecomment-519719412
[12]https://pytorch.org/docs/stable/notes/ddp.html, DISTRIBUTED DATA PARALLEL
[13]https://www.paddlepaddle.org.cn/documentation/docs/zh/2.2/api...\_cn.html#dataparallel, paddle DataParallel
举荐浏览:
分析多利熊业务如何基于分布式架构实际稳定性建设
百度工程师的软件品质与测试随笔[](http://mp.weixin.qq.com/s?__biz=Mzg5MjU0NTI5OQ==&mid=22475598...)
百度APP iOS端包体积50M优化实际(一)总览
基于FFmpeg和Wasm的Web端视频截帧计划
百度研发效力从度量到数字化变质之路
百度内容了解推理服务FaaS实战——Punica零碎