3 月 19 日下午 2 点,锁定 NVIDIA AI 网络中文专场。立即注册观看
数据中心/云端

优化 CUDA C++ 编译时间

在现代软件开发中,时间是非常宝贵的资源,尤其是在编译过程中。对于在大规模 GPU 加速应用程序上使用 CUDA C++的开发者而言,优化编译时间可以显著提高工作效率并简化整个开发周期。

使用 nvcc 编译器进行离线编译时,高效的编译时间使您能够快速构建代码并保持势头。在使用 nvrtc 的即时 (JIT) 编译环境中,最小化编译时间有助于减少执行或运行时延迟,并提高应用程序性能。如果您在实时系统或交互式应用程序上工作,您将从尽可能快的编译时间中受益匪浅。

理解编译瓶颈的来源并不总是那么简单。CUDA 编译过程十分复杂,因为编译器会对代码执行各种优化和转换,几乎看不到代码的哪些部分需要很长时间才能编译。

例如,看似简单的代码行可能会触发复杂的模板实例化,从而导致其他模板的递归扩展,进而消耗过多的编译时间。如果不清楚幕后发生了什么,您就不知道编译时间较长的根本原因是什么,以及它是深度递归模板、特别大的头文件还是效率低下的代码模式。

CUDA 编译流程本身就很复杂。这不是一个单一的单一流程,而是一系列互联的子流程。例如,nvcc 可能会调用主机编译器、生成设备特定的代码或运行各种优化过程,每次都会延长总编译时间。

如果不能正确了解编译过程,就很难确定哪些子进程会导致漫长的编译时间。主机侧编译期间是否会出现速度减慢的情况?设备端优化是否会消耗过多的周期?或者,是否还有其他问题完全阻碍了这一过程?这种缺乏透明度的情况可能会让您束手无策,无法解决编译时瓶颈的根本原因。

鉴于 CUDA 编译管道的复杂性,您可以使用工具分析代码与编译器的交互方式。您需要一种方法来衡量代码对编译过程的性能影响,并确定哪些领域的优化时机已经成熟。这就是 CUDA 的 --fdevice-time-trace 功能为 CUDA 和 CUDA 来发挥作用的地方。

--fdevice-time-trace 是在 CUDA 12.8 中发布的工具,可直观呈现整个编译过程。它会生成各个编译阶段的详细时间线,让您能够清晰地了解所花费的时间。

无论是特别昂贵的模板实例化还是耗时的头文件,--fdevice-time-trace 都会详细说明该过程,并突出显示导致编译时间过长的确切区域。这种级别的可见性使您能够控制代码对编译器的影响,从而为更高效的构建和更快的开发周期铺平道路。

在本文中,我们将探讨此功能的工作原理、提供的见解,以及它如何通过识别和缓解编译时瓶颈来帮助您优化 CUDA 项目。

启用 --fdevice-time-trace 功能

要利用 CUDA C++编译器中的 --fdevice-time-trace 功能,启用该功能非常简单。对于 nvcc,可以使用以下命令激活该功能:

nvcc --fdevice-time-trace <output_filename>

在这种情况下,<output_filename> 是指编译完成时生成的文件的名称。这将生成遵循“Trace Event”(一种广泛认可的用于分析的格式) 格式的 .json 文件。可以打开 .json 追踪文件并在公共查看器中查看:

  • edge://tracing/
  • chrome://tracing/
  • Perfetto 界面

这些工具提供了编译各个阶段的可视化分解,使您能够逐步分析编译过程。图 1 展示了如何使用 chrome://tracing/ 打开 trace.json 文件。

The GIF shows an example of loading the trace file into a browser. Open a web  browser, type “about://tracing” into the browser window, and then click “Load”, which opens a file explorer where you can select your trace file.
图 1、 使用 about://tracing 在 Web 浏览器中加载追踪文件

为大型构建系统启用 --fdevice-time-trace

默认情况下,追踪文件涵盖对 nvcc 的一次调用。但是,在实际项目中,您可能需要在多个编译单元或多次调用 nvcc 的大型构建系统中捕获追踪。为此,nvcc 提供了为每个编译单元生成唯一追踪文件的功能:

nvcc --fdevice-time-trace=- <input_file>

使用此选项时,nvcc 会生成 .json 追踪文件,其基础名称与每次调用的输出文件相同,从而防止手动追踪文件重命名。如果重复使用输出文件名,nvcc 会覆盖追踪文件,以便每个追踪对应于一次编译器调用。

nvrtc 启用 --fdevice-time-trace

要使用 nvrtc 进行即时 (JIT) 编译,启用 --fdevice-time-trace 功能同样简单。调用 nvrtcCompileProgram 时,传递以下选项:

--fdevice-time-trace <output_filename>

nvcc 不同,nvrtc 不支持 --fdevice-time-trace=-。但是,它有一个特殊的优势:对于多次调用 nvrtcCompileProgram (在 JIT 上下文中很常见) 的程序,所有追踪文件都会自动附加。这使您能够在所有 nvrtcCompileProgram 调用中使用相同的 <output_filename> 值,并将所有火焰图形收集到单个报告中,从而提供整个 JIT 编译过程的综合视图。

此功能在运行时编译多个内核的复杂应用程序中特别有用,使您能够收集每个内核的详细性能见解,而无需手动管理多个 trace files。

用例

现在我们已经介绍了这项有助于分析编译流程的新功能,我们将展示一些有用的用例:

  • 可视化编译的端到端工作流程
  • 识别模板实例化瓶颈
  • 识别昂贵的头文件
  • 识别异常瓶颈

可视化编译的端到端工作流程

借助 --fdevice-time-trace 功能,您可以可视化编译过程的端到端工作流程,并提供所有主要阶段的全面时间表:

  • 预处理
  • 主机和设备代码编译
  • 设备链接
  • 二进制生成

这种整体视图对于了解不同阶段如何交互以及对总编译时间的影响尤为重要。通过检查此可视化,您可以确定主导工作流程的阶段,确定延迟是由特定代码结构还是编译器管道本身造成的,并确定需要优化的区域的优先级。

可视化工具还会显示编译器的多线程模式,例如 --threads--split-compile。图 2 和 3 显示了使用和不使用 nvcc0 时可视化的差异。

The screenshot shows a timeline with the various steps of compilation going from preprocessing, a CUDA frontend, cicc, ptxas, and fatbinary creation all the way to program linking. In this screenshot, the compute_100 code is built first, then the compute_90 code.
图 2. 端到端 nvcc 编译流
The screenshot shows a timeline with the various steps of compilation going from GCC preprocessing, the CUDA frontend, cicc, ptxas, and fatbinary creation all the way to program linking. However, with the --threads flag enabled, the compute_100 and compute_90 code is built in parallel.
图 3. 启用 --threads 端到端 nvcc 编译流

识别模板实例化瓶颈

模板元编程是一种强大的实践,可让您编写灵活且可重复使用的代码。Thrust 和 Cutlass 等热门 CUDA 项目通常使用模板来实现编译时的灵活性,使这些库能够适应各种数据类型、执行策略和硬件功能。

虽然这种灵活性可让您更轻松地编写高性能 GPU 代码,但也会产生成本。复杂的模板,尤其是具有递归或深度嵌套实例化的模板,可以显著增加编译时间。

由于编译器必须使用特定类型和参数对每个模板进行实例化,因此复杂的模板会增加编译时间,而这一过程可以级联为深度递归或分层模板的多个嵌套实例化。每个实例化都需要额外的编译器资源,因为它会为每个可能的变体评估和生成代码,最终会消耗大量时间和内存,尤其是在高度模板化的代码中。

图 4 显示了使用深度模板递归树的程序概要。可视化可以轻松识别递归树,并提供足够的信息供您识别源代码中存在问题的代码。然后,您可以重构代码以最小化模板复杂性,使用技术如使用 extern 模板避免冗余实例化、使用迭代方法替换深度递归模板、对常用模板进行显式实例化等。

The screenshot shows a compilation profile with a deep template recursion tree. Instantiating the template functions has many layers and consumes a significant part of the compilation time.
图 4、使用 --fdevice-time-trace 识别深度模板树

识别昂贵的头文件

对编译时间有显著影响的标头通常包含跨多个翻译单元的复杂模板或宏。这些标头可能会导致编译器重复工作,因为它会为每个包含内容处理相同的定义和实例化。

借助 --fdevice-time-trace 功能,您可以通过分析头文件事件的时间轴来识别需要大量处理时间的头文件。借助这种见解,您可以使用预编译头文件、减少不必要的包含项或模块化大型头文件等技术来优化构建流程,从而最大限度地减少冗余编译工作并提高整体性能。

识别异常瓶颈(Anomalous Bottlenecks)

除了模板和标头等外部因素之外,在编译过程的特定阶段,编译器本身也可能会出现瓶颈。

例如,编译单元的 NVVM 优化器、代码生成器、设备链接器或后端 PTX 优化可能会意外地消耗大量时间。如果不详细了解编译器的工作流程,通常很难检测到这些内部异常。

--fdevice-time-trace 功能提供了一个时间轴,可将编译器的执行分解为细化阶段,突出显示花费时间最多的区域。如果某个特定阶段 (例如 NVVM Optimizer 或 PTX 生成) 特别耗时,则表示有机会进一步研究。

这种透明度有助于您确定瓶颈是由于代码结构还是编译器中的特定行为造成的,从而实现更明智的优化策略。它还可以让您识别异常行为并向编译器团队 提交错误

图 5 展示了在 PTXAS 中优化一个内核时编译时间过度瓶颈的真实示例。这表明,PTXAS 在尝试优化 "_Z9NopKernelPi" 时花费了大量时间。此报告可帮助您了解潜在问题,并为您提供必要的见解,以便根据需要向工程团队提交详细的错误报告。

The screenshot shows a compilation profile with an anomalous bottleneck in ptxas.

图 5、使用 --fdevice-time-trace 识别异常瓶颈

结语

--fdevice-time-trace 功能代表着在提高开发者工作效率和优化 CUDA C++ 中的编译工作流方面迈出的重要一步。它提供有关模板实例化、头文件处理、内部编译器阶段和整个编译工作流程的详细见解,使您能够精确识别和解决瓶颈,并且可以成为开发过程中不可或缺的一部分。

我们鼓励您在项目中试用 --fdevice-time-trace,探索其潜力,并与社区分享您的反馈。您的反馈对于完善此功能并确保其满足全球 CUDA 开发者的需求至关重要。让我们携手合作,让 CUDA 开发更快、更高效!

 

 

标签