当前位置: 首页 > 科技观察

使您的PyTorch模型训练更快的9个技巧!

时间:2023-03-14 21:37:31 科技观察

不要让你的神经网络变成这样面对现实吧,你的模型可能还停留在石器时代。我打赌你仍然使用32位精度或GASP,甚至只在一个GPU上训练。我了解到网上有各种神经网络加速指南,但是没有checklist(现在有),使用这个checklist,一步一步来保证你能榨出你模型的所有性能。本指南涵盖从最简单的结构到最复杂的更改,以充分利用您的网络。我将向您展示可在Pytorch-lightningTrainer中使用的示例Pytorch代码和相关标志,因此您不必自己编写这些代码!**本指南适用于哪些人?**任何使用Pytorch进行深度学习模型研究的人,比如研究人员、博士生、学者等,我们这里说的模型可能需要你几天,甚至几周或几个月的时间来训练。我们将讨论:使用DataLoadersDataLoader中的worker数量Batchsize梯度累积预留计算图移动到单个16位混合精度训练移动到多个GPU(模型复制)移动到多个GPU节点(8+GP??U)ThinkModel加速技巧Pytorch-Lightning你可以在Pytorch的库Pytorch-lightning中找到我在这里讨论的每一个优化。Lightning是Pytorch之上的包装器,可自动进行训练,同时让研究人员完全控制关键模型组件。Lightning使用最新的最佳实践并最大限度地减少可能出错的地方。我们为MNIST定义了LightningModel,并使用Trainer来训练模型。frompytorch_lightningimportTrainermodel=LightningModule(...)trainer=Trainer()trainer.fit(model)1.DataLoaders这可能是最容易获得速度增益的地方。保存h5py或numpy文件以加速数据加载的日子已经一去不复返了,使用Pytorch数据加载器加载图像数据很简单(对于NLP数据,请查看TorchText)。在Lightning中,你不需要指定一个训练循环,只需要定义dataLoaders和Trainer,它们会在需要的时候被调用。dataset=MNIST(root=self.hparams.data_root,traintrain=train,download=True)loader=DataLoader(dataset,batch_size=32,shuffle=True)forbatchinloader:x,y=batchmodel.training_step(x,y)...2.DataLoaders中的工作人员数量另一个加速魔法是允许批量并行加载。因此,您可以一次加载nb_workers批次,而不是一次加载一批。#slowloader=DataLoader(dataset,batch_size=32,shuffle=True)#fast(use10workers)loader=DataLoader(dataset,batch_size=32,shuffle=True,num_workers=10)3.批量大小在开始下一个优化步骤之前,增加将批大小设置为CPU-RAM或GPU-RAM允许的最大值。下一节将重点介绍如何帮助减少内存占用,以便您可以继续增加批处理大小。请记住,您可能需要再次更新学习率。一个好的经验法则是,如果将批量大小加倍,则学习率也会加倍。4.梯度积累当你已经达到了计算资源的上限时,你的batchsize还是太小了(比如8),这时我们需要模拟一个更大的batchsize来为梯度下降提供一个很好的估计。假设我们想要达到128的批量大小。我们需要执行16次前向和反向传递,批量大小为8,然后是优化步骤。#clearlaststeoptimizer.zero_grad()#16accumulatedgradientstepsscaled_loss=0foraccumulated_step_iinrange(16):out=model.forward()loss=some_loss(out,y)loss.backward()scaled_loss+=loss.item()#updateweightsafter8steps.effective6opatcht=8izerstep()#lossisnowscaledupbythenumberofaccumulatedbatchesactual_loss=scaled_loss/16在闪电中,一切都为你做好了,只要设置accumulate_grad_batches=16:trainer=Trainer(accumulate_grad_batches=16)trainer.fit(model)日志。losses=[]...losses.append(loss)print(f'curturloss:{torch.mean(losses)'})上面的问题是loss仍然包含整个图的副本。在这种情况下,调用.item()来释放它。![1_CER3v8cok2UOBNsmnBrzPQ](9TipsForTrainingLightning-FastNeuralNetworksInPytorch.assets/1_CER3v8cok2UOBNsmnBrzPQ.gif)#badlosses.append(loss)#goodlosses.append(loss.item())Lightning会小心不要保留计算图的副本。6.单GPU训练完成前面的步骤后,就可以继续进行GPU训练了。在GPU上进行训练将跨多个GPU内核并行进行数学计算。您获得的加速取决于您使用的GPU类型。个人推荐2080Ti,公司推荐V100。乍一看,这可能会让您不知所措,但您实际上只需要做两件事:1)将您的模型移至GPU,以及2)每次通过它运行数据时将数据放在GPU上。#putmodelonGPUmodel.cuda(0)#putdataongpu(cudaonavariablereturnsacudacopy)xx=x.cuda(0)#runsonGPUnowmodel(x)如果你用Lightning,什么都不用做,设置Trainer(gpus=1)即可。#asklightningtousegpu0fortrainingtrainer=Trainer(gpus=[0])trainer.fit(model)在GPU上训练时要注意的主要事情是限制CPU和GPU之间的传输次数。#expensivexx=x.cuda(0)#veryexpensivexx=x.cpu()xx=x.cuda(0)如果内存耗尽,不要将数据移回CPU以节省内存。在求助于GPU之前,请尝试以其他方式优化GPU之间的代码或内存分配。另一件需要注意的事情是调用强制GPU同步的操作。清除内存缓存就是一个例子。#reallybadidea.StopsalltheGPUsuntiltheyallcatchuptorch.cuda.empty_cache()但是,如果使用Lightning,唯一可能出错的地方是在定义Lightning模块时。闪电特别注意不要犯这些错误。7.16位精度16位精度是一项将内存使用量减半的惊人技术。大多数模型都是使用32位精度数字训练的。然而,最近的研究发现16位模型也可以正常工作。混合精度意味着对某些东西使用16位,但将诸如权重之类的东西保持在32位。要在Pytorch中使用16位精度,请安装NVIDIA的apex库,并对您的模型进行这些更改。#enable16-bitonthemodelandtheoptimizermodel,optimizers=amp.initialize(model,optimizers,opt_level='O2')#whendoing.backward,letampdoitsoitcanscalethelosswithamp.scale_loss(loss,optimizer)asscaled_loss:scaled_loss.backward()放大器包会处理大部分事情.如果梯度爆炸或趋于0,它甚至可以缩放损失。在Lightning中,启用16bit不需要修改模型中的任何内容,也不需要执行我上面写的操作。只需设置Trainer(precision=16)。trainer=Trainer(amp_level='O2',use_amp=False)trainer.fit(model)8.转向多个GPU现在,事情变得非常有趣。有3种(也许更多?)方法可以进行多GPU训练。批量训练A)将模型复制到每个GPU,B)给每个GPU一部分批处理第一种方法称为“批量训练”。该策略将模型复制到每个GPU,每个GPU获得批次的一部分。#copymodeloneachGPUandgiveafourthofthebatchtoeachmodel=DataParallel(model,devices=[0,1,2,3])#outhas4outputs(oneforeachgpu)out=model(x.cuda(0))在Lightning中,你只需要增加GPU的数量并告诉trainer,什么都不做。#asklightningtouse4GPUsfortrainingtrainer=Trainer(gpus=[0,1,2,3])trainer.fit(model)模型分布训练将模型的不同部分放在不同的GPU上,batch按顺序移动有时你的模型可能太大而不是能够完全在记忆中。例如,带有编码器和解码器的序列到序列模型在生成输出时可能会占用20GB的RAM。在这种情况下,我们希望将编码器和解码器放在不同的GPU上。#eachmodelissooobigwecan'tfitbothinmemoryencoder_rnn.cuda(0)decoder_rnn.cuda(1)#runinputthroughencoderonGPU0encoder_out=encoder_rnn(x.cuda(0))#runoutputthroughdecoderonthenextGPUout=decoder_rnn(encoder_out.cuda(1))#normallywewanttsouttobring对于这种类型的训练不需要在Lightning中指定任何GPU,您应该将LightningModule中的模块放在正确的GPU上。classMyModule(LightningModule):def__init__():self.encoder=RNN(...)self.decoder=RNN(...)defforward(x):#modelswon'tbemovedafterthefirstforward因为#theyarealreadyonthecorrectGPUsself.encoder.cuda(0)self.decoder.cuda(1)out=self.encoder(x)out=self.decoder(out.cuda(1))#don'tpassGPUstotrainermodel=MyModule()trainer=Trainer()trainer.fit(model)两者混合top在的情况下,编码器和解码器仍然可以从并行化操作中获益。#changetheselinesself.encoder=RNN(...)self.decoder=RNN(...)#tothese#noweachRNNisbasedondifferentgpusetself.encoder=DataParallel(self.encoder,devices=[0,1,2,3])self.decoder=DataParallel(self.encoder,devices=[4,5,6,7])#inforward...out=self.encoder(x.cuda(0))#noticeinputsonfirstgpuindevicesout=self.decoder(out.cuda(4))#<---the4here使用多个GPU时要考虑的注意事项:如果模型已经在GPU上,model.cuda()将不会执行任何操作。始终将输入放在设备列表中的第一个设备上。在设备之间传输数据很昂贵,请将其用作最后的手段。优化器和梯度将保存在GPU0上,因此GPU0上使用的内存可能比其他GPU大得多。9.多节点GPU训练每台机器上的每个GPU都有一份模型。每台机器都获取一部分数据,并仅针对该部分进行训练。每台机器都可以同步梯度。如果你已经做到了这一步,你现在可以在几分钟内训练Imagenet!这并不像你想象的那么难,但它可能需要更多的计算集群知识。这些说明假定您在集群上使用SLURM。Pytorch通过在每个节点上的每个GPU上复制模型并同步梯度来允许进行多节点训练。因此,每个模型都在每个GPU上独立初始化,本质上是在数据分区上独立训练,除了它们都从所有模型接收梯度更新。在高层次上:在每个GPU上初始化模型的副本(确保设置种子,以便每个模型都初始化为相同的权重,否则会失败)。将数据集拆分为子集(使用DistributedSampler)。每个GPU只训练自己的小子集。在.backward()上,所有副本都会收到所有模型的梯度副本。这是模型之间唯一的通信。Pytorch有一个很好的抽象,叫做DistributedDataParallel,可以为你做这件事。要使用DDP,您需要做4件事:deftng_dataloader():d=MNIST()#4:Adddistributedsampler#samplersendsaportionoftngdatatoeachmachinedist_sampler=DistributedSampler(dataset)dataloader=DataLoader(d,shuffle=False,sampler=dist_sampler)defmain_process_entrypoint(gpu_nb):#2:setupconnectionsbetweenallgpusacrossallmachines#allgpusconnecttoasingleGPU"root"#thedefaultusesenv://world=nb_gpus*nb_nodesdist.init_process_group("nccl",rank=gpu_nb,worldworld_size=world)#3:wrapmodelinDPPtorch.cuda.pund_device())model=DistributedDataParallel(模型,device_ids=[gpu_nb])#trainyourmodelnow...if__name__=='__main__':#1:spawnnumberofprocesses#yourclusterwillcallmainforeachmachinemp.spawn(main_process_entrypoint,nprocs=8)但是,在Lightning中,只需设置节点数量,它就会处理剩下的给你。#trainon1024gpusacross128nodestrainer=Trainer(nb_gpu_nodes=128,gpus=[0,1,2,3,4,5,6,7])Lightning还附带一个SlurmCluster管理器,它可以方便地帮助您提交SLURM作业的正确详细信息.10.福利!在单个节点上使用多个GPU进行更快的训练事实证明,distributedDataParallel比DataParallel快得多,因为它只执行梯度同步通信。因此,一个好的技巧是使用distributedDataParallel而不是DataParallel,即使是在单台机器上进行训练也是如此。在Lightning中,这很容易通过将distributed_backend设置为ddp并设置GPU的数量来实现。#trainon4gpusonthesamemachineMUCHfasterthanDataParalleltrainer=Trainer(distributed_backend='ddp',gpus=[0,1,2,3])关于模型加速的想法解释如何通过寻找瓶颈来思考问题。我将模型分为几个部分:首先,我想确保数据加载没有瓶颈。为此,我使用了我描述的现有数据加载解决方案,但如果没有任何解决方案满足您的需求,请考虑离线处理并缓存到高性能数据存储中,例如h5py。接下来看看你在训练步骤中做了什么。确保你的前向传播速度快,避免过度计算,并尽量减少CPU和GPU之间的数据传输。最后,避免做会降低GPU速度的事情(在本指南中介绍)。接下来,我尝试最大化我的批量大小,这通常受GPU内存大小的限制。现在,关注如何跨多个GPU分布并在使用大批量时最小化延迟(例如,我可能会尝试在多个GPU上使用8000+的有效批量)。但是,您需要注意大批量。对于您的具体问题,请查看文献以了解人们缺少什么!