深度学习框架PyTorch的全面追踪分析介绍¶
创建日期: 2024年1月2日 | 最后更新日期: 2024年1月5日 | 最后验证日期: 2024年11月5日
作者: Anupam Bhatnagar
在本教程中,我们将演示如何使用整体追踪分析(HTA)来分析分布式训练作业的追踪数据。开始之前,请按照以下步骤操作。
安装 HTA¶
我们建议使用 Conda 环境来安装 HTA。要安装 Anaconda,请参阅 官方 Anaconda 文档。
使用pip安装HTA:
pip install HolisticTraceAnalysis
(可选且推荐)设置一个 Conda 环境:
# create the environment env_name conda create -n env_name # activate the environment conda activate env_name # When you are done, deactivate the environment by running ``conda deactivate``
开始使用¶
启动一个 Jupyter notebook 并将 trace_dir 变量设置为轨迹的路径。
from hta.trace_analysis import TraceAnalysis
trace_dir = "/path/to/folder/with/traces"
analyzer = TraceAnalysis(trace_dir=trace_dir)
时间分解¶
为了有效利用GPU,了解特定任务中它们的时间分配至关重要。它们是主要进行计算、通信或内存操作,还是处于闲置状态?时间分解功能提供了对这三类操作所花费时间的详细分析。
空闲时间 - GPU 处于空闲状态。
计算时间 - 正在使用GPU进行矩阵乘法或向量运算。
非计算时间 - GPU 正在用于通信或内存事件。
为了实现高效的训练效果,代码应最大化计算时间,并最小化空闲时间和非计算时间。以下函数生成一个数据框,提供了每个节点在时间上的详细使用情况分解。
analyzer = TraceAnalysis(trace_dir = "/path/to/trace/folder")
time_spent_df = analyzer.get_temporal_breakdown()
当 visualize 参数设置为 True 时,在 get_temporal_breakdown 函数中还会生成一个按排名划分的条形图。
空闲时间分解¶
了解GPU空闲的时间量及其原因可以帮助指导优化策略。当GPU上没有运行内核时,它被认为是空闲的。我们开发了一种算法,将Idle时间分为三个不同的类别:
Host 等待:指的是由于CPU未能快速将内核加入队列而导致的GPU空闲时间,从而使GPU无法充分利用。可以通过检查导致速度减慢的CPU操作符、增加批量大小和应用操作符融合来解决这些类型的效率问题。
内核等待:这指的是在GPU上连续启动内核时产生的短暂开销。通过使用CUDA图优化,可以最小化此类别所分配的空闲时间。
其他等待时间: 此类别包括由于信息不足而目前无法归因的空闲时间。可能的原因包括使用CUDA事件同步CUDA流以及内核启动延迟。
主机等待时间可以解释为GPU因CPU原因而停滞的时间。为了将空闲时间归因于内核等待,我们采用以下启发式方法:
gap between consecutive kernels < threshold
默认的阈值为30纳秒,可以通过consecutive_kernel_delay参数进行配置。默认情况下,仅对排名为0的进程计算空闲时间分解。如果要计算其他排名的分解,请在get_idle_time_breakdown函数中使用ranks参数。以下是如何生成空闲时间分解:
analyzer = TraceAnalysis(trace_dir = "/path/to/trace/folder")
idle_time_df = analyzer.get_idle_time_breakdown()
该函数返回一个包含数据框的元组。第一个数据框按每个流和每个节点的类别显示空闲时间。
第二个数据框是在将 show_idle_interval_stats 设置为 True 时生成的。它包含每个流在每个进程上的空闲时间的汇总统计信息。
提示
默认情况下,空闲时间分解显示每个空闲时间类别的百分比。将参数 visualize_pctg 设置为 False,
函数将以绝对时间为 y 轴进行渲染。
内核分解¶
内核分解功能按类型拆分了在所有节点上花费的时间,例如通信(COMM)、计算(COMP)和内存(MEM),并展示了每类时间所占的比例。以下是每类时间所占比例的饼状图:
核函数分解可以按照以下方式计算:
analyzer = TraceAnalysis(trace_dir = "/path/to/trace/folder")
kernel_type_metrics_df, kernel_metrics_df = analyzer.get_gpu_kernel_breakdown()
该函数返回的第一个数据框包含生成饼状图所使用的原始值。
内核持续时间分布¶
`get_gpu_kernel_breakdown` 返回的第二个数据框包含每个内核的持续时间汇总统计信息。具体来说,这包括每个内核在每个进程上的计数、最小值、最大值、平均值、标准差、总和以及内核类型。
HTA 使用这些数据创建了许多可视化图表来识别性能瓶颈。
每个核类型在每个秩中的顶级核占比饼图。
每个顶级内核以及每种内核类型的平均持续时间柱状图,在所有等级上的表现。
提示
所有图像都是使用 plotly 生成的。将鼠标悬停在图表上会显示右上角的模式栏,用户可以通过它缩放、平移、选择和下载图表。
上面的饼图显示了计算、通信和内存内核中排名前5的内核。每个rank都会生成类似的饼图。可以通过将num_kernels参数传递给get_gpu_kernel_breakdown函数来配置饼图以显示前k个内核。此外,duration_ratio参数可用于调整需要分析的时间百分比。如果同时指定了num_kernels和duration_ratio,则num_kernels具有优先权。
上述柱状图显示了所有进程组中NCCL AllReduce内核的平均运行时间。黑色线条表示每个进程中最小和最大时间。
警告
在使用 JupyterLab 时,请将 “image_renderer” 参数值设置为 “jupyterlab”,否则图表将不会在笔记本中显示。
有关此功能的详细说明,请参阅存储库示例文件夹中的gpu_kernel_breakdown 笔记本。
通信计算重叠¶
在分布式训练中,大量时间被花费在GPU之间的通信和同步事件上。为了实现高GPU效率(例如TFLOPS/GPU),保持GPU持续处于计算核的过载状态至关重要。换句话说,GPU不应因未解决的数据依赖而被阻塞。衡量计算被数据依赖阻塞程度的一种方法是计算通信与计算的重叠度。如果通信事件与计算事件重叠,会观察到更高的GPU效率。缺乏通信与计算的重叠会导致GPU闲置,从而造成低效率。 总之,更高的通信与计算重叠度是有益的。为了计算每个进程的重叠百分比,我们测量以下比率:
(time spent in computation while communicating) / (time spent in communication)
通信计算重叠可以按照以下方式计算:
analyzer = TraceAnalysis(trace_dir = "/path/to/trace/folder")
overlap_df = analyzer.get_comm_comp_overlap()
该函数返回一个包含每个等级重叠百分比的数据框。
当参数 visualize 设置为 True 时,get_comm_comp_overlap 函数还会生成一个按排名表示重叠的条形图。
增强计数器¶
内存带宽与队列长度计数器¶
内存带宽计数器测量在使用内存复制(memcpy)和内存设置(memset)事件从H2D、D2H和D2D复制数据时使用的内存拷贝带宽。HTA还计算每个CUDA流上的未完成操作数量。我们将此称为队列长度。当流上的队列长度达到1024或更大时,新事件将无法调度到该流上,并且CPU将在GPU流上的事件处理完毕后才会继续执行。
generate_trace_with_counters API 会输出一个新的跟踪文件,其中包含内存带宽和队列长度计数器。新跟踪文件包含指示 memcpy/memset 操作使用的内存带宽的轨迹以及每个流的队列长度轨迹。默认情况下,这些计数器是使用排名为 0 的跟踪文件生成的,新文件的名称中包含后缀 _with_counters。用户可以选择通过在 generate_trace_with_counters API 中使用 ranks 参数来为多个排名生成计数器。
analyzer = TraceAnalysis(trace_dir = "/path/to/trace/folder")
analyzer.generate_trace_with_counters()
生成的跟踪文件截图,其中包含增强的计数器。
HTA 还提供了被分析代码部分的内存复制带宽和队列长度的摘要以及这些计数器的时间序列数据,通过以下 API 实现:
要查看摘要和时间序列,请使用:
# generate summary
mem_bw_summary = analyzer.get_memory_bw_summary()
queue_len_summary = analyzer.get_queue_length_summary()
# get time series
mem_bw_series = analyzer.get_memory_bw_time_series()
queue_len_series = analyzer.get_queue_length_series()
总结部分包含计数、最小值、最大值、平均值、标准差、第25百分位数、第50百分位数和第75百分位数。
时间序列仅包含值发生变化的点。一旦观察到一个值,时间序列就会保持不变,直到下一次更新。内存带宽和队列长度的时间序列函数返回一个字典,其中键是秩,值是该秩的时间序列。默认情况下,仅计算秩为0的时间序列。
CUDA内核启动统计信息¶
对于GPU上启动的每个事件,CPU上都有一个对应的调度事件,例如CudaLaunchKernel、CudaMemcpyAsync、CudaMemsetAsync。这些事件在跟踪中通过一个共同的相关性ID关联起来——请参见上方的图表。此功能计算CPU运行时事件的持续时间、其对应的GPU内核以及启动延迟,例如GPU内核开始时间和CPU操作符结束时间之间的差异。内核启动信息可以按如下方式生成:
analyzer = TraceAnalysis(trace_dir="/path/to/trace/dir")
kernel_info_df = analyzer.get_cuda_kernel_launch_stats()
生成的数据框截图如下。
CPU操作的持续时间、GPU内核的运行时间和启动延迟使我们能够找到以下内容:
短GPU内核 - GPU内核的持续时间小于相应的CPU运行事件。
运行时异常事件 - CPU 运行时事件持续时间过长。
延迟异常 - GPU内核调度时间过长。
HTA 为上述三种类别中的每一种生成分布图。
短GPU内核
通常,CPU侧的启动时间范围在5到20微秒之间。在某些情况下,GPU执行时间甚至低于启动时间本身。下面的图表帮助我们找到此类情况在代码中出现的频率。
运行时事件异常
运行时异常值取决于用于分类异常值的阈值,因此
get_cuda_kernel_launch_stats
API 提供了 runtime_cutoff 参数来配置该值。
延迟发射异常值
启动延迟异常值取决于用于分类异常值的阈值,因此 get_cuda_kernel_launch_stats API 提供了
launch_delay_cutoff 参数来配置该值。
结论¶
在本教程中,您已经学习了如何安装和使用 HTA, 一个性能工具,它使您能够分析分布式训练工作流中的瓶颈。 要了解如何使用 HTA 工具进行跟踪差异分析,请参阅 使用整体跟踪分析进行跟踪差异。