PyTorch“炼金术”如何提高速度?最近有个叫LorenzKuhn的小哥分享了他炼金过程中投入最少效果最好的17种提高训练速度的方法,而且基本上都可以在PyTorch中直接改,不需要引入额外的库。但是,需要注意的是,这些方法都假设模型是在GPU上训练的。该分享在Reddit上获得了600个赞。接下来,我们将从提速层面开始,依次介绍这些方法。1.选择合适的学习率方案。选择的学习率计划对收敛速度以及模型的泛化性能有很大影响。LeslieSmith提出的周期性学习率(CLR)和1cycle策略可以使复杂模型的训练快速完成。比如在cifar10上训练resnet-56时,通过使用1cycle,可以减少10次迭代次数,并且可以获得和原论文一样的准确率。在最好的情况下,与传统计划相比,此计划实现了巨大的加速。但是有一个缺点,他们引入了一些额外的超参数。为什么这行得通?一种可能的解释是,定期增加学习率有助于更快地遍历损失函数中的鞍点。2.在DataLoader和pinmemory中使用多个worker。使用torch.utils.data.DataLoader时,请设置num_workers>0而不是默认值0,pin_memory=True而不是默认值False。Nvidia高级工程师SzymonMicacz使用4个worker和pinnedmemory在单个训练epoch中将速度提高了一倍。需要注意的是,在选择worker数量时,建议将其设置为可用GPU数量的四倍。worker数量多寡都会减慢速度,数量多了会增加CPU内存消耗。3.最大化批量大小。这种方法极具争议性。但一般来说,使用GPU内存允许的最大批大小可以加快训练速度。如果要修改batchsize,还需要调整其他超参数,比如学习率。一般来说,批量大小加倍会使学习率加倍。之前,进行了一些不同批量大小的实验,通过将批量大小从64增加到512,实现了4倍的加速。4.使用自动混合精度(AMP)。PyTorch1.6包括PyTorch自动混合精度训练的本机实现。与其他地方使用的单精度(FP32)相比,某些操作可以在半精度(FP16)中运行得更快,而不会损失精度。然后,让AMP自动决定以何种格式执行操作,这样既可以加快训练速度,又可以减少内存占用。一些研究人员发现,在NVIDIAV100GPU上对一些常见的语言和视觉模型进行基准测试时,使用AMP比传统的FP32训练快2倍,最高可达5.5倍。目前,只有CUDAops可以通过这种方式进行自动广播。5.使用不同的优化器,例如AdamW,AdamW是具有权重衰减的Adam(而不是L2正则化),它在错误的实现和训练时间上优于Adam。此外,还有一些非局部优化器值得关注,比如LARS和LAMB。NVIDA的APEX实现了一些常见优化器的融合版本,例如Adam。与Adam的PyTorch实现相比,它避免了多次传入和传出GPU内存,从而实现了大约5%的加速。6.打开cudNN基准测试。如果您的模型架构保持固定并且输入大小保持不变,您可以设置torch.backends.cudnn.benchmark=True以启用cudNN自动缩放器。它将对cudNN中计算卷积的多种不同方式进行基准测试,以获得最佳性能指标。7.防止CPU和GPU之间频繁的数据传输。请注意,与.item()和.numpy()一样,tensor.cpu()通常用于将张量从GPU传输到CPU,请改用.detach()。如果您正在创建一个张量,您可以使用关键字参数device=torch.device('cuda:0')将它直接分配给您的GPU。在传输数据的上下文中,只要传输后没有任何同步点,就可以使用.to(non_blocking=True)。8.使用梯度/激活检查点。检查点通过将计算交换为内存来工作。Checkpoint部分并不是说整个计算图的所有中间activations都被存储并反向计算,而是保存中间activations并在后面的pass中重新计算。它可以应用于模型的任何部分。具体来说,在前向传递中,该函数将表现为torch.no_grad(),即不会存储任何中间激活。相反,前向传递保存输入元组和函数参数。在向后传递中,检索保存的输入和函数,然后再次在函数上计算前向传递,现在跟踪中间激活,使用这些激活值计算梯度。虽然对于给定的批处理大小,这可能会略微增加运行时间,但会显着减少内存占用。这反过来又允许您进一步增加所使用的批处理大小,从而提高GPU利用率。9.使用梯度累加。另一种增加批量大小的方法是在调用optimizer.step()之前在多个.backward()过程中累积梯度。这种方法主要是为了规避GPU内存限制而开发的,但尚不清楚额外的.backward()循环之间是否存在权衡。10.使用DistributedDataParallel进行多GPU训练。加速分布式训练的方法可能值得单独写一篇文章,但一个简单的方法是使用torch.nn.DistributedDataParallel而不是torch.nn.DataParallel。这样做可以让每个GPU都由一个专用的CPU内核驱动,避免了DataParallel的GIL问题。11.将梯度设置为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.如果不需要,请关闭调试API。Pytorch提供了很多调试工具,比如autograd.profiler、autograd.grad_check和autograd.anomaly_detection,一定要用的时候用,不需要的时候关掉,否则会拖慢你的速度训练速度。14.使用渐变剪裁。剪裁梯度可以加快收敛速度??。最初用于避免RNN中的梯度爆炸,这可以使用orch.nn.utils.clipgrad_norm来实现。目前尚不清楚哪些模型可以通过多少梯度裁剪来加速,但它似乎对RNN、基于Transformer和ResNets的架构以及一系列不同的优化器非常有用。15.在BatchNorm之前关闭偏差。这是关闭BatchNormalization层之前层偏置的一种非常简单的方法。对于二维卷积层,这可以通过将bias关键字设置为False来完成:torch.nn.Conv2d(…,bias=False,…)16.在验证期间关闭梯度计算。在验证期间设置torch.no_grad()。17.使用输入和批量归一化。额外提示,使用JIT来融合逐点操作。如果您有连续的逐点操作,您可以使用PyTorchJIT将它们组合成一个FusionGroup,然后在单个内核上启动,这样可以节省一些内存读写。在表达感谢的同时,不少网友也分享了自己在训练中的小技巧。比如炼金术士分享的“第十八”法,下载更多RAM。还有两个建议:1.数据转换(用于数据扩充)可能是速度改进的另一个来源。一些仅使用简单Python语句的转换可以通过使用numba包来加速。2.将数据集预处理成单个文件也有利于提高速度。除了这些,还有哪些方法可以提高训练速度呢?欢迎与我们分享~
