Holistic Trace Analysis 简介#
创建日期:2024 年 1 月 2 日 | 最后更新:2024 年 1 月 5 日 | 最后验证:2024 年 11 月 5 日
作者: 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,了解它们在特定作业上如何花费时间至关重要。它们主要是在进行计算、通信、内存操作,还是处于空闲状态?时间分解功能提供了这三类耗时的详细分析。
空闲时间 (Idle time) - GPU 处于空闲状态。
计算时间 (Compute time) - GPU 正用于矩阵乘法或向量运算。
非计算时间 (Non-compute time) - 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 上没有运行内核时,它被视为处于空闲状态。我们开发了一种算法,将空闲时间分为三个不同的类别:
主机等待 (Host wait): 指 CPU 因入队内核速度不够快而无法保持 GPU 充分利用,从而导致的 GPU 空闲时间。这类低效问题可以通过检查导致减速的 CPU 算子、增加批大小 (batch size) 以及应用算子融合来解决。
内核等待 (Kernel wait): 指在 GPU 上连续启动内核时产生的短暂开销。归因于此类的空闲时间可以通过使用 CUDA Graph 优化来最小化。
其他等待 (Other wait): 此类别包含目前由于信息不足而无法归类的空闲时间。可能的原因包括使用 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 轴进行渲染。
内核分解#
内核分解功能可以细分每种内核类型(如通信 (COMM)、计算 (COMP) 和内存 (MEM))所花费的时间,并展示在所有 rank 中各类别所占的时间比例。以下是每种类别所占时间的饼图百分比:
内核分解可以按如下方式计算:
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 的每种内核类型的 Top 内核饼图。
针对每个 Top 内核和每种内核类型,在所有 rank 上的平均持续时间柱状图。
提示
所有图像均使用 plotly 生成。悬停在图表上会显示右上角的模式工具栏,允许用户进行缩放、平移、选择和下载图表。
上面的饼图显示了 Top 5 的计算、通信和内存内核。每个 rank 都会生成类似的饼图。可以通过传递给 get_gpu_kernel_breakdown 函数的 num_kernels 参数来配置饼图以显示 Top k 个内核。此外,duration_ratio 参数可用于调整需要分析的时间百分比。如果同时指定了 num_kernels 和 duration_ratio,则 num_kernels 具有优先权。
上面的柱状图显示了 NCCL AllReduce 内核在所有 rank 上的平均持续时间。黑色线条指示了每个 rank 上所花费的最小和最大时间。
警告
使用 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()
该函数返回一个包含每个 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()
生成的 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 进行跟踪差异分析。