全链路追踪分析简介
作者: 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
变量设置为跟踪文件的位置。
fromhta.trace_analysisimport 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 上没有内核运行时,就被视为空闲。我们开发了一种算法,将空闲时间分为三个不同的类别:
-
主机等待: 指的是由于 CPU 未能足够快地入队内核,导致 GPU 未被充分利用而产生的空闲时间。这类效率低下问题可以通过检查导致速度减慢的 CPU 操作符、增加批次大小以及应用操作符融合来解决。
-
内核等待: 指的是在 GPU 上连续启动内核时产生的短暂开销。通过使用 CUDA Graph 优化,可以最小化归因于此类的空闲时间。
-
其他等待: 此类包括由于信息不足而无法归因的空闲时间。可能的原因包括使用 CUDA 事件进行 CUDA 流之间的同步以及内核启动的延迟。
主机的等待时间可以解释为 GPU 因 CPU 而停滞的时间。为了将空闲时间归因于内核等待,我们使用以下启发式方法:
连续内核之间的间隔小于阈值
默认的阈值是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 闲置,从而降低效率。总而言之,更高的通信与计算重叠率是理想的。为了计算每个 rank 的重叠率,我们测量以下比率:
(计算时间 / 通信时间)
通信计算重叠可以按如下方式计算:
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工具进行追踪差异分析,请参阅使用Holistic Trace Analysis进行追踪差异分析。