先进的机器学习编译器会执行一系列变换:高级图优化,如算子合并和布局改变;通过多面体方法进行的张量级循环重构;以及面向特定目标的L代码生成。虽然这些步骤对性能至关重要,但它们给分析带来了重要难题:我们如何将低级剖析器中观察到的性能指标(如GPU内核执行时间或CPU缓存未命中率)与在高级机器学习框架中定义的原始操作(例如PyTorch中的特定torch.nn.Conv2d层或TensorFlow中的tf.keras.layers.Dense)关联起来?没有这种关联,性能分析将变得令人困惑地抽象,使得我们难以确定原始模型的哪个部分造成了瓶颈,或者特定编译器优化对特定层的作用效果如何。
连接高低层信息的必要性
建立高级模型结构与低级编译执行之间的明确联系不可或缺,原因如下:
- 针对性瓶颈分析: NVIDIA Nsight Compute或Intel VTune等剖析器可能会显示,特定CUDA内核或特定CPU循环表现出低效(例如,低占用率、高内存延迟、指令停顿)。关联使我们能够确定是哪个原始层或操作(例如,第三个卷积层,某个矩阵乘法)生成了有问题的低级代码。这有效地引导了调试和优化工作。
- 评估编译器优化: 我们应用算子合并或激进分块等高级优化。关联有助于我们检查这些优化是否确实提升了目标操作的性能。例如,合并卷积和ReLU是否确实减少了模型图中特定实例的开销,还是无意中产生了效率较低的内核?
- 调试性能退化: 当模型代码更改或编译器/运行时更新时,性能可能会意外地下降。关联更改前后的剖析数据使我们能够快速找到哪些操作现在运行变慢,并找出根本原因,无论是特定层生成代码的变化还是执行调度调整。
建立关联的方法
机器学习编译器、运行时和剖析工具采用多种方法,通常结合使用,以维持框架层和编译代码之间的联系:
1. 源文件位置追踪
类似于传统编译中的调试信息(-g标志),机器学习编译器可以在中间表示(IR)降低的各个阶段传播关于原始源代码位置(文件、行号、函数名)的元数据。例如,MLIR中的一个操作可能带有指向Python堆栈帧的属性,该堆栈帧是相应的TensorFlow或PyTorch操作的定义位置。能够读取此元数据的剖析器可以直接将内核指标或执行事件与原始模型定义中的代码行关联起来。
- 工作原理: 前端(例如PyTorch-MLIR导入器)在追踪或脚本化模型时捕获源位置信息。此信息附加到最高层IR操作上。编译器通道旨在在操作转换、合并或降低时保留或正确更新此位置信息。后端代码生成器将此信息(如果可能且已启用)嵌入到最终可执行文件中或用于标注剖析数据。
- 局限: 激进的优化,尤其是算子合并,使之复杂化。一个单一的合并内核对应多个原始操作。调试信息可能指向其中一个原始操作或合并模式本身,需要仔细解释。
2. 语义命名约定
编译器通常将原始框架操作的语义信息嵌入到生成的函数、内核或IR节点的名称中。例如,由合并卷积、偏置加法和ReLU产生的内核可能被命名为fused_conv2d_bias_relu_nhwc_fp32_kernel_...。类似地,XLA HLO操作通常保留源自原始TensorFlow操作的名称。
- 工作原理: 命名方案在编译器内部建立。当操作被降低或合并时,以编程方式生成新名称,其中包含原始操作、数据类型、布局或唯一标识符的详细信息。
- 用途: 剖析器显示这些生成的名称。开发人员通常可以通过检查名称组成部分推断其来源。搜索剖析器输出或IR转储中与特定层(
layer3、output_dense)相关的名称,可以帮助找到相应的生成代码。
- 局限: 名称可能变得非常长且难以理解。大量合并可能导致通用名称(例如
fusion_kernel_123),如果不交叉引用编译器日志或IR转储,则提供很少直接信息。
3. 编译器和运行时事件标注
这是一种有效方法,编译器或运行时系统显式地将插桩调用插入到执行流程中。这些调用生成自定义事件或标记,通常与高级操作边界相关联,并被系统级剖析器捕获。
- 工作原理: 框架或运行时使用厂商专用或跨平台API(如NVIDIA GPU的NVTX、Intel API的ITT、AMD GPU的ROC Profiler标记或平台无关的追踪点)来发出事件。例如,在启动对应于
Conv2D层的编译内核之前,运行时可能会发出一个名为Conv2D_Layer3的“开始”事件,并在内核完成后发出一个“结束”事件。
- 用途: Nsight Systems、Perfetto或厂商专用追踪查看器等工具在时间轴上显示这些事件,同时显示CPU活动、GPU内核执行和内存传输。这提供了高级逻辑操作与底层硬件活动之间的直接视觉关联。
- 框架集成示例: PyTorch的
torch.profiler在NVIDIA GPU上运行时可以自动发出NVTX范围,使这种关联相对顺利。TensorFlow的剖析器与TensorBoard等工具集成以可视化类似的关联。
一个图示,说明框架操作(左)如何被编译成可能合并的内核(中),然后生成运行时事件(如NVTX范围)并执行在剖析器时间线(右)中可见的特定低级内核。这使得剖析器数据可以追溯回原始操作。
4. 中间表示 (IR) 转储
大多数机器学习编译器提供选项(例如环境变量或命令行标志)以在编译过程的各个阶段转储其内部IR(例如MLIR在特定通道前/后、XLA HLO、TVM TIR)。
- 工作原理: 通过启用这些转储,开发人员可以手动检查模型在转换过程中的表示。他们可以搜索与他们高级代码对应的操作名称或结构,并追踪它们如何被降低、合并或优化。
- 用途: 这主要是一种手动调试方法。它对于理解编译器如何精确地转换模型特定部分非常有价值,补充剖析器中可用的信息。例如,检查MLIR仿射方言表示可以显示为张量操作生成的循环结构,在最终代码生成之前。
- 局限: 需要理解编译器特定的IR。筛选潜在的大型IR转储可能耗时。
工具与实际关联
现代剖析工具套件通常集成专门用于帮助这种关联的功能:
- NVIDIA Nsight Systems: 擅长可视化系统级活动。其主要的关联机制是通过NVTX范围。当PyTorch或TensorFlow等框架(经过适当配置)发出NVTX事件时,Nsight Systems会在时间轴上显示这些命名范围,清楚地划分与高级操作对应的执行阶段。您可以直接查看启动了哪些GPU内核以及在特定框架操作范围内花费了多少CPU时间。
- TensorFlow Profiler & TensorBoard: 提供集成的剖析能力。它可以显示执行时间线,显示TensorFlow操作执行、相应的XLA HLO操作以及实际的设备内核执行(CPU或GPU)。它尝试关联这些阶段,通常依赖命名约定和内部追踪机制。
- PyTorch Profiler: 提供多种视图,包括与CUDA内核启动关联的算子分解(如果适用)。它可以显示每个算子花费的CPU和GPU时间,并可能追溯到调用操作的源代码行,利用命名和源位置追踪。
- Intel VTune Profiler & AMD uProf/ROCprof: 它们主要分别侧重于CPU和GPU内核分析,但如果框架或运行时发出运行时标注(如ITT或ROC Profiler标记),它们通常可以摄取或显示这些标注,为其各自硬件提供与Nsight Systems相似的时间线关联能力。
应对难题
虽然这些方法有效,但关联并非总是完美:
- 算子合并: 如前所述,一个单一的合并内核执行多个原始操作的工作。剖析器可能会将内核的开销与整个合并组关联(通过NVTX范围),或根据命名或调试信息将其归因于合并中的一个主要操作。理解融合内核中每个原始操作的贡献通常需要检查编译器的IR或报告。
- 异步执行: 运行时大量依赖异步执行(例如,在GPU流上启动内核而无需等待)。事件标注和时间线可视化在这里很重要,因为它们捕获操作的实际开始和结束时间,包括重叠。简单的顺序映射不符合实际。
- 动态性 (JIT/动态形状): 在JIT场景或处理动态张量形状时,生成和执行的确切代码可能在不同运行之间,甚至在单次运行内部有所不同。剖析捕获特定执行实例。关联机制需要处理这种变化,通常依赖于与剖析数据一同捕获的运行时信息。
有效关联框架操作与编译内核需要使用合适的工具并理解特定编译器和运行时栈采用的方法。利用语义命名、源位置传播,尤其是在系统剖析器中可视化的运行时事件标注,提供最明确的途径,将低级性能特征归因回高级模型代码,有助于明智的性能分析和优化。