全局追踪分析简介¶
Created On: Jan 02, 2024 | Last Updated: Jan 05, 2024 | Last Verified: Nov 05, 2024
作者: 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笔记本,并将``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()

当`get_temporal_breakdown <https://hta.readthedocs.io/en/latest/source/api/trace_analysis_api.html#hta.trace_analysis.TraceAnalysis.get_temporal_breakdown>`_函数中的``visualize``参数设置为``True``时,它还会生成一个按排名表示分解比例的条形图。

空闲时间分解¶
了解GPU空闲时间及其背后的原因,有助于制定优化策略。当GPU上没有内核运行时,我们认为它是空闲的。我们开发了一种算法,将`空闲`时间划分为三种不同类别:
**主机等待:**指由于CPU未能快速写入内核以保持GPU充分利用,导致的GPU空闲时间。这类低效可以通过检查导致减速的CPU操作、增加批量大小和应用操作融合来解决。
**内核等待:**这是指在GPU上启动连续内核时的短暂开销。可以通过使用CUDA图优化来尽量减少归因于该类别的空闲时间。
**其他等待:**此类别包括由于信息不足无法归因的空闲时间。可能的原因包括使用CUDA事件在CUDA流之间进行同步以及启动内核的延迟。
主机等待时间可解释为GPU因CPU的阻塞而停滞的时间。为了将空闲时间归因于内核等待,我们使用以下启发式方法:
连续内核之间的间隙 < 阈值
默认的阈值为30纳秒,可通过``consecutive_kernel_delay``参数进行配置。默认情况下,空闲时间分解仅针对排名0进行计算。为了计算其他排名的分解,请在`get_idle_time_breakdown <https://hta.readthedocs.io/en/latest/source/api/trace_analysis_api.html#hta.trace_analysis.TraceAnalysis.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 <https://hta.readthedocs.io/en/latest/source/api/trace_analysis_api.html#hta.trace_analysis.TraceAnalysis.get_gpu_kernel_breakdown>`_返回的第二个数据框包含每个内核的时长统计数据,特别是每个排名上的内核的计数、最小值、最大值、平均值、标准偏差、总和及内核类型。

使用这些数据,HTA生成许多可视化图表以识别性能瓶颈。
每个排名的每种内核类型的前几个内核的饼状图。
每种内核类型的平均时长条形图,跨越所有排名。

小技巧
所有图像均使用plotly生成。鼠标悬停在图表上可显示右上角的模式栏,允许用户缩放、平移、选择并下载图表。
上面的饼状图显示了前5个计算、通信和内存内核。对每个排名生成类似的饼状图。可以使用传递给`get_gpu_kernel_breakdown`函数的``num_kernels``参数配置饼状图以显示前k个内核。此外,还可以使用``duration_ratio``参数调整需要分析的时间比例。如果同时指定``num_kernels``和``duration_ratio``,则``num_kernels``优先。

上方的条形图显示了跨所有排名的NCCL AllReduce内核的平均时长。黑线表示每个排名上的最小和最大时间。
警告
在使用jupyter-lab时,将``image_renderer``参数值设置为``jupyterlab``,否则图表无法在笔记本中渲染。
有关此功能的详细讲解,请参阅仓库示例文件夹中的`gpu_kernel_breakdown notebook <https://github.com/facebookresearch/HolisticTraceAnalysis/blob/main/examples/kernel_breakdown_demo.ipynb>`_。
通信与计算重叠¶
在分布式训练中,GPU之间的通信和同步事件占据了大量时间。为了实现高效的GPU效率(如每GPU TFLOPS),关键是让GPU充分利用计算内核换挡。换句话说,GPU不应因未解析的数据依赖关系而被阻塞。评估计算是否因数据依赖关系而受阻的一种方法是计算通信和计算的重叠。通信事件与计算事件的重叠越高,GPU效率越高。缺乏通信和计算的重叠会导致GPU空闲,从而降低效率。总而言之,较高的通信计算重叠是理想的。为了计算每个排名的重叠百分比,我们测量以下比率:
(进行通信的同时用于计算的时间) / (通信花费的时间)
可以按以下方式计算通信计算重叠:
analyzer = TraceAnalysis(trace_dir = "/path/to/trace/folder")
overlap_df = analyzer.get_comm_comp_overlap()
该函数返回一个数据框,包含每个排名的重叠百分比。

当参数``visualize``设置为True时,函数`get_comm_comp_overlap <https://hta.readthedocs.io/en/latest/source/api/trace_analysis_api.html#hta.trace_analysis.TraceAnalysis.get_comm_comp_overlap>`_还会生成一张条形图,按排名表示重叠程度。

增强计数器¶
内存带宽与队列长度计数器¶
内存带宽计数器测量了将数据从H2D、D2H和D2D复制时内存拷贝(memcpy)和内存置零(memset)事件的内存拷贝带宽。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 <https://hta.readthedocs.io/en/latest/source/api/trace_analysis_api.html#hta.trace_analysis.TraceAnalysis.get_cuda_kernel_launch_stats>`_ API提供了``runtime_cutoff``参数用于配置该值。

启动延迟异常值
启动延迟异常值取决于用于分类异常值的阈值,因此`get_cuda_kernel_launch_stats` API提供了``launch_delay_cutoff``参数用于配置该值。

总结¶
在本教程中,您已经学习到如何安装和使用HTA,这是一种性能工具,可以用于分析分布式训练工作流程中的瓶颈。要了解如何使用HTA工具进行追踪差异分析,请参见`使用整体追踪分析进行追踪差异 <https://pytorch.org/tutorials/beginner/hta_trace_diff_tutorial.html>`__。