【论文阅读】Retargeting and Respecializing GPU Workloads for Performance Portability

摘要

  为了接近峰值性能,像gpu这样的加速设备需要大量的特定于架构的调优,以了解共享内存、并行性、tensor core等的可用性。不幸的是,对更高性能和更低成本的追求导致了架构设计的显著多样化,甚至是产自同一供应商的产品也是如此。这就产生了跨不同gpu的性能可移植性需求,这对于具有特定体系结构的特定编程模型中的程序尤其重要。即使程序可以在不同的体系结构上无缝地执行,但它也可能遭受性能损失,因为它没有根据可用的硬件资源(如快速内存和寄存器)适当地调整大小,更不用说没有使用体系结构的新高级特性了。
  我们提出了一种新的方法,通过自动调整每个并行线程所做的工作量,以及它所需的内存和寄存器资源的数量,来提高现代机器上(遗留)CUDA程序的性能。通过在MLIR编译器基础架构内操作,我们还能够通过执行CUDA的自动转换来将AMD gpu作为转换目标,并同时调整程序粒度以适应目标gpu的大小
  结合平台特定编译器辅助的自动调优,我们的方法在Rodinia基准套件上比baseline CUDA实现提高了27%的geomean速度,并且在其他相似的NVIDIA平台上和AMD gpu上执行相同CUDA程序,其两个平台的性能相当.

本文为了解决两问题:

  1. 同一编程模型(同一平台),不同硬件架构或硬件参数,实现性能可移植。
  2. 不同编程模型(不同平台),实现代码翻译,性能可移植。

介绍

GPU 硬件的演变
CUDA 编程模型和语法随时间相对稳定,但底层 GPU 硬件已经显著发展,增加了许多新特性和指令。
性能可移植性问题
即使 CUDA 编写的 GPU 内核能够在更新的 NVIDIA GPU 上运行,它们也可能因为kernel尺寸与目标架构不匹配而无法达到类似的利用率。
编译器机制的提出
作者提出了一种基于编译器的机制,通过自动调整每个 GPU 线程的工作量以及内存和寄存器资源的使用量,来“调整”GPU 程序以适应特定架构。
MLIR 编译器基础设施
该工作基于 MLIR 编译器基础设施,利用 Polygeist 编译器将 CUDA 代码翻译成目标无关的表示,并进行自动调整。
性能提升和目标 GPU 的适配
结合编译器后端的自动调优,所提出的方法在 Rodinia 基准测试套件上有显著的性能提升,并在类似的 NVIDIA 和 AMD GPU 上执行相同的 CUDA 程序时实现了性能一致性。

利用现有的CUDA代码,编译器自动调节程序粒度、每个线程的工作量、以及内存和寄存器的数量,而无需更改编程模型。

  1. 将CUDA代码转成基于mlir的与目标无关的IR表示
  2. 使用thread 或者block的coarsening来控制每个线程的工作量和内存资源
  3. 能够自动地将原本为 NVIDIA GPU 编写的 CUDA 程序重新定位(retarget)或适配(adapt),使其能够在 AMD GPU 上运行。
  4. 最后对接到后端编译器,获取较低级别的信息(寄存器数量和溢出量),这使得在编译时自动调整过滤掉性能低下的程序,根据这个运行时间也可得到最优的粒度。

背景

  GPU编程模型和架构,主要讲解GPU的执行结构、合并访存、Occupancy、性能可移植;

合并访存:当warp中的线程执行load时,可以进行coalesced,如果满足某些要求(load size,strided,global offset),这将load到menory中执行更少的内存事务,从而减小最小的带宽。
Occupancy:SM上活动的线程 / SM所能驻留的最大线程数,其达到最大时未必是性能最高时,因为

  Polygeist/MLIR 中的 GPU 编译,主要讲解MLIR,Polygetist,thread coarsening

Polygetist:是一个 C++ 编译器和使用 MLIR 构建的扩展。引入了一种基于 MLIR 的 GPU barrier同步原语编译器抽象,可实现 GPU 到 CPU 的转译和barrier优化。

在这里插入图片描述

thread coarsening:增加每个线程处理数据的量,通过隐藏访存操作的延迟来提高GPU性能,有时会因为引入strided memory阻碍coalesced load或增加寄存器压力,从而降低 GPU occupancy。

Polygetist-GPU pipeline 概述

  Polygeist 以提供接受 CUDA 代码、执行优化和kernel粒度选择并针对 NVIDIA 和 AMDGPU 的端到端 GPU 编译器。我们将CPU端与GPU端代码的编译结合在一起,从而允许同时更新kernel配置和启动以及kernel代码本身。具体就是将 GPU 代码的 MLIR 表示内联到 CPU 代码中,且使用显式的并行循环表示(scf.parallel,不同于gpu.launch),可直接进行循环分析和优化。kernel将由特定于目标的程序进行概述和处理。通过 MLIR 中可用的pipeline来生成作为全局数据嵌入到 IR 中的特定于目标的二进制文件。最后,剩余的主机代码由特定于目标的pipeline进行处理,用各自GPU的runtime调用,替换掉gup_warppers,并且使用LLVM进一步降低为自包含的优化二进制。
在这里插入图片描述
在这里插入图片描述

嵌套并行循环的UNROLL-AND-INTERLEAVE

  研究嵌套并行循环的经典循环展开变换,同时处理 GPU 式的barrier synchronization。fig6为循环因子为2的for循环展开;for循环进行展开后,有语句运行的绝对先后顺序的限制,而并行循环只需要满足语句的相对先后顺序即可。
在这里插入图片描述
在这里插入图片描述

A.嵌套控制流:Unroll、Jam 和 Interleave

在这里插入图片描述

B.Synchronization

  现在考虑 Interleave 情况下的嵌套循环的主体,fig10所示。barrier 的变化需要进行转换,以保留 barrier 所保护语句的相对顺序。这个条件可以通过将来自不同迭代的语句副本分组在一起来满足,fig10 左侧所示。此外,最终变得连续的几个 barrier 可以轻松地用一个 barrier 替换。相反,如果 barrier 重复,例如,当不同的block运行不同次数的内循环迭代时,外循环的展开和交错可能会变得非法,fig10 右。因此,如果 GPU 块具有嵌套控制流,并且条件在编译时未知,则我们不会将转换应用于与 GPU 块相对应的并行循环。
在这里插入图片描述

C.多维循环

  CUDA 编程模型使程序员能够将指定block和grid可以划分为三个维度:x、y 和 z。因此,kernel中的并行循环是具有 3 个(或更少)维度的多并行。 同时也提出了应该在哪些维度执行thread and block coarsening的问题。其提供了两种方法来实现这一点。第一,可以指定多并行的总coarsening factor,并且 Polygeist-GPU 将尝试在不具有恒定大小1的维度上平衡因子(例如,对于总因子 16,我们将用 4、2 和 2 粗化 3 个维 度分别,而对于 6,我们将使用 3、2 和 1 进行粗化。). 另一种选择是显式指定 x、y 和 z 因子。

Coarsening 作为粒度变化

A.Thread Coarsening(线程粗化)

  定义:线程粗化是一种优化技术,通过增加每个线程处理的工作量来减少线程的数量。这意味着每个线程将执行更多的工作单元,从而减少总的线程数,但保持块(block)的大小不变
  目的:通过隐藏内存访问延迟和提高线程的占用率(occupancy),从而提高GPU内核的性能。

B.Block Coarsening(块粗化)

  定义:块粗化是另一种优化技术,通过增加每个块处理的工作量来减少块的数量。这通常涉及到减少网格(grid)的尺寸,但保持每个块中的线程数不变
  目的:通过减少同步操作和提高内存访问效率,以及更有效地利用GPU的执行资源,来提升性能。

C.Block and Thread Coarsening之间的权衡

  与线程粗化不同,块粗化结合了来自不同块的共享内存分配,增加了共享内存的使用。 这可能会提高那些未充分利用共享内存或带宽的kernel的性能,但在那些使用大量共享内存的kernel可能会降低性能,因为共享内存成为了主要的occupancy限制因素。请注意,这两种粗化都会增加每个线程的寄存器使用量,这是另一个occupancy限制因素,但无法在特定于平台的编译器之外直接控制。
  根据实现的不同,线程粗化可能会通过引入跨步访问模式(strided)而干扰kernel中最初存在的coalescing-friendly访问模式。块粗化保留了独立block中存在的内存访问模式。
  当粗化因子不是并行循环上限(loop upper)的除数时,会出现如何执行剩余迭代的额外问题。 当执行线程粗化时,剩余的迭代必须在同一块内执行,以保持块内同步。然而,执行剩余工作的额外线程会干扰在线程之间平衡工作负载,从而确保以完整的warp进行运行,并引入收敛分支执行的复杂性。由于这些原因,我们将线程粗化因子限制为仅上限的约数
  相反,在进行块粗化时, 我们生成一个完成块其余部分的工作的 epilogue kernel。因此,块粗化可以扩展到任何粗化因子,而不仅仅是上限的除数。由于线程粗化减少了每个块的线程数量,因此大的粗化因子或最初具有很少线程的block可能最终运行的线程数量少于warp的线程数。这将导致并行线程利用率不足和性能下降。类似地,块粗化减少了网格中块的数量,这可能会导致kernel的block数小于 SM,并导致相应的性能下降。
  我们的初步观察表明,kernel通常被设计为尽可能利用block级别的并行性,因此更多的并行性,从而可以在该级别找到粗化机会。 最后,可以将块粗化和线程粗化结合起来,以结合优点或减轻缺点。

在这里插入图片描述

这里主要分析了两种粗化的利弊,以及之间的权衡,包括了粗化对内存和寄存器的影响,以及粗化因子的选取

Alternative Code Path

  由于 Unroll 和 Interleave 转换是在相对较高的抽象级别上执行的,因此我们在中间表示中引入了替代区域(alternative regions)的概念, 以提供对编译时多版本控制的支持。然后,我们可以将具有不同因子的块和线程粗化应用于不同的区域,以便每个区域以不同的粒度进行相同的计算。这使我们能够推迟选择最佳替代方案,直到编译pipeline中较低级别的抽象提供足够详细的信息(例如,有关寄存器使用的信息)为止。否则,我们将不得不开发一个高抽象级别的性能模型,并致力于很大程度上不精确的启发式方法来选择最佳粗化因子。
  实际上,替代方案以新的 multi-region MLIR 操作表示(fig12)。它们是在我们的流程中通过简单地多次复制kernel body region,并对不同region应用不同因子的粗化而产生的。然后,编译pipeline可以正常进行,每个region分别进行optimized和lowered,直到达到以下决策点之一。
在这里插入图片描述

共享内存使用的早期剪枝:

  鉴于静态共享内存必须预先分配,大小在编译时已知,我们可以在生成替代方案后立即对其进行分析,以计算所使用的共享内存总量。此时可以丢弃所需要共享内存超过目标硬件上可用内存的替代方案

kernel统计:

  在并行循环表示中,我们可以使用闭式表达式收集循环中的算术和内存操作数量的信息,如果编译时循环边界未知,则使用符号表示。在LLVM表示中,我们还可以额外收集GPU控制流中的分支操作数量信息,众所周知,分支操作会负面影响性能,因为控制流发散可能表现为依次执行分支,同时将不相关的线程屏蔽掉。

这段话描述了编译过程中如何收集和分析GPU kernel代码的性能和相关信息,这里涉及到两个阶段的代码表示:

  1. 并行循环表示:在这个阶段,编译器分析并行循环并计算其中的算术和内存操作数量。如果循环的边界在编译时不能确定(即循环次数可能依赖于运行时的数据),则使用符号表达式来表示这些未知的边界。
  2. LLVM表示:在LLVM的中间表示阶段,除了继续分析算术和内存操作外,编译器还会关注控制流中的分支操作。分支操作可能会引起性能问题,因为它们可能导致线程执行路径的分歧。在GPU编程中,理想的状况是所有线程尽可能执行相同的指令路径,以避免资源浪费。如果分支操作导致某些线程执行了其他线程不需要执行的代码路径(即“不相关线程被屏蔽”),这可能会导致GPU执行单元的利用率下降,从而降低整体性能。

  最后,使用特定于平台的后端(例如 ptxas)将表示形式编译为二进制,为我们提供有关寄存器使用和溢出、估占用率等的信息。我们丢弃在此阶段产生新溢出的替代方案,因为 GPU 上的溢出会将数据放入本地内存(loaction memory)比寄存器慢几个数量级。

Timing-Driven Optimization (TDO) or 自动调优

  最后,通过初步过滤的替代方案将作为独立kernels包含在最终的二进制文件中。我们的编译器提供了一种"性能分析(profiling)"模式,在该模式下,编译器会生成额外的逻辑代码,这些代码能够允许在编译时或运行时选择不同的实现版本(alternative implementations)。每个替代方案都可以在不同的数据上执行一次或多次,以测量平均执行时间,然后选择性能最佳的一个。然后可以再次调用编译器以删除所有其他替代方案并提供单一版本,而无需额外调度。

实验

  Combining Block and Thread Coarsening:将两种coarsening结合使用,并与单独只进行一种coarsening进行比较。
在这里插入图片描述
在这里插入图片描述

  Comparison against a Mainstream CUDA Compiler:将Polygeist编译器生成的代码与主流CUDA编译器(clang)生成的代码进行性能比较。
在这里插入图片描述

  Translation to AMD: hipify+clang vs. Polygeist-GPU:使用编译器表示的CUDA代码在AMD gpu上运行的自动翻译,并将其与baseline的source-to-source方法进行比较。
在这里插入图片描述

实验结果

  1. 结合使用线程粗化和块粗化在多个内核上系统地优于仅使用其中一种粗化技术。
  2. Polygeist编译器在优化后相比于clang编译器在不同NVIDIA GPU上实现了17%至27%的性能提升。 在AMD
  3. GPU上,Polygeist编译器自动翻译的CUDA代码相比于hipify工具翻译的代码在性能上取得了16%至17%的提升。

实验结论

  1. 粗化技术能够根据目标硬件的特性调整程序的执行粒度,从而提高性能。
  2. Polygeist编译器证明了其在跨不同GPU架构(包括NVIDIA和AMD)的性能可移植性方面的优势。

最后附上链接

论文连接https://domke.gitlab.io/paper/ivan-retargeting-2024.pdf
Polygeist项目源码https://github.com/llvm/Polygeist

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瘦瘦无感

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值