评价此页

整体追踪分析 (Holistic Trace Analysis) 简介#

创建日期:2024年1月2日 | 最后更新:2024年1月5日 | 最后验证:2024年11月5日

作者: Anupam Bhatnagar

在本教程中,我们将演示如何使用整体追踪分析 (HTA) 来分析分布式训练作业的追踪数据。请按照以下步骤开始。

安装 HTA#

我们建议使用 Conda 环境来安装 HTA。有关安装 Anaconda 的说明,请参阅 Anaconda 官方文档

  1. 使用 pip 安装 HTA

    pip install HolisticTraceAnalysis
    
  2. (可选且推荐)设置 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()
../_images/temporal_breakdown_df.png

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

../_images/temporal_breakdown_plot.png

空闲时间分解#

深入了解 GPU 空闲的时长及其背后的原因,有助于指导优化策略。当 GPU 上没有运行内核时,它被认为是空闲的。我们开发了一种算法,将 空闲 时间归纳为三个不同的类别:

  • 主机等待 (Host wait): 指 CPU 将内核加入队列的速度不够快,无法使 GPU 保持充分利用而导致的 GPU 空闲时间。这类效率低下问题可以通过检查导致减速的 CPU 算子、增加批大小 (batch size) 以及应用算子融合 (operator fusion) 来解决。

  • 内核等待 (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()
../_images/idle_time_breakdown_percentage.png

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

../_images/idle_time.png

show_idle_interval_stats 设置为 True 时,会生成第二个数据框。它包含每个 Rank 上每个流的空闲时间汇总统计信息。

../_images/idle_time_summary.png

提示

默认情况下,空闲时间分解展示的是各类空闲时间所占的百分比。将 visualize_pctg 参数设置为 False,函数将以绝对时间作为 y 轴进行渲染。

内核分解#

内核分解功能可以对每种类型的内核(如通信 COMM、计算 COMP 和内存 MEM)所花费的时间进行分解,并呈现各类别在所有 Rank 上所占的时间比例。以下是按类别划分的时间百分比饼图:

../_images/kernel_type_breakdown.png

内核分解的计算方式如下:

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 上每个内核的计数、最小值、最大值、平均值、标准差、总和以及内核类型。

../_images/kernel_metrics_df.png

利用这些数据,HTA 可以创建多种可视化图形来识别性能瓶颈。

  1. 每个 Rank 各类内核的前几名内核的饼图。

  2. 对于各类前几名内核,所有 Rank 的平均持续时间条形图。

../_images/pie_charts.png

提示

所有图片均使用 plotly 生成。将鼠标悬停在图表上会显示右上角的模式栏,允许用户进行缩放、平移、选择和下载图表。

上面的饼图显示了前 5 个计算、通信和内存内核。类似的饼图会为每个 Rank 生成。可以通过传递给 get_gpu_kernel_breakdown 函数的 num_kernels 参数来配置饼图显示前 K 个内核。此外,duration_ratio 参数可用于调整需要分析的时间百分比。如果同时指定了 num_kernelsduration_ratio,则 num_kernels 优先。

../_images/comm_across_ranks.png

上面的条形图显示了 NCCL AllReduce 内核在所有 Rank 上的平均持续时间。黑线表示每个 Rank 上所花费的最短和最长时间。

警告

在使用 jupyter-lab 时,请将 “image_renderer” 参数值设置为 “jupyterlab”,否则图表将无法在笔记本中渲染。

有关此功能的详细演练,请参阅存储库 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 重叠百分比的数据框。

../_images/overlap_df.png

visualize 参数设置为 True 时,get_comm_comp_overlap 函数还会生成一个表示各 Rank 重叠情况的条形图。

../_images/overlap_plot.png

增强计数器#

内存带宽和队列长度计数器#

内存带宽计数器用于衡量在通过内存拷贝 (memcpy) 和内存设置 (memset) 事件进行 H2D、D2H 和 D2D 数据传输时所使用的内存拷贝带宽。HTA 还会计算每个 CUDA 流上未完成的操作数量,我们将其称为 队列长度 (queue length)。当流上的队列长度达到 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()

带有增强计数器的生成追踪文件的截图。 ../_images/mem_bandwidth_queue_length.png

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% 分位数。 ../_images/queue_length_summary.png

时间序列仅包含数值发生变化时的点。一旦观察到某个值,时间序列将保持恒定直到下一次更新。内存带宽和队列长度时间序列函数返回一个字典,其键为 Rank,值为该 Rank 的时间序列。默认情况下,时间序列仅为 Rank 0 计算。

CUDA 内核启动统计信息#

../_images/cuda_kernel_launch.png

对于在 GPU 上启动的每个事件,CPU 上都有一个相应的调度事件,例如 CudaLaunchKernelCudaMemcpyAsyncCudaMemsetAsync。这些事件在追踪中通过公共关联 ID 连接 - 参见上图。此功能计算 CPU 运行时事件、其对应的 GPU 内核以及启动延迟的持续时间(例如,GPU 内核开始时间与 CPU 算子结束时间之差)。内核启动信息生成方式如下:

analyzer = TraceAnalysis(trace_dir="/path/to/trace/dir")
kernel_info_df = analyzer.get_cuda_kernel_launch_stats()

下面是生成的数据框截图。 ../_images/cuda_kernel_launch_stats.png

CPU 算子、GPU 内核的持续时间和启动延迟允许我们发现以下内容:

  • 短 GPU 内核 (Short GPU kernels) - 持续时间小于相应 CPU 运行时事件的 GPU 内核。

  • 运行时事件异常值 (Runtime event outliers) - 持续时间过长的 CPU 运行时事件。

  • 启动延迟异常值 (Launch delay outliers) - 调度耗时过长的 GPU 内核。

HTA 为上述三个类别中的每一个生成分布图。

短 GPU 内核

通常,CPU 端的启动时间在 5-20 微秒范围内。在某些情况下,GPU 执行时间比启动时间本身还要短。下图帮助我们发现代码中此类实例发生的频率。 ../_images/short_gpu_kernels.png

运行时事件异常值

运行时异常值取决于用于分类异常值的截止值,因此 get_cuda_kernel_launch_stats API 提供了 runtime_cutoff 参数来配置该值。 ../_images/runtime_outliers.png

启动延迟异常值

启动延迟异常值取决于用于分类异常值的截止值,因此 get_cuda_kernel_launch_stats API 提供了 launch_delay_cutoff 参数来配置该值。 ../_images/launch_delay_outliers.png

结论#

在本教程中,您已经学习了如何安装和使用 HTA,这是一款能够帮助您分析分布式训练工作流中瓶颈的性能工具。要了解如何使用 HTA 工具执行追踪差异分析,请参阅 使用整体追踪分析进行追踪差异 (Trace Diff) 分析