Holistic Trace Analysis 简介¶
创建于:2024 年 1 月 2 日 | 最后更新:2024 年 1 月 5 日 | 最后验证:2024 年 11 月 05 日
作者: Anupam Bhatnagar
在本教程中,我们将演示如何使用 Holistic Trace Analysis (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 用于矩阵乘法或向量运算。
非计算时间 - GPU 用于通信或内存事件。
为了实现高训练效率,代码应最大化计算时间,并最小化空闲时间和非计算时间。以下函数生成一个数据帧,其中提供了每个 rank 的时间使用情况的详细分解。
analyzer = TraceAnalysis(trace_dir = "/path/to/trace/folder")
time_spent_df = analyzer.get_temporal_breakdown()

当 get_temporal_breakdown 函数中的 visualize
参数设置为 True
时,它还会生成一个条形图,表示按 rank 分解的情况。

空闲时间分解¶
深入了解 GPU 空闲时间量以及其背后的原因可以帮助指导优化策略。当 GPU 上没有内核运行时,它被认为是空闲的。我们开发了一种算法,将 Idle 时间分为三个不同的类别
主机等待: 指的是 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()

该函数返回一个数据帧元组。第一个数据帧包含每个 rank 的每个流上按类别划分的空闲时间。

当 show_idle_interval_stats
设置为 True
时,将生成第二个数据帧。它包含每个 rank 上每个流的空闲时间的摘要统计信息。

提示
默认情况下,空闲时间分解显示每个空闲时间类别的百分比。将 visualize_pctg
参数设置为 False
,该函数将在 y 轴上以绝对时间呈现。
内核分解¶
内核分解功能将所有 rank 的每种内核类型(例如,通信 (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 返回的第二个数据帧包含每个内核的持续时间摘要统计信息。特别是,这包括每个 rank 上每个内核的计数、最小值、最大值、平均值、标准差、总和和内核类型。

使用此数据,HTA 创建了许多可视化效果来识别性能瓶颈。
每个 rank 的每种内核类型的前几个内核的饼图。
每个内核类型的前几个内核的跨所有 rank 的平均持续时间的条形图。

提示
所有图像均使用 plotly 生成。将鼠标悬停在图表上会在右上角显示模式栏,用户可以通过它进行缩放、平移、选择和下载图表。
上面的饼图显示了前 5 个计算、通信和内存内核。为每个 rank 生成类似的饼图。可以使用传递给 get_gpu_kernel_breakdown 函数的 num_kernels
参数将饼图配置为显示前 k 个内核。此外,可以使用 duration_ratio
参数来调整需要分析的时间百分比。如果同时指定了 num_kernels
和 duration_ratio
,则 num_kernels
优先。

上面的条形图显示了所有 rank 中 NCCL AllReduce 内核的平均持续时间。黑线表示每个 rank 上花费的最短和最长时间。
警告
使用 jupyter-lab 时,将“image_renderer”参数值设置为“jupyterlab”,否则图表将不会在 notebook 中呈现。
有关此功能的详细演练,请参阅 repo 的 examples 文件夹中的 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()
该函数返回一个数据帧,其中包含每个 rank 的重叠百分比。

当 visualize
参数设置为 True 时,get_comm_comp_overlap 函数还会生成一个条形图,表示按 rank 划分的重叠。

增强计数器¶
内存带宽 & 队列长度计数器¶
内存带宽计数器测量内存复制带宽,该带宽用于通过内存复制 (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 个百分位数。

时序仅包含值发生更改的点。一旦观察到值,时序将保持不变,直到下一次更新。内存带宽和队列长度时序函数返回一个字典,其键是 rank,值是该 rank 的时序。默认情况下,仅计算 rank 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 内核 - 持续时间小于相应 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 进行跟踪差异分析。