使用PyTorch Profiler进行模型性能分析,改善并加速PyTorch训练

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
实时计算 Flink 版,5000CU*H 3个月
实时数仓Hologres,5000CU*H 100GB 3个月
简介: 加速机器学习模型训练是工程师的关键需求。PyTorch Profiler提供了一种分析工具,用于测量CPU和CUDA时间,以及内存使用情况。通过在训练代码中嵌入分析器并使用tensorboard查看结果,工程师可以识别性能瓶颈。Profiler的`record_function`功能允许为特定操作命名,便于跟踪。优化策略包括使用FlashAttention或FSDP减少内存使用,以及通过torch.compile提升速度。监控CUDA内核执行和内存分配,尤其是避免频繁的cudaMalloc,能有效提升GPU效率。内存历史记录分析有助于检测内存泄漏和优化批处理大小。

如果所有机器学习工程师都想要一样东西,那就是更快的模型训练——也许在良好的测试指标之后

加速机器学习模型训练是所有机器学习工程师想要的一件事。更快的训练等于更快的实验,更快的产品迭代,还有最重要的一点需要更少的资源,也就是更省钱。

熟悉PyTorch Profiler

在进行任何优化之前,你必须了解代码的某些部分运行了多长时间。Pytorch profiler是一个用于分析训练的一体化工具。它可以记录:

CPU操作时间、CUDA内核计时、内存消耗历史

要记录事件,只需要将训练嵌入到分析器上下文中,如下所示:

 import torch.autograd.profiler as profiler

 with profiler.profile(
   activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
   on_trace_ready=torch.profiler.tensorboard_trace_handler('./logs'),
 ) as prof:
   train(args)

然后就可以启动tensorboard查看分析轨迹。如果这一步有问题,请查看是否安装了torch-tb-profiler。

Profiler有很多不同的选项,但最重要的是activities和profile_memory,一般情况下我们只需要这两个选项,因为启用的选项越少,开销就越小。

如果只想要分析CUDA内核执行时间,那么关闭CPU分析和所有其他功能也是可以的。因为在这种模式下,我们可以理解为显卡能力的真实评测。

为了方便分析,我们可以为每一步操作指定名称,例如

 with profiler.record_function("forward_pass"):
   result = model(**batch)

 with profiler.record_function("train_step"):
   step(**result)

或者增加更精细的自定义的标签,这里的名称将在跟踪中可见,我们就可以更简单的追踪想要的东西了。

 with profiler.record_function("transformer_layer:self_attention"):
   data = self.self_attention(**data)

 ...

 with profiler.record_function("transformer_layer:encoder_attention"):
   data = self.encoder_attention(**data, **encoder_data)

查看PyTorch Traces

收集完信息后,tensorboard显示是这样的

训练的过程一般包括:数据加载、前向传播、反向传播

反向传播由PyTorch在一个单独的线程中处理(上图中的线程16893),因此很容易识别,这部分门也控制不了,因为都是Pytorch根据我们的计算来自动进行的。(当然也可以自定义反向传播,但是这过于复杂,一般不建议自己实现)

首先看看数据加载:对于数据加载我们希望时间接近于零。

这是因为在数据加载过程中,GPU什么也不做,这会使可用资源利用率不足。并且在Pytorch的训练时数据处理可以与GPU计算重叠,因为它们是独立的部分,也就是说我们加载一个批次的时间只要与一个前向和一个反向传播的时间相近就可以了,这样就可以最大化的利用GPU的资源。

这里可以很容易地识别GPU空闲的区域-查看性能分析器跟踪中的GPU Est. SM效率和GPU利用率数字。没有活动的区域是我们的关注点,因为GPU什么都不做。

如果使用PyTorch DataLoader,则可以通过指定num_workers来多线程处理数据。如果您使用IterableDataset,则会更复杂,因为数据将被复制。这个问题可以通过使用get_worker_info()来解决,需要以某种方式调整迭代,以便每个worker接收不同的、不相交的行,所以这个比较麻烦,一般尽量避免IterableDataset。

内存分配器 memory allocator

当你在CUDA设备上使用PyTorch分配张量时,PyTorch将使用缓存分配器。这里是CUDA的执行机制:cudaMalloc和cudaFree的操作比较昂贵,我们要尽量避免。所以PyTorch会尝试重用以前通过cudaMalloc块分配的,如果PyTorch的分配器有一个合适的块可用,它会直接给出它,而不调用cudaMalloc。这样cudaMalloc只在开始时被调用。

但是如果你处理的是可变长度的数据(比如文本数据),不同的正向传播将需要不同大小的中间张量。因此,PyTorch的分配器可能没有适当的可用数据块。在这种情况下,分配器会调用cudaFree释放以前分配的块,为新的分配释放空间。

然后分配器再次开始构建它的缓存,进行大量的cudaMalloc,这是一个昂贵的操作,但是可以通过tensorboard分析器查看器的内存分析器部分来发现这个问题。

可以看到与分配器的保留内存相对应的红线不断变化。这意味着PyTorch分配器不能有效地处理分配请求。而当分配程序在没有频繁调用的情况下处理分配时,红线是完全笔直的,如下图所示:

我们如何解决呢?

第一件值得尝试的事情是设置PyTorch相对较新的分配器模式:

 PYTORCH_CUDA_ALLOC_CONF="expandable_segments:True"

这告诉PyTorch分配器分配可以在将来扩展的块。但是,如果大小变化太大,它仍然可能无法解决问题。

所以我们智能手动来进行优化,那就是是使数据形状一致。这样分配器就更容易找到合适的数据块进行重用。

比如最简单的将数据填充到相同的大小。或者可以通过运行具有最大输入大小的模型来预热分配器。

内存历史记录

我们想要最大化的使用所有可用的GPU内存——这让我们能够运行大量数据,并更快地处理数据。但是在某些时候,当增加批处理太大时,将遇到CUDA内存不足错误。是什么导致了这个错误?

为了调试它,我们可以查看分配器的内存历史记录。它可以通过PyTorch记录,然后在https://pytorch.org/memory_viz上可视化

  • Start:torch.cuda.memory._record_memory_history(max_entries=100000)
  • Save:torch.cuda.memory._dump_snapshot(file_name)
  • Stop:torch.cuda.memory._record_memory_history(enabled=None)

可视化会画出这样的东西:

x轴表示时间,y轴表示已使用的总内存,彩色块表示张量。它显示了张量何时被分配,何时被释放。

你可能会注意到狭窄的尖峰,这些是持续时间很短的张量,并且占据了很多空间。通过点击一个张量,可以得到这个张量被分配到哪里的信息。我们希望的就是最小化这些峰值,因为它们限制了有效的内存使用。检查导致这个峰值的原因,并考虑优化或者使用其他计算方法替代。

除了峰值之外,很容易检测到内存泄漏:

第一次运行之后的一些数据没有被清除,所以导致内存占用过高。通过点击块,可以知道这些张量是从哪里来的。在图像中,梯度在训练步骤之后没有被清除,因此它们在向前传递过程中处于无用状态,占用了宝贵的内存。

提高模型速度,减少内存使用

我们知道了原因,并且可以通过Profiler来找到瓶颈,那么我们可以通过什么方法来加速训练呢?

1、FlashAttention

首先可以使用FlashAttention来计算点积注意力来提高效率。如果你没有听说过它,它是一种计算精确的点积注意力的方法,并且不需要明确地构建注意力矩阵。这优化了GPU的io操作,提高了速度,也极大地减少了内存消耗。

但是FlashAttention仅适用于兼容硬件上的fp16和bf16精度。那就是NVIDIA Ampere, Hooper以上的GPU

当然也有其他的库可以替换,例如XFormers,和NV自己的Transformer Engine

新版本的PyTorch也内置了FlashAttention的支持,在文档中:

torch.backends.cuda.enable_flash_sdp()

: Globally enables or disables FlashAttention.

2、 FSDP 优化多gpu数据冗余

如果使用多个gpu来运行训练,基本的解决方案是使用DistributedDataParallel。生成了几个相同的进程,并且在反向传播期间聚合梯度。

当我们生成相同的进程时,在每个GPU上都有相同的模型和优化器状态,这是冗余的。可以通过跨数据分片来优化内存使用

当在多个gpu上进行训练时,每个进程在使用DDP进行训练时都有相同数据的精确副本。可以通过实现以下几个增强功能来优化它:

ZeRO 1 :分片优化器状态

当使用DDP进行训练时,每个进程都拥有优化器状态的完整副本。对于zer01,可以让每个rank只保留优化器状态的一部分。在反向传播期间,每个rank只需要收集与其参数相关的优化器状态来进行优化步骤。这种冗余的减少有助于节省内存。

💡在Adam的情况下,它保存的参数大约是模型大小的两倍,将优化器状态分片为8个rank意味着每个rank只存储总状态大小的四分之一(2/8)。

ZeRO 2:梯度分片

除对优化器状态进行分片外,还可以修改优化器步骤来切分梯度。我么可以

将所有与该rank持有的状态相关的梯度集合起来,计算优化步骤,然后将部分参数的优化步骤发送给所有其他rank

现在每个rank不需要保存一个完整的梯度副本,这样可以进一步降低峰值内存消耗。

ZeRO 3 :模型参数分片

我么不需要在每个rank上存储模型的完整副本,我们将在向前和向后期间及时获取所需的参数。在大型模型的情况下,这些优化可以显著降低内存消耗

如何使用FSDP?

其实很简单。我们所需要的就是用FSDP包裹模型:

 import torch
 import torch.nn as nn
 import torch.optim as optim
 from torch.distributed.fsdp import FullyShardedDataParallel as FSDP


 model = FSDP(model)

 # it's critical to get parameters from the wrapped model
 # as only a portion of them returned (sharded part)
 optimizer = optim.Adam(model.parameters())

 # consuct training as usual
 train(model, optimizer)

可以指定FSDP的分片策略。例如可以选择SHARD_GRAD_OP策略来实现与ZeRO2类似的行为。

3、torch.compile

这是最简单也是最直接的优化方式了,只要启用torch compile,它就可以将代码的速度提高几个百分点。

在Torch2.0中增加了compile方法,他会跟踪执行图,并尝试将其编译成一种有效的格式,以便几乎无需Python调用即可执行模型。

 import torch

 model = torch.compile(model)

也就是说,2.0以后只要你的模型能用compile那么就用compile吧。

总结

本文中介绍了使用PyTorch Profiler来查找运行瓶颈,并且介绍了一些简单的提速方法,虽然这篇文章没有完整的解释,但是里面提供的方法都是值得马上尝试方法,希望对大家有所帮助。

https://avoid.overfit.cn/post/95f7fa956805466db713e797d9d62e67

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
1月前
|
算法 PyTorch 算法框架/工具
Pytorch学习笔记(九):Pytorch模型的FLOPs、模型参数量等信息输出(torchstat、thop、ptflops、torchsummary)
本文介绍了如何使用torchstat、thop、ptflops和torchsummary等工具来计算Pytorch模型的FLOPs、模型参数量等信息。
153 2
|
11天前
|
监控 PyTorch 数据处理
通过pin_memory 优化 PyTorch 数据加载和传输:工作原理、使用场景与性能分析
在 PyTorch 中,`pin_memory` 是一个重要的设置,可以显著提高 CPU 与 GPU 之间的数据传输速度。当 `pin_memory=True` 时,数据会被固定在 CPU 的 RAM 中,从而加快传输到 GPU 的速度。这对于处理大规模数据集、实时推理和多 GPU 训练等任务尤为重要。本文详细探讨了 `pin_memory` 的作用、工作原理及最佳实践,帮助你优化数据加载和传输,提升模型性能。
41 4
通过pin_memory 优化 PyTorch 数据加载和传输:工作原理、使用场景与性能分析
|
1月前
|
机器学习/深度学习 自然语言处理 监控
利用 PyTorch Lightning 搭建一个文本分类模型
利用 PyTorch Lightning 搭建一个文本分类模型
55 8
利用 PyTorch Lightning 搭建一个文本分类模型
|
1月前
|
机器学习/深度学习 自然语言处理 数据建模
三种Transformer模型中的注意力机制介绍及Pytorch实现:从自注意力到因果自注意力
本文深入探讨了Transformer模型中的三种关键注意力机制:自注意力、交叉注意力和因果自注意力,这些机制是GPT-4、Llama等大型语言模型的核心。文章不仅讲解了理论概念,还通过Python和PyTorch从零开始实现这些机制,帮助读者深入理解其内部工作原理。自注意力机制通过整合上下文信息增强了输入嵌入,多头注意力则通过多个并行的注意力头捕捉不同类型的依赖关系。交叉注意力则允许模型在两个不同输入序列间传递信息,适用于机器翻译和图像描述等任务。因果自注意力确保模型在生成文本时仅考虑先前的上下文,适用于解码器风格的模型。通过本文的详细解析和代码实现,读者可以全面掌握这些机制的应用潜力。
51 3
三种Transformer模型中的注意力机制介绍及Pytorch实现:从自注意力到因果自注意力
|
2月前
|
并行计算 PyTorch 算法框架/工具
基于CUDA12.1+CUDNN8.9+PYTORCH2.3.1,实现自定义数据集训练
文章介绍了如何在CUDA 12.1、CUDNN 8.9和PyTorch 2.3.1环境下实现自定义数据集的训练,包括环境配置、预览结果和核心步骤,以及遇到问题的解决方法和参考链接。
106 4
基于CUDA12.1+CUDNN8.9+PYTORCH2.3.1,实现自定义数据集训练
|
2月前
|
机器学习/深度学习 PyTorch 调度
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
在深度学习中,学习率作为关键超参数对模型收敛速度和性能至关重要。传统方法采用统一学习率,但研究表明为不同层设置差异化学习率能显著提升性能。本文探讨了这一策略的理论基础及PyTorch实现方法,包括模型定义、参数分组、优化器配置及训练流程。通过示例展示了如何为ResNet18设置不同层的学习率,并介绍了渐进式解冻和层适应学习率等高级技巧,帮助研究者更好地优化模型训练。
131 4
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
|
2月前
|
机器学习/深度学习 监控 PyTorch
PyTorch 模型调试与故障排除指南
在深度学习领域,PyTorch 成为开发和训练神经网络的主要框架之一。本文为 PyTorch 开发者提供全面的调试指南,涵盖从基础概念到高级技术的内容。目标读者包括初学者、中级开发者和高级工程师。本文探讨常见问题及解决方案,帮助读者理解 PyTorch 的核心概念、掌握调试策略、识别性能瓶颈,并通过实际案例获得实践经验。无论是在构建简单神经网络还是复杂模型,本文都将提供宝贵的洞察和实用技巧,帮助开发者更高效地开发和优化 PyTorch 模型。
40 3
PyTorch 模型调试与故障排除指南
|
1月前
|
存储 并行计算 PyTorch
探索PyTorch:模型的定义和保存方法
探索PyTorch:模型的定义和保存方法
|
3月前
|
机器学习/深度学习 边缘计算 PyTorch
PyTorch 与边缘计算:将深度学习模型部署到嵌入式设备
【8月更文第29天】随着物联网技术的发展,越来越多的数据处理任务开始在边缘设备上执行,以减少网络延迟、降低带宽成本并提高隐私保护水平。PyTorch 是一个广泛使用的深度学习框架,它不仅支持高效的模型训练,还提供了多种工具帮助开发者将模型部署到边缘设备。本文将探讨如何将PyTorch模型高效地部署到嵌入式设备上,并通过一个具体的示例来展示整个流程。
489 1
|
3月前
|
机器学习/深度学习 并行计算 PyTorch
GPU 加速与 PyTorch:最大化硬件性能提升训练速度
【8月更文第29天】GPU(图形处理单元)因其并行计算能力而成为深度学习领域的重要组成部分。本文将介绍如何利用PyTorch来高效地利用GPU进行深度学习模型的训练,从而最大化训练速度。我们将讨论如何配置环境、选择合适的硬件、编写高效的代码以及利用高级特性来提高性能。
630 1