从数据科学中选择
作者:尤金·克维琴亚
机器之心编译
参加者:小周、蛋酱、魔王
高性能PyTorch 训练流程是什么样的?哪种模型提供的准确率最高?易于理解和扩展吗?或者易于并行化吗?答案就是以上所有。
如何用最少的精力完成最高效的PyTorch训练?一位拥有两年PyTorch使用经验的Medium博主最近分享了这方面的10个诚实技巧。
在Efficient PyTorch 的这一部分中,作者提供了一些识别和消除I/O 和CPU 瓶颈的技巧。第二部分描述了一些高效张量运算的技术,第三部分描述了高效的模型调试技术。
在阅读本文之前,您应该对PyTorch 有一些了解。
好吧,让我们从最明显的开始:
提示0:了解代码中的瓶颈在哪里
像nvidia-smi、htop、iotop、nvtop、py-spy 和strace 这样的命令行工具应该是你最好的朋友。您的训练管道是否受IO 限制?这些工具可以帮助您找到答案。
您可能从未听说过这些工具,或者即使听说过,也可能从未使用过它们。没关系。如果不立即使用也没关系。请记住,其他人使用它们训练模型的速度可能比您快5%、10%、15%。最终,在营销和就业机会方面可能会出现不同的结果。
数据预处理
几乎每个训练管道都从Dataset 类开始。负责提供数据样本。任何必要的数据转换或增强都可以在此处执行。也就是说,数据集报告其大小以及给定索引的数据样本。
如果您正在处理类似图像的数据(2D、3D 扫描),磁盘I/O 可能会成为瓶颈。要获取原始像素数据,您的代码必须从磁盘读取数据并将图像解码到内存中。每个任务都很快,但是当您需要尽快处理数百或数千个任务时,它可能会变得很困难。 NVidia 等库提供GPU 加速的JPEG 解码。如果您在数据处理管道中遇到IO 瓶颈,这种方法值得尝试。
作为另一种选择,SSD 磁盘访问时间约为0.08 至0.16 毫秒。 RAM 访问时间在纳秒范围内。数据可以直接保存到内存中。
建议1:如果可能,将全部或部分数据移至RAM。
如果内存中有足够的RAM 来加载和存储训练数据,这是从管道中排除最慢的数据采集步骤的最简单方法。
此建议对于Amazon 的p3.8xlarge 等云实例特别有用。该实例具有EBS磁盘,默认设置的性能非常有限。然而,该实例配备了高达248 GB 的RAM。这足以将整个ImageNet 数据集放入内存中。这一目标可以通过以下方式实现:
class RAMDataset(Dataset): def __init__(image_fnames, target): self.targets=target self.images=[] for fname in tqdm(image_fnames, desc='将文件加载到RAM'): with open(fname, ' rb' ) f: 作为self.images.append(f.read())
def __len__(self): 返回len(self.targets)
def __getitem__(self,index): target=self.targets[index] 图像, retval=cv2.imdecode(self.images[index], cv2.IMREAD_COLOR) 返回图像, target
我自己也遇到过这个瓶颈问题。我有一台配备4x1080Ti GPU 的家用电脑。在某个时候,我购买了一个带有4 个NVidia Tesla V100 的p3.8xlarge 实例,并将我的训练代码移到了那里。考虑到V100 比我的旧1080Ti 更新且更快,我预计训练速度会快15-30%。没想到,每一期的训练时间都增加了。这使得我们不仅要关注CPU 和GPU 的速度,还要关注基础设施和环境的差异。
根据您的场景,您可以保持每个文件的二进制内容不变并在RAM 中即时解码,也可以解码未压缩的图像并保留原始像素。但无论您采取哪种方法,这里都有第二条建议。
提示2:分析、测量和比较。彻底评估每个拟议管道变更的影响。
此建议仅关注训练速度,假设您没有对模型、超参数、数据集等进行任何更改。您可以设置魔法命令行参数(魔法开关)。如果指定,将在适当的数据示例上进行训练。此功能使您可以快速分析您的管道。
# 分析CPU 瓶颈。 python -m cProfile Training_script.py --profiling
# 分析GPU 瓶颈snvprof --print-gpu-trace python train_mnist.py
# 分析系统调用瓶颈strace -fcT python Training_script.py -e trace=open,close,read
建议3: *离线预处理所有内容*
技巧3:离线预处理所有内容
如果您想训练由多张2048x2048 图像创建的512x512 尺寸图像,请预先调整。如果您使用灰度图像作为模型的输入,请离线调整颜色。如果您想进行自然语言处理(NLP),请预先将其标记化并将其保存到磁盘。在训练过程中一遍又一遍地重复相同的动作是没有意义的。执行渐进式学习时,您可以以多种分辨率保存训练数据。这比在线调整目标分辨率更快。
对于表格数据,请考虑在创建数据集时将pd.DataFrame 目标转换为PyTorch 张量。
建议4:调整DataLoader 工作线程
PyTorch 使用DataLoader 类来简化训练模型的批处理过程。为了加快处理速度,您可以使用多个Python进程并行运行。大多数情况下您可以直接使用它。还有一些事情需要记住。
每个进程都会生成一批数据,这些数据可通过互斥锁同步供主进程使用。如果您有N 个工作线程,您的脚本将需要N 倍的RAM 来将这些批次的数据存储在系统内存中。您到底需要多少RAM?
让我们算一下:
假设您要训练批量大小为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 个工作线程,则总内存需求为167 Mb * 8=1,336 Mb。
我想事情不会那么糟糕,对吧?当您的硬件配置可以容纳超过8 个工作人员的批处理时,就会出现问题。也许可以简单地部署64 个工作线程,但这至少会消耗接近11Gb 的RAM。
如果数据是3D 立体扫描,情况会更糟。在这种情况下,512x512x512 的单通道卷将占用134Mb,批量大小为32 时,8 个工作线程将占用4.2Gb,仅需要32Gb 的RAM 来将中间数据保存在内存中。
此问题有一个解决方案,可以部分解决问题——。您可以尽可能减少输入数据的通道深度。
将RGB 图像保持为每通道8 位深度。图像可以轻松转换为浮点格式或在GPU 上标准化。
数据集使用uint8 或uint16 数据类型而不是long。
类MySegmentationDataset(Dataset): def __getitem__(self,index): image=cv2.imread(self.images[index]) target=cv2.imread(self.masks[index])
# 这里没有完成数据规范化和类型转换return torch.from_numpy(image).permute(2,0,1).contigously(), torch.from_numpy(target).permute(2,0,1) .contigously()
类Normalize(nn.Module): # https://github.com/BloodAxe/pytorch-toolbelt/blob/develop/pytorch_toolbelt/modules/normalize.py def __init__(self,means,std): super().__init__() self.register_buffer ('平均', torch.tensor(平均).float().reshape(1, len(平均), 1, 1).contigious()) self.register_buffer('std', torch.tensor(std).float ().reshape(1, len(std), 1, 1).倒数().连续())
defforward(self, input: torch.Tensor) - torch.Tensor: return (input.to(self.mean.type) - self.mean) * self.std
class MySegmentationModel(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) 输出=self.backbone(image)
如果target 不是None : loss=self.loss(output, target.long()) 返回损失
返回输出
这显着降低了RAM 要求。对于上面的例子。与之前的167Mb 相比,每批高效存储数据表示的内存使用量减少了五倍,达到33Mb。当然,这需要模型中的额外步骤来标准化数据或将数据转换为适当的数据类型。然而,张量越小,从CPU到GPU的传输速度就越快。
应谨慎选择DataLoader 工作线程的数量。您需要检查CPU 和IO 系统的速度、拥有多少内存以及GPU 处理数据的速度。
多GPU训练推理
神经网络模型变得越来越大。当前的趋势是使用多个GPU 来增加训练时间。幸运的是,较大的批量通常可以提高模型性能。 PyTorch 拥有只需几行代码即可运行多个GPU 的所有功能。然而,有一些注意事项乍一看可能并不明显。
model=nn.DataParallel(model) # 在所有可用的GPU 上运行模型
运行多个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 返回密集预测掩码,仅返回单个标量损失。
使用分布式训练,也称为nn.DistributedDataParallel。使用分布式训练的另一个好处是GPU可以实现100%负载。
如果您想了解更多信息,请查看这三篇文章:
https://medium.com/huggingface/training-larger-batches-practical-tips-on-1-gpu-multi-gpu-distributed-setups-ec88c3e51255
https://medium.com/@theaccelerators/learn-pytorch-multi-gpu-properly-3eb976c030ee
https://towardsdatascience.com/how-to-scale-training-on-multiple-gpus-dae1041f49d2
如果您有2 个或更多GPU,建议使用5:
您节省多少时间很大程度上取决于解决方案。我们发现在4x1080Ti 上训练图像分类管道可节省大约20% 的时间。还值得一提的是,nn.DataParallel 和nn.DistributedDataParallel 也可以用于推理。
关于自定义损失函数
创建自定义损失函数是一项有趣的练习,因此我们建议不时尝试一下。当谈到逻辑上如此复杂的损失函数时,必须记住一件事。它们都在CUDA 上运行,并且必须能够编写“CUDA 高效”代码。 “CUDA高效”意味着“没有Python控制流”。您还可以在CPU和GPU之间来回访问GPU张量中的各个值,但这会降低性能。
不久前,我实现了一个取自论文《Segmenting and tracking cell instances with cosine embeddings and recurrent hourglass networks》 的自定义余弦嵌入损失函数。虽然文字形式看起来很简单,但实现起来有点复杂。
我编写的第一个简单实现花了几分钟(不包括错误)来计算单个批次的损失。为了分析CUDA 瓶颈,PyTorch 提供了一个非常有用的内置分析器。它非常简单易用,并提供解决代码瓶颈的所有信息。
def test_loss_profiling(): loss=nn.BCEWithLogitsLoss() with torch.autograd.profiler.profile(use_cuda=True) as prof: input=torch.randn((8, 1, 128, 128)).cuda() input.requires_grad=真
目标=torch.randint(1, (8, 1, 128, 128)).cuda().float()
for i in range(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
版权声明:本文由今日头条转载,如有侵犯您的版权,请联系本站编辑删除。