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

PyTorch指南:让您的深度学习模型训练快速的17个技巧!

时间:2023-03-18 16:24:18 科技观察

如果你在pytorch中训练深度学习模型,你如何加速模型训练?在这篇文章中,我将介绍一些最小的变化和最大的影响方法来加速pytorch中的深度学习模型。对于每种方法,我都会简要描述其想法,然后估计改进速度并讨论其局限性。我将突出显示我认为重要的部分,并在每个部分中展示一些示例。接下来我将假设您正在使用GPU来训练模型。这些方法基本不需要导入其他库,只需要在pytorch中进行改动即可。以下是我根据估计的加速比对不同方法的排名:考虑使用其他学习率调整计划在DataLoader和页面锁定内存中使用多个工作进程以最大化批处理大小使用自动混合精度AMP考虑打开cudNN基准测试的不同优化器当心CPU和GPU之间的数据传输使用梯度/激活检查点使用梯度累积多GPU分布式训练将梯度设置为None而不是0使用.as_tensor()而不是.tensor()仅在需要时打开调试模式使用梯度裁剪并忽略偏差在BatchNorm之前。关闭梯度计算归一化输入和批处理1.考虑使用其他学习率调整方案。训练中使用的学习率调整方案会极大地影响收敛速度和模型泛化能力。.循环学习率和1Cycle学习率方法由LeslieN.Smith提出,然后由fast.ai的JeremyHoward和SylvainGugger推广。总体而言,1Cycle学习率方法如下图所示:在最好的情况下,与传统的学习率策略相比,该策略实现了巨大的加速——Smith称之为“超收敛”。例如,使用1Cycle策略在ImageNet上将ResNet-56训练迭代次数减少了10倍,与原始论文的性能相匹配。这种策略似乎适用于常见的架构和优化器。PyTorch提供了两个方法torch.optim.lr_scheduler.CyclicLR和torch.optim.lr_scheduler.OneCycleLR来实现这个操作,请参考相关文档。这两种方法的缺点是引入了许多额外的超参数。这篇文章和存储库提供了如何找到好的超参数(包括上面提到的学习率)的详细概述和实现。至于为什么这样做?今天还不完全清楚,但一种可能的解释是,定期提高学习率有助于更快地通过损失鞍点。2.在DataLoader和page-lockmemory中使用多个辅助进程使用torch.utils.data.DataLoader时,设置num_workers>0而不是默认值0,设置pin_memory=True而不是默认值False。至于为什么要这样做,这篇文章会给你答案。根据上述方法,SzymonMicacz在具有四个worker和页面锁定内存的单个epoch中实现了2倍的加速。根据经验,通常将进程数设置为可用GPU数的四倍,任何高于或低于此值都会减慢训练速度。但请注意,增加num_workers会增加CPU内存消耗。3.最大化batchsize一直以来,人们对增加batchsize没有定论。一般来说,在GPU显存允许的情况下,增加batchsize会提高训练速度,但同时需要调整学习率等其他超参数。根据经验,当批量大小加倍时,学习率也会相应加倍。OpenAI的论文表明,不同的batchsize有不同的收敛周期。DanielHuynh进行了一些不同批量大小的实验(使用上述1Cycle策略),其中他将批量大小从64增加到512,实现了4倍的加速。但是请注意,较大的批量大小会降低模型的泛化能力,反之亦然。4.使用自动混合精度AMPPyTorch1.6,支持本地自动混合精度训练。在不损失精度的情况下,某些操作使用半精度(FP16)比使用单精度(FP32)更快。AMP自动决定应以何种精度执行哪些操作,从而加快训练速度并减少内存使用。AMP的使用如下:importtorch#Createsonceatthebeginningoftrainingscaler=torch.cuda.amp.GradScaler()fordata,labelindata_iter:optimizer.zero_grad()#Castsoperationstomixedprecisionwithtorch.cuda.amp.autocast():loss=model(data)#Scalestheloss,并向后调用()#tocreatescaledgradientsscaler.scale(loss).backward()#Unscalesgradientsandcalls#orskipsoptimizer.step()scaler.step(optimizer)#Updatethescaleforexiterationscaler.update()Huang和同事在NVIDIAV100GPU上进行了一些通用的语言和视觉模型我进行了基准测试,发现在FP32训练中使用AMP,训练速度提升了2倍左右,最高甚至达到了5.5倍。目前只有CUDA支持以上方法,查看此文档了解更多信息。5.考虑不同的优化器AdamW是fast.ai提出的带权重衰减的Adam(而不是L2正则化),在PyTorch中由torch.optim.AdamW实现。在错误率和训练时间上,AdamW都优于Adam。查看这篇文章,了解为什么权重衰减可以让Adam工作得更好。Adam和AdamW都适合上面提到的1Cycle策略。此外,LARS和LAMB等其他优化器也受到了很多关注。NVIDA的APEX优化并融合了常见的优化器,例如Adam。与PyTorch中的原始Adam相比,训练速度提高了约5%,因为它避免了GPU内存之间的多次传输。6.开启cudNNbenchmark如果你的模型架构是固定的,输入大小保持不变,设置torch.backends.cudnn.benchmark=True可能会提高模型速度(帮助文档)。通过启用cudNN自动缩放器,可以对cudNN中计算卷积的多种方法进行基准测试,并选择最快的方法。至于提速效果,SzymonMigacz在前向卷积中提速70%,同时前向和反向卷积提速27%。请注意,如果您想根据上述方法最大化批量大小,则此自动调整可能非常耗时。7.注意CPU和GPU之间的数据传输Tensors可以通过tensor.cpu()从GPU传输到CPU,否则使用tensor.cuda(),但是这样的数据转换是昂贵的。.item()和.numpy()的使用也是如此,推荐使用.detach()。如果您正在创建一个新的张量,请使用关键字参数device=torch.device('cuda:0')将其直接分配给GPU。最好使用.to(non_blocking=True)来传输数据,保证传输后没有同步点。此外,SantoshGupta的SpeedTorch也值得一试,虽然它的加速效果并不完全清楚。8.使用梯度/激活检查点检查点通过将计算保存到内存来工作。Checkpoints在反向传播算法中不保存计算图的中间激活,而是在反向传播过程中重新计算,可用于模型的任何部分。具体来说,在前向传递中,函数使用torch.no_grad()运行并且不存储中间激活。相反,前向传递将保存输入元组和函数参数。在反向传播过程中,检索保存的输入和函数,再次向前传播函数,记录中间激活并使用这些激活值计算梯度。因此,对于特定的批处理大小,这可能会稍微增加运行时间,但会显着减少内存消耗。反过来,您可以进一步增加批量大小以更好地利用GPU。虽然checkpoint可以通过torch.utils.checkpoint很方便的实现,但是还是需要理解它的思想和本质。PriyaGoyal的教程清楚地展示了检查点的一些关键思想,推荐阅读。9.另一种使用梯度累积增加批量大小的方法是在调用Optimizer.step()之前在多次.backward()过程中累积梯度。根据HuggingFace的ThomasWolf发表的一篇文章,梯度累加可以这样实现:,标签)#Computelossfunctionloss=loss/accumulation_steps#Normalizeourloss(ifaveraged)loss.backward()#Backwardpassif(i+1)%accumulation_steps==0:#Waitforseveralbackwardstepsoptimizer.step()#Nowwecandoanooptimizerstepmodel.zero_grad()#Resetgradientstensors%i(evaluation_steps==0:#Evaluatethemodelwhenwe...evaluate_model()#...havenogradientsaccumulated这种方法主要是为了避免GPU显存的限制,但不知道其他.backward()循环之间的权衡。讨论fastai论坛似乎表明它实际上加速了训练,所以它可能值得一试。有关详细信息,请参阅托管在GitHub上的rawgradient_accumulation.py。10.多GPU分布式训练使用distribu加速模型的简单方法ted训练是使用torch.nn.DistributedDataParallel而不是torch.nn.DataParallel。这样,每个GPU将由专用CPU内核驱动,避免了DataParallel的GIL问题。强烈建议阅读分布式训练相关文档以获取更多信息:PyTorchDistributedOverview—PyTorchTutorials1.7.0documentation11。将渐变设置为None而不是0。设置.zero_grad(set_to_none=True)而不是.zero_grad()。通过这种方式,内存分配器会处理梯度而不是主动将其设置为0,这会产生文档中所示的适度加速,但不要抱太大希望。请注意,这样做不会有任何副作用!阅读文档以获取更多信息。12.使用.as_tensor()而不是.tensor()torch.tensor()本质上是复制数据,因此如果要转换numpy数组,请使用torch.as_tensor()或torch.from_numpy()以避免复制数据。13.只在需要的时候开启调试模式Pytorch提供了很多调试工具,比如autograd.profiler、autograd.grad_check和autograd.anomaly_detection。使用时要小心,这些调试工具明显会影响训练速度,所以不用的时候关掉。14.使用梯度裁剪为了避免RNN中的梯度爆炸,使用梯度裁剪gradient=min(gradient,threshold)可以加速收敛。该方法得到了理论和实验的支持。HuggingFace的Transformer提供了一个将渐变裁剪与其他方法(如AMP)有效结合的清晰示例。在PyTorch中,这也可以使用torch.nn.utils.clip_grad_norm_(文档)来完成。虽然我并不完全清楚哪些模型可以从梯度裁剪中受益,但毫无疑问,对于RNNs,一系列基于Transformer和ResNets结构的优化器,该方法显然发挥了作用。15.IgnosingthedeviationbeforeBatchNorm是关闭BatchNormalization层之前上一层偏差的一种简单有效的方法。对于二维卷积层,可以通过设置bias关键字为False来实现,即torch.nn.Conv2d(...,bias=False,...)。阅读文档以了解其工作原理。与其他方法相比,这种方法的速度提升是摆在那里的。16.验证时关闭梯度计算,在模型验证时命令torch.no_grad()提示:使用JIT进行pointwisefusion如果要进行相邻的pointwise操作,可以使用PyTorchJIT将它们组合成一个FusionGroup并启动它在单核上而不是像默认的多核,同时也可以节省一些内存用于读写。SzymonMigacz展示了如何使用@torch.jit.script装饰器来融合GELU操作,如下所示:@torch.jit.scriptdeffused_gelu(x):returnx*0.5*(1.0+torch.erf(x/1.41421))comparedtoUnfused版本,融合这些操作可以让fused_gelu的执行速度提高5倍。本文转载自雷锋网。如需转载,请在雷锋网官网申请授权。