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

有错误!用PytorchLightning重构代码比较慢,修复后速度翻倍

时间:2023-03-21 00:18:07 科技观察

PyTorchLightning是一个重构PyTorch代码的工具,可以提取代码中复杂和重复的部分,使AI研究可扩展和快速迭代。然而,一位名叫FlorianErnst的博主最近发现PyTorchLightning存在一个bug——使得本应加速的训练变得更慢。本文的作者FlorianErnstErnst写了一篇博客,详细描述了他是如何发现这个bug的。以下为博客原文。两周前,我将一些深度学习代码重构为PytorchLightning,预计会有大约1.5倍的加速。但是,训练、评估和测试任务的速度降低了1/4。重构后的神经网络需要好几天才能出结果,想找出原因,尽量减少训练时间。事情是这样的,我正在使用一些开源深度学习代码,旨在展示一些机器学习任务的最先进架构。但是,代码本身既不干净也不优化。我注意到几次加速并将代码重构为Pytorch代码,这使训练速度提高了大约3倍。但我认为还有改进的余地。PytorchLightning是一个非常好的工具:它删除了很多样板代码并进行了一些优化,所以我决定使用Lightning重构这段代码。我原本期望代码加速大约1.5倍,但是当重构完成后,我惊讶地发现迭代时间从4秒变为15秒,这将训练时间增加了近3倍。哪里有问题?我首先运行Lightning的分析器来找出问题所在。basicprofiler给了我一个起点:大部分时间都花在运行一个epoch上;高级探查器没有给我更多信息。我想知道我是否在神经网络上错误配置了一些超参数。我弄乱了其中一些超参数,但训练速度没有变化。然后我调整了数据加载器,发现改变作业数量n_jobs对总训练时间有影响。但是,效果不是加快计算速度,而是减慢计算速度。随着工作数量的变化,在100个epoch中花费的时间。使用n_jobs=0完全禁用多处理使我的迭代速度几乎比使用6个内核快2倍。默认情况下,Pytorch会杀死正在运行的进程(worker)并在两个epoch之间重新加载,因此需要重新加载数据集。就我而言,加载数据集非常慢。我将DataLoader中的persistent_workers参数设置为True以防止正在运行的进程被终止,从而防止数据被重新加载。#MydataLoaderparametersDataLoader(train_dataset,batch_size=64,shuffle=True,num_workers=n_workers,persistent_workers=True,pin_memory=True,)因此,有两种可能:PytorchLightning杀死worker时没有考虑persistent_workers参数;问题出在别的地方。我在GitHub上创建了一个问题,希望Lightning团队能够意识到这个问题,然后我会找到问题的根源。GitHub地址:https://github.com/PyTorchLightning/pytorch-lightning/issues/10389寻找问题的根源Lightning的分析器与上下文管理器一起运行,并计算给定块需要多长时间。它使搜索特定分析器操作变得容易,例如运行“run_training_epoch”。开始摸索Lightning源码,查看导致循环变慢的指令,发现了一些问题:Loop.run调用Loop.on_run_start,Loop.on_run_start重新加载dataloader,如下图:Loop.run调用Loop.on_run_start...Loop.on_run_start回顾数据加载器问题似乎确实来自每个时期重新加载DataLoader。查看DataLoader的源码,发现是这样的:当使用persistent_workers>0迭代DataLoader时,如果_iterator`为None,则使用_get_iterator()重新加载整个dataset。确定是PytorchLightning误重置了_iterator,导致了这个问题。为了证实这一发现,我将DataLoader替换为自定义的仅可重载的__iter__方法:正如预期的那样,迭代后正确设置了_iterator属性,但重置为None。n_jobs=1,persistent_workers=True现在我只需要知道该属性何时设置为None以便找到问题的根源。我尝试使用调试器,但程序因多处理或CUDA而崩溃。我开始使用Python的getter&setter用法:当DataLoader._iterator设置为None时,将打印堆栈跟踪。这非常有效,输出将如下所示:(data_fetcher)File"loops\base.py",line139,inrunself.on_run_start(*args,**kwargs)File"loops\epoch\training_epoch_loop.py",line142,inon_run_startself._dataloader_iter=_update_dataloader_iter(...)文件"loops\utilities.py",line121,in_update_dataloader_iterdataloader_iter=enumerate(data_fetcher,batch_idx)File"utilities\fetching8__self.py",line19.reset()File"utilities\fetching.py",line212,inresetself.dataloader.reset()。..File"trainer\supporters.py",line498,in_shutdown_workers_and_reset_iteratordataloader._iterator=None通过跟踪发现每次开始运行时都会调用DataLoader.reset。深入研究代码后,我发现每次迭代都会重置DataFetcher,这会导致DataLoader也被重置。代码中没有条件可以避免重置:DataLoader必须在每个时期重置。这是我发现的迭代缓慢的根本原因。修复错误既然发现了错误,就必须找到修复它的方法。修复bug很简单:我从DataFetcher的__iter__方法中删除了self.reset行:使用修改再次训练现在每次迭代需要1.5秒,相比之前的15秒和vanillaPytorch的3秒,相比之下,速度有确实进步了很多。我向Lightning团队报告了我发现的错误,他们修复了它并在第二天推送了补丁。然后我更新了库,更新后发现他们的修复确实有效。相信会有更多人从这个修复中受益,他们的Lightning模型的训练和测试时间也会得到改善。如果您最近没有更新依赖项,请尝试安装pytorch-lightning==1.5.1或更高版本!