全面追踪分析简介
创建于: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笔记本,并将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函数中将visualize
参数设置为True
时,它还会生成一个表示按等级分解的条形图。

空闲时间分解
了解GPU空闲时间的长短及其背后的原因可以帮助指导优化策略。当没有内核在GPU上运行时,GPU被认为是空闲的。我们开发了一种算法,将空闲时间分为三个不同的类别:
主机等待: 指的是由于CPU未能足够快地排队内核以保持GPU充分利用而导致的GPU空闲时间。可以通过检查导致减速的CPU操作符、增加批量大小和应用操作符融合来解决这些类型的效率低下问题。
内核等待: 这是指在GPU上连续启动内核时产生的短暂开销。通过使用CUDA图优化,可以将归因于此类别的空闲时间最小化。
其他等待: 此类别包括由于信息不足而目前无法归因的空闲时间。可能的原因包括使用CUDA事件进行CUDA流之间的同步以及内核启动的延迟。
主机等待时间可以解释为由于CPU而导致GPU停滞的时间。为了将空闲时间归因于内核等待,我们使用以下启发式方法:
gap between consecutive kernels < threshold
默认的阈值是30纳秒,可以通过consecutive_kernel_delay
参数进行配置。默认情况下,空闲时间分解仅针对rank 0进行计算。为了计算其他rank的分解,请在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个计算、通信和内存内核。每个等级都会生成类似的饼图。可以通过传递给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。
通信计算重叠
在分布式训练中,大量的时间花费在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 操作使用的内存带宽的轨道以及每个流上队列长度的轨道。默认情况下,这些计数器是使用 rank 0 跟踪文件生成的,新文件的名称中包含后缀 _with_counters
。用户可以通过在 generate_trace_with_counters
API 中使用 ranks
参数来为多个 rank 生成计数器。
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()
生成的 dataframe 的截图如下所示。

CPU操作、GPU内核的持续时间以及启动延迟使我们能够找到以下内容:
短GPU内核 - 持续时间小于相应CPU运行时事件的GPU内核。
运行时事件异常 - 持续时间过长的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工具执行跟踪差异分析,请参阅使用整体跟踪分析进行跟踪差异分析。