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

高性能的PyTorch是如何炼成的?来过的人整理的10个避坑指南

时间:2023-03-23 11:48:48 科技观察

吐血。如何用最少的精力完成最高效的PyTorch训练?一位拥有两年PyTorch使用经验的Medium博主最近分享了他在这方面的10条真诚建议。在关于高效PyTorch的这一部分中,作者提供了一些识别和消除I/O和CPU瓶颈的技巧。第二部分讲解一些高效张量运算的技巧,第三部分介绍高效模型的调试技巧。在阅读本文之前,您需要对PyTorch有一定的了解。好吧,从最明显的开始:建议0:知道代码中的瓶颈在哪里nvidia-smi、htop、iotop、nvtop、py-spy、strace等命令行工具应该是你最好的伙伴。您的训练流水线是否受CPU限制?IO绑定?GPU绑定?这些工具将帮助您找到答案。您可能从未听说过这些工具,如果您听说过它们,您可能没有使用过它们。没关系。如果您不立即使用它们也没关系。请记住,其他人可能正在使用它们来训练模型,速度比您快5%、10%、15%——……在营销或工作机会方面,这可能最终会产生不同的结果。数据预处理几乎每个训练管道都从数据集类开始。它负责提供数据样本。任何必要的数据转换和扩充都可以在这里进行。简而言之,数据集报告其大小,并在给定索引的情况下报告数据样本。如果您正在处理类似图像的数据(2D、3D扫描),那么磁盘I/O可能会成为瓶颈。为了获得原始像素数据,您的代码需要从磁盘读取数据并将图像解码到内存中。每个任务都很快,但是当您需要尽可能快地处理成百上千个任务时,它就会成为一个挑战。NVidia等库将提供GPU加速的JPEG解码。如果您在数据处理管道中遇到IO瓶颈,这种方法绝对值得一试。还有另一种选择,SSD盘的访问时间约为0.08-0.16毫秒。RAM访问时间在纳秒级。我们可以将数据直接存入内存。建议1:如果可能,将全部或部分数据移至RAM。如果内存中有足够的RAM来加载和保存训练数据,这是从管道中排除最慢数据检索步骤的最简单方法。此建议可能对云实例特别有用,例如Amazon的p3.8xlarge。实例有EBS磁盘,默认情况下性能非常有限。但是,该实例配备了高达248Gb的RAM。这足以将整个ImageNet数据集装入内存!您可以通过以下方式实现此目的:.images.append(f.read())def__len__(self):returnlen(self.targets)def__getitem__(self,index):target=self.targets[索引]图像,retval=cv2.imdecode(self.images[索引],cv2.IMREAD_COLOR)returnimage,target这个瓶颈问题我个人遇到过。我有一台配备4x1080TiGPU的家用PC。有一次,我使用了一个带有4个NVidiaTeslaV100的p3.8xlarge实例,并将我的训练代码移到了那里。考虑到V100比我的老式1080Ti更新更快,我预计训练速度会提高15-30%。出乎意料的是,每个时期的训练时间都会增加。这教会了我要注意基础设施和环境的差异,而不仅仅是CPU和GPU的速度。根据您的情况,您可以保持每个文件的二进制内容完整并在RAM中对其进行动态解码,或者解码未压缩的图像并保留原始像素。但无论您采用何种方法,这里还有第二条建议:建议2:分析、衡量、比较。每次您提议对管道进行任何更改时,都要深入评估其全部影响。假设你没有对你的模型、超参数、数据集等进行任何更改,这个建议只关注训练速度。您可以设置一个神奇的命令行参数(魔法开关),当指定该参数时,可以在一些合理的数据样本上运行训练。使用此功能,您可以快速解析管道。#ProfileCPUbottleneckspython-mcProfiletraining_script.py--profiling#ProfileGPUbottlenecksnvprof--print-gpu-tracepythontrain_mnist.py#Profilesystemcallsbottlenecksstrace-fcTpythontraining_script.py-etrace=open,close,readAdvice3:*Preprocesseverythingoffline*Advice3:如果你想训练,离线预处理所有东西12x51由多张2048x2048图片合成的尺寸图片,请事先调整尺寸。如果您使用灰度图像作为模型的输入,请离线调整颜色。如果你做的是自然语言处理(NLP),请提前做分词并存盘。在训练过程中一遍又一遍地重复相同的操作是没有意义的。做增量学习的时候,可以保存多分辨率的训练数据,还是比在线调整到目标分辨率要快。对于表格数据,请考虑在创建数据集时将pd.DataFrame目标转换为PyTorch张量。建议4:调整DataLoaderWorkersPyTorch使用DataLoader类来简化训练模型的批处理过程。为了加快速度,它可以使用Python中的多处理并行执行。在大多数情况下,它可以直接使用。还有几件事要记住:每个进程都会生成一批数据,这些批数据可用于通过互斥锁同步的主进程。如果您有N个工作人员,那么您的脚本将需要N倍的RAM来将这批数据存储在系统内存中。您需要多少内存?让我们算一下:假设我们为Cityscapes训练一个图像分割模型,批量大小为32,RGB图像大小为512x512x3(高度、宽度、通道)。我们在CPU端进行图像归一化(稍后我将解释为什么这很重要)。在这种情况下,我们最终的图像张量将为512*512*3*sizeof(float32)=3,145,728字节。乘以批量大小,结果为100,663,296字节,或大约100Mb;除了图像,我们还需要提供一个真实掩码。它们各自的大小为(默认掩码类型为long,8字节)-512*512*1*8*32=67,108,864约67Mb;因此,一批数据所需的总内存为167Mb。假设有8个工作人员,总内存需求为167Mb*8=1,336Mb。听起来还不错,不是吗?当您的硬件设置可以容纳8名以上工人提供的较大批次时,就会出现问题。也许天真地放置64个工人,但这至少会消耗将近11Gb的RAM。如果您的数据是3D体积扫描,情况会更糟。在这种情况下,一个512x512x512的单通道卷将占用134Mb,批量大小为32,8个worker将占用4.2Gb,仅将中间数据保存在内存中,您将需要32Gb的RAM。对于这个问题,有一个部分解决方案——你可以尽可能地减少输入数据的通道深度:保持RGB图像在每通道深度8位。图像可以很容易地转换为浮点数或在GPU上标准化。在数据集中使用uint8或uint16数据类型而不是long。classMySegmentationDataset(Dataset):...def__getitem__(self,index):image=cv2.imread(self.images[index])target=cv2.imread(self.masks[index])#Nodatanormalizationandtypecastingherereturntorch.from_numpy(image).permute(2,0,1).contiguous(),torch.from_numpy(target).permute(2,0,1).contiguous()classNormalize(nn.Module):#https://github.com/BloodAxe/pytorch-toolbelt/blob/develop/pytorch_toolbelt/modules/normalize.pydef__init__(self,mean,std):super().__init__()self.register_buffer("mean",torch.tensor(mean).float().reshape(1,len(mean),1,1).contiguous())self.register_buffer("std",torch.tensor(std).float().reshape(1,len(std),1,1).reciprocal().contiguous())defforward(self,input:torch.Tensor)->torch.Tensor:return(input.to(self.mean.type)-self.mean)*self.stdclassMySegmentationModel(nn.Module):def__init__(self):self.normalize=Normalize([0.221*255],[0.242*255])self.loss=nn.CrossEntropyLoss()defforward(self,image,target):image=self.normalize(image)output=self.backbone(image)iftargetisnotNone:loss=self.loss(output,target.long())returnlossreturnoutput通过这样做,上述示例的RAM要求大大降低。用于有效存储数据表示的内存使用量将为每批33Mb,低于之前的167Mb,减少了五倍。当然,这需要模型中的额外步骤来规范化数据或将数据转换为适当的数据类型。然而,张量越小,CPU到GPU的传输速度越快。应仔细选择DataLoaderworker的数量。你应该看看你的CPU和IO系统有多快,你有多少RAM,你的GPU处理数据的速度有多快。多GPU训练和推理神经网络模型越来越大。如今,使用多个GPU来增加训练时间已成为一种趋势。幸运的是,它通常会提高模型性能以实现更大的批量。PyTorch拥有仅需几行代码即可在多个GPU上运行的所有功能。但是,有些注意事项乍一看并不明显。model=nn.DataParallel(model)#RunsmodelonallavailableGPUs运行多个GPU的最简单方法是将模型包装在nn.DataParallel类中。除非您正在训练图像分割模型(或生成大张量作为输出的任何其他模型),否则它在大多数情况下都运行良好。在正向推导结束时,nn.DataParallel会收集主GPU上的所有GPU输出通过输出向后运行,完成梯度更新。所以,现在有两个问题:GPU负载不平衡;主GPU上的聚合需要额外的显存首先,只有主GPU可以做损失计算、逆推和梯度步骤,其他GPU会冷却到60摄氏度以下,等待下一组数据。其次,在主GPU上聚合所有输出所需的额外内存通常会促使您减少批量大小。nn.DataParallel在多个GPU上均匀分布批次。假设您有4个GPU,总批处理大小为32;然后每个GPU将获得一个包含8个样本的块。但问题是,虽然所有主GPU都可以轻松地将这些批次放入相应的VRAM中,但主GPU必须分配额外的空间来容纳32个批次大小以供其他卡输出。对于这种不平衡的GPU使用,有两种解决方案:在训练期间,继续使用前向传播中的nn.DataParallel计算损失。在这种情况下。za不向主GPU返回densepredictionmasks,而只返回一个singlescalarloss;使用分布式训练,也称为nn.DistributedDataParallel。使用分布式训练的另一个好处是看到GPU处于100%负载。如果你想了解更多,可以看看这三篇文章:https://medium.com/huggingface/training-larger-batches-practical-tips-on-1-gpu-multi-gpu-distributed-setups-ec88c3e51255https://medium.com/@theaccelerators/learn-pytorch-multi-gpu-properly-3eb976c030eehttps://towardsdatascience.com/how-to-scale-training-on-multiple-gpus-dae1041f49d2建议5:如果你有两个或更多GPU可以节省多少时间在很大程度上取决于你的解决方案,我观察到在4x1080Ti上训练图像分类管道时,它可以节省大约20%的时间。另外值得一提的是,您还可以使用nn.DataParallel和nn.DistributedDataParallel进行推理。关于自定义损失函数编写自定义损失函数是一项有趣的练习,我建议大家不时尝试一下。当谈到这个逻辑上复杂的损失函数时,你必须记住一件事:它们都在CUDA上运行,你应该能够编写“CUDA高效”代码。“CUDA-efficient”意味着“没有Python控制流”。在CPU和GPU之间来回切换,访问GPU张量的个别值也可以,但性能会很差。前段时间,我从这篇论文《Segmenting and tracking cell instances with cosine embeddings and recurrent hourglass networks》中实现了一个自定义余弦嵌入损失函数,它在文本形式上非常简单,但实现起来有点复杂。我编写的第一个简单实现花了(除了错误)几分钟来计算单个批次的损失。为了分析CUDA瓶颈,PyTorch提供了非常方便的内置分析器,非常简单易用,提供了解决代码瓶颈的所有信息:deftest_loss_profiling():loss=nn.BCEWithLogitsLoss()withtorch.autograd.profiler.profile(use_cuda=True)asprof:input=torch.randn((8,1,128,128)).cuda()input.requires_grad=Truetarget=torch.randint(1,(8,1,128,128)).cuda(.float()foriinrange(10):l=loss(input,target)l.backward()print(prof.key_averages().table(sort_by="self_cpu_time_total"))建议9:如果设计自定义模块和损失-配置并测试它们在对初始实现进行分析后,它能够加速100倍。有关在PyTorch中编写高效张量表达式的更多信息,请参阅高效PyTorch—第2部分。时间与金钱最后但并非最不重要的一点,有时投资更强大的硬件可能比优化代码更有价值。软件优化始终是一个结果不确定的高风险旅程,同时升级CPU、RAM、GPU或所有硬件可能会更有效率。金钱和时间都是资源,两者的平衡使用是成功的关键。通过硬件升级可以更轻松地解决某些瓶颈。写在最后,知道如何充分利用日常工具是提高熟练度的关键。尽量不要创建“快捷方式”。如果遇到不清楚的地方,请深入挖掘。总有机会发现新知识。正所谓“一日一省”:问问自己,我的代码还能改进吗?这种对卓越的信念与其他技能一样对于计算机工程师的道路至关重要。原文链接:https://towardsdatascience.com/efficient-pytorch-part-1-fe40ed5db76c【本文为《机器之心》专栏原文翻译,微信公众号《机器之心》(id:almosthuman2014)"]戳这里,阅读更多本作者的好文