加速你的 PyTorch 模型训练的简单方法
原文towardsdatascience.com/simple-ways-to-speed-up-your-pytorch-model-training-9c9d4899313d?sourcecollection_archive---------3-----------------------#2024-05-28如果所有机器学习工程师都希望得到一个东西那就是更快的模型训练—— 也许在获得良好的测试指标之后。https://alexdremov.medium.com/?sourcepost_page---byline--9c9d4899313d--------------------------------https://towardsdatascience.com/?sourcepost_page---byline--9c9d4899313d-------------------------------- Alex Dremov·发布于 Towards Data Science ·11 分钟阅读 ·2024 年 5 月 28 日–https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/eb75109bbcb6622409f76f7e507791cd.png图片来源Julian Hochgesang / Unsplash这个话题甚至需要介绍吗加速机器学习模型训练是所有机器学习工程师的目标。更快的训练意味着更快的实验也意味着更快的产品迭代。此外这还意味着一次模型训练将需要更少的资源。所以直接进入正题容器化是的单靠这个不会加速你的训练。但它针对的是另一个重要方面——可重现性。有时候使用固定库版本的 virtualenv 就足够了但我鼓励你更进一步为你的模型训练构建一个一体化的 Docker 容器。这确保了在调试、分析和最终训练过程中环境的一致性。你最不希望的事情就是优化一个部分代码而这个部分由于 Python12 提升了速度已经不再是瓶颈了。例如甚至有一个错误在不同的 CUDA 版本下无法重现。作为起点你可以使用 NVIDIA 提供的预构建镜像。这些镜像已经安装了 CUDA、PyTorch 和其他流行的库[## PyTorch | NVIDIA NGCPyTorch 是一个 GPU 加速的张量计算框架。功能可以通过常见的 Python 库进行扩展……catalog.ngc.nvidia.com](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/pytorch?refalexdremov.mesourcepost_page-----9c9d4899313d--------------------------------) Docker 容器是解决此类问题的终极方案“嘿它在我的机器上可以工作我不知道为什么在你的机器上不行。”熟悉 PyTorch 分析器在优化任何东西之前你必须了解你的一些代码部分运行了多久。PyTorch 分析器几乎是一个功能齐全的训练分析工具。它能够记录CPU 操作的时间CUDA 内核的时间内存消耗历史这就是你所需要的一切。而且它很容易启用要记录事件你只需要像这样将训练嵌入分析器上下文中importtorch.autograd.profilerasprofilerwithprofiler.profile(activities[ProfilerActivity.CPU,ProfilerActivity.CUDA],on_trace_readytorch.profiler.tensorboard_trace_handler(./logs),)asprof:train(args)之后你可以启动 tensorboard 并查看分析轨迹。别忘了安装 torch-tb-profiler。[## PyTorch Profiler 与 TensorBoard - PyTorch 教程 2.3.0cu121 文档]准备数据和模型 使用分析器记录执行事件 运行分析器 使用 TensorBoard 查看结果并…pytorch.org](https://pytorch.org/tutorials/intermediate/tensorboard_profiler_tutorial.html?refalexdremov.mesourcepost_page-----9c9d4899313d--------------------------------)分析器有许多不同的选项但最重要的是activities和profile_memory。你可以尝试其他选项但请记住一个简单的规则你启用的选项越少开销就越小。所以如果你想分析 CUDA 内核执行的时间最好关闭 CPU 分析和其他所有功能。在这种模式下分析将尽可能接近真实执行。为了让轨迹更容易理解考虑添加描述你代码核心部分的分析上下文。如果没有启用分析这些上下文将无效。withprofiler.record_function(forward_pass):resultmodel(**batch)withprofiler.record_function(train_step):step(**result)这样你使用的标签将在轨迹中可见。这样识别代码块会更加容易。甚至在模式的 forward 中进行更细粒度的分析withprofiler.record_function(transformer_layer:self_attention):dataself.self_attention(**data)...withprofiler.record_function(transformer_layer:encoder_attention):dataself.encoder_attention(**data,**encoder_data)理解 PyTorch 轨迹收集轨迹后在 tensorboard 中打开它们。这就是 CPU CUDA 分析的样子https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b207f5f91e4560da06402145f3cfb486.png© 版权 2024PyTorch |pytorch.org/tutorials/intermediate/tensorboard_profiler_tutorial.html立刻找到任何训练的核心部分数据加载前向传播反向传播反向传播由 PyTorch 在单独的线程中处理如上图中的线程 16893所以很容易识别。数据加载对于数据加载我们希望接近零的时间。没有妥协。这是因为在数据加载期间 GPU 不做任何事情这会导致可用资源的低效利用。然而数据处理可以与 GPU 计算重叠因为它们是独立的部分。你可以轻松地识别 GPU 闲置的区域——只需查看分析器跟踪中的GPU 估算 SM 效率和GPU 利用率数值。没有活动的区域就是我们的“患者”。这就是 GPU 什么也不做的地方。一个简单的解决方案是在后台进程中处理数据没有 GIL在并行进程中处理数据增强和变换如果你使用 PyTorch 的 DataLoader那么可以通过指定num_workers来轻松实现这一点。如果你使用IterableDataset情况会更复杂因为数据将会重复。但这个问题仍然可以通过使用 get_worker_info() 来解决——你需要调整迭代方式以确保每个工作进程接收不同且不重叠的行。对于更可配置的处理你可以考虑使用multiprocessing自行实现多进程变换 如果你从未检查过代码的数据处理速度那么这个小小的修改可能会带来剧烈的加速。与内存分配器成为朋友你希望与 PyTorch 的 CUDA 缓存分配器成为朋友。当你在 CUDA 设备上使用 PyTorch 分配张量时PyTorch 会使用一个缓存分配器。这是因为cudaMalloc/cudaFree是昂贵的操作我们希望避免调用它们因此 PyTorch 有自己的分配器它会尝试重用通过cudaMalloc分配的块。也就是说如果 PyTorch 的分配器有合适的块可用它会直接提供而不需要调用cudaMalloc。这样cudaMalloc只会在一开始调用。然而如果你处理的是长度可变的数据不同的前向传递将需要不同大小的中间张量。因此PyTorch 的分配器可能没有合适的数据块可用。在这种情况下分配器会崩溃并通过调用cudaFree释放之前分配的块以为新的分配腾出空间。之后分配器开始重新构建它的缓存进行大量的cudaMalloc这是一项昂贵的操作。你可以通过查看张量板分析器视图的内存分析部分来发现这个问题。 你也可以在跟踪记录中发现这个问题。它将以对cudaMalloc和cudaFree的调用形式显示出来https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/414070253ac9c94f59f8f522399d7fb4.pngPyTorch 分配器崩溃 | 图片来自作者如你所见与分配器保留的内存对应的红线不断变化。这意味着 PyTorch 的分配器无法有效地处理分配请求。当分配操作不再让分配器崩溃时红线完全是直的https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/e8e801553645665ba7dff138173a00cd.pngPyTorch 分配器按预期工作 | 图片来自作者如我所说这通常是由于张量的形状不固定。如何修复这个问题可扩展的段第一件值得尝试的事情是设置 PyTorch 相对较新的分配器模式PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True如果设置为*True*此设置指示分配器创建可以在以后扩展的 CUDA 分配以更好地处理工作负载频繁更改分配大小的情况例如批量大小变化的情况。所以这告诉 PyTorch 分配器分配将来可能会扩展的块这正是我们的情况。尽管如此如果大小变化过大仍然可能无法解决问题。在这种情况下转到下一个选项。使分配的变化更少另一种可能的解决方案是使数据形状保持一致。这样分配器将更容易找到一个适合重用的数据块。为了实现这一点你可以将数据填充到相同的大小。或者你可以通过运行一个具有最大输入大小的模型来预热分配器。你可以在以下文章中了解更多关于 PyTorch 分配器修改的信息。[## CUDA 语义 - PyTorch 2.3 文档torch.cuda 的指南PyTorch 模块用于执行 CUDA 操作pytorch.org](https://pytorch.org/docs/stable/notes/cuda.html?refalexdremov.mesourcepost_page-----9c9d4899313d--------------------------------)整理分配历史我们希望使用所有可用的 GPU 内存——这使我们能够运行更大的批次并更快地处理数据。然而在某个时刻当增加批量大小时你将遇到CUDA 内存不足错误。是什么导致了这个错误为了调试这个问题我们可以查看分配器的内存历史。它可以通过 PyTorch 记录然后在pytorch.org/memory_viz上进行可视化。开始torch.cuda.memory._record_memory_history(max_entries100000)保存torch.cuda.memory._dump_snapshot(file_name)停止torch.cuda.memory._record_memory_history(enabledNone)可视化将显示如下内容https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/270f72428251b625a672af134cf2f454.png© 版权 2024, PyTorch |pytorch.org/blog/understanding-gpu-memory-1/x 轴表示时间y 轴表示总使用内存彩色块表示张量。因此它显示了张量何时被分配以及何时被释放。你可能会注意到窄尖峰——这些是占用大量空间的短暂张量。通过点击张量你可以获得该张量分配的位置。我们希望尽量减少这些尖峰因为它们限制了内存的高效使用。检查一下是什么导致了这个尖峰并考虑其他计算方式。除了尖峰外检测内存泄漏也很容易https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d257a3b5af410d7d0bb7b3e46ecda12d.png© 版权 2024, PyTorch |pytorch.org/blog/understanding-gpu-memory-1/正如你所看到的第一次前向传播后一些数据并没有被清除。通过点击这些模块你可以大致了解这些张量来自何处。图中显示的是训练步骤后梯度未被清除的情况这导致它们在前向传播过程中仍然存在限制了增加批量大小以适应更多数据的能力。[## 理解 GPU 内存 1可视化所有分配随时间变化在你使用 PyTorch 进行 GPU 计算的过程中你可能已经熟悉这个常见的错误信息pytorch.org加速模型并减少内存使用有什么比这更好的呢我们可以通过使用FlashAttention内核来计算点积注意力从而实现这一目标。[## GitHub - Dao-AILab/flash-attention快速且内存高效的精确注意力快速且内存高效的精确注意力。通过创建一个账户为 Dao-AILab/flash-attention 的开发做出贡献…github.com如果你还没有听说过它是一种计算精确点积注意力的方法无需显式构造注意力矩阵。它优化了 GPU 的输入输出操作从而提高了速度并且大幅度减少了内存消耗。简直没有理由不使用它。 不幸的是有一个理由不能使用它——硬件。Flash attention 仅在兼容硬件上使用fp16和bf16精度。这包括 NVIDIA Ampere、Hooper 等架构。其他库在底层使用了 flash attention因此你可以考虑使用其他更适合你代码库的变种。XFormers[## GitHub - facebookresearch/xformers可操作且优化过的 Transformer 构建模块支持…可操作且优化过的 Transformer 构建模块支持可组合构建。 - facebookresearch/xformersgithub.comTransformer Engine[## GitHub - NVIDIA/TransformerEngine一个加速 Transformer 模型在 NVIDIA GPU 上的运行的库…一个加速 Transformer 模型在 NVIDIA GPU 上运行的库包括使用 8 位浮点FP8精度…github.comPyTorch 本身这是事实PyTorch 的新版本可能会在适用的情况下使用闪电注意力flash attention。要激活此模式你需要在上下文管理器中执行注意力块指定使用哪种注意力策略## torch.nn.functional.scaled_dot_product_attention - PyTorch 2.3 文档阅读 PyTorch Domains 文档了解更多关于特定领域的库pytorch.org优化多 GPU 数据冗余 — FSDP如果你使用多个 GPU 进行训练基本的解决方案是使用DistributedDataParallel类。这样多个相同的进程将被启动并且在反向传播步骤中会聚合梯度。然而这并不是最优的问题在于当我们启动相同的进程时每个 GPU 上都有相同的模型和优化器状态这造成了冗余。解决方案是将数据分片。我们可以使用完全分片数据并行的 PyTorch 封装来实现这一点。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b542604a9a173f89aed4bc0375e2e515.png© 版权 2024PyTorch | https://pytorch.org/tutorials/intermediate/FSDP_tutorial.html它是如何工作的正如我所说当在多个 GPU 上训练时使用 DDP 时每个进程都有相同数据的副本。我们可以通过实现几个增强功能来优化这一过程分片优化器状态ZeRO 1在使用 DDP 训练时每个进程持有优化器状态的完整副本。而使用 ZeRO1 时我们将这些优化器状态在所有的 rank 之间进行分片使得每个 rank 只持有优化器状态的一部分。在反向传播过程中每个 rank 只需要收集与其参数相关的优化器状态来进行优化步骤。这种减少冗余的方式有助于节省内存。 在 Adam 中由于其参数大约是模型大小的两倍将优化器状态分片到 8 个 rank 中意味着每个 rank仅存储总状态大小的四分之一2/8。分片梯度ZeRO 2我们对优化器状态进行分片。现在我们将修改优化器步骤以便也对梯度进行分片。如果一个 rank 拥有部分参数的优化器状态那么我们将聚合与该 rank 持有状态相关的所有梯度计算优化步骤将部分参数的优化步骤发送到所有其他 rank正如你所注意到的现在每个 rank 不再需要持有完整的梯度副本。我们可以在梯度可用时立即将其发送到相关的 rank。因此我们可以进一步减少峰值内存消耗。分片模型参数ZeRO 3这将是史诗级的。为什么我们需要在每个 rank 上存储模型的完整副本让我们在所有 rank 之间分片模型参数。然后我们将在前向和反向传播过程中按需即时获取所需的参数。 对于大模型这些优化可以显著减少内存消耗。如何使用 FSDP其实非常简单。我们只需要用 FSDP 包装模型importtorchimporttorch.nnasnnimporttorch.optimasoptimfromtorch.distributed.fsdpimportFullyShardedDataParallelasFSDP modelFSDP(model)# its critical to get parameters from the wrapped model# as only a portion of them returned (sharded part)optimizeroptim.Adam(model.parameters())# consuct training as usualtrain(model,optimizer)你还可以指定 FSDP 的分片策略。例如我们可以选择SHARD_GRAD_OP策略以实现类似 ZeRO2 的行为。你可以在这里了解其他策略## FullyShardedDataParallel - PyTorch 2.3 文档用于在数据并行工作者之间分配分片模块参数的包装器。这个灵感来自于 Xu 等人以及……pytorch.org此外你还可以使用 FSDP 包装子模块。在上面的示例中只使用了一个 FSDP 模块这样会降低计算效率和内存效率。它的工作原理是假设你的模型包含 100 个 Linear 层。如果你执行 FSDP(model)那么将只有一个 FSDP 单元包装整个模型。在这种情况下allgather 会收集所有 100 个 Linear 层的完整参数因此不会为参数分片节省 CUDA 内存。你可以显式地包装子模块或定义自动包装策略。要了解更多关于 FSDP 的信息请阅读 PyTorch 指南[## 入门完全分片数据并行(FSDP) - PyTorch 教程 2.3.0cu121…注意在 GitHub 上查看和编辑本教程。大规模训练 AI 模型是一项具有挑战性的任务要求……pytorch.org使用torch.compile的神奇加速也就是说torch compile 只需启用它就可以让你的代码加速几个百分点。Torch 会跟踪你的执行图并尝试将其编译为高效的格式以便模型几乎可以在没有 Python 调用的情况下执行。基本使用方法是将模型与 compile 一起包装importtorch modeltorch.compile(model)这几乎会立即执行。实际的跟踪只会在第一次前向传播时发生。它还具有许多值得尝试的选项## torch.compile - PyTorch 2.3 文档使用 TorchDynamo 和指定的后端优化给定的模型/函数。具体来说对于在……pytorch.org Torch 编译器是一个重要特性将在后续的帖子中讲解敬请期待在这里了解更多关于 torch compile 的信息[## Introduction to torch.compile - PyTorch Tutorials 2.3.0cu121 documentationtorch.compile已包含在最新的 PyTorch 版本中。在 GPU 上运行 TorchInductor 需要 Triton而 Triton 已包含在…pytorch.org](https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html?refalexdremov.mesourcepost_page-----9c9d4899313d--------------------------------)结论本文并非包含所有解释而是提供了值得立即尝试的加速方法清单。希望对你有所帮助欢迎留言评论考虑订阅原文发表于https://alexdremov.me于 2024 年 5 月 28 日。图片来源于 PyTorch 博客这是 Linux 基金会的一个项目受 Linux 基金会的政策约束。所以所有图片均可按照创意共享 3.0 许可协议使用。*