AMD CDNA™2 GPU 中的寄存器压力

news2024/12/23 0:37:48

Register pressure in AMD CDNA™2 GPUs — ROCm Blogs

注意: 此博客以前是  AMD实验室笔记 博客系列的一部分。

GPU kernel 中的寄存器压力对高性能计算 (HPC) 应用程序的整体性能有着巨大的影响。理解和控制寄存器的使用可以让开发者精心设计代码,以最大化硬件资源。下面的博客文章专注于一个实际演示,展示如何应用2022年8月23日的 OLCF培训讲座 中解释的建议。这里是 培训存档,你也可以在那里找到幻灯片。我们只专注于使用 ROCm 5.4 的 AMD CDNA™2 架构 (MI200系列GPU)。

寄存器和占用率

通用寄存器是传统处理器中最快三的内存类型。在大多数情况下,传统处理器和加速器(如GPU)中的算术逻辑单元(ALU)是唯一可以直接访问寄存器的组件。不幸的是,寄存器是一种稀缺且昂贵的资源,编译器会尽力“优化”局部变量分配到硬件寄存器以供ALU操作时使用。

当我们使用“优化”一词时,我们总是要澄清优化过程的目标。实际上,常规CPU和加速器(如GPU)由于其本质不同,它们执行程序和实现高性能的方法也不同。一方面,传统CPU是面向延迟设计的,旨在尽可能多地执行单个串行线程的指令。另一方面,GPU是面向吞吐量设计的,旨在尽可能利用独立线程之间的并行性。

在AMD GPU中,在同一个计算单元(CU)上运行的高数量并发波前(wavefront)使得GPU能够隐藏全局内存访问时间,比执行计算操作所需的时间更长,而此时其他波前正在执行操作。

“占用率”这个术语表示在同一个CU上能同时运行的最大波前数量。一般来说,较高的占用率有助于通过其他操作隐藏昂贵的内存访问操作,从而实现更好的性能,但这并不是总能如此。

在图1中,我们展示了CDNA™2架构中CU的示意图。矢量通用寄存器(VGPRs)用于存储在波前中各个工作项不同的数据,即每个工作项的数据是不同的。它们是CU中最通用的寄存器,可以直接由矢量ALU(VALU)操作。VALU负责执行CU中的大部分工作,包括浮点运算(FLOPs)、内存加载、整数和逻辑操作等。

标量通用寄存器(SGPRs)代表一组用于存储在编译时已知的波前中各工作项相同数据的寄存器。SGPRs由标量ALU(SALU)操作,而SALU与VALU不同,只能用于有限的操作集,如整数和逻辑操作。

本地数据共享(LDS)是一种快速的CU上管理的内存,可以在块内所有工作项之间高效共享数据。

../../_images/gcn_compute_unit.png

图1:CDNA™2架构中CU的示意图

理想情况下,我们希望始终有尽可能高的占用率。实际上,占用率受硬件设计选择和由运行在卡上的内核(如HIP、OpenCL等)所决定的资源限制。例如,每个基于AMD CDNA™2的GPU的CU有四组波前缓冲区,每个执行单元(EU,也称为图1中的SIMD单元)一个波前缓冲区,每个CU有四个EU。每个EU最多可以管理*八个*波前。这意味着CDNA™2中的物理占用率上限是每个CU 32个波前。

内核所需的寄存器数量是最常见的占用率限制因素之一。另一个常见的限制因素是LDS。以下表格总结了在AMD CDNA™2基于的GPU上,作为内核使用的VGPRs数量的函数,可以达到的最大占用率。

../../_images/occupancy_vgpr.JPG

表1:MI200中与VGPRs使用相关的占用率

寄存器溢出

寄存器分配是将GPU内核的局部变量和表达式结果分配到硬件可用寄存器的过程。它由编译器在编译时进行,并受到指令调度等其他阶段的影响。找到该问题的最优解决方案是NP难题,因此必须采用启发式技术在合理时间内找到接近最优的解决方案。

编译器试图通过减少寄存器的需求来应用启发式技术以最大化占用率,遵循表1。当请求的寄存器数量变得过高时,性能会因“寄存器压力”而受到惩罚,这导致较低的占用率和临时存储器的使用。

有时,编译器可能认为,即使请求的寄存器数量超过表1中报告的限制,达到更高的占用率也是有益的。例如,应用程序需要134个寄存器,但编译器仅分配128个,其余的放在临时存储器中。通过在临时存储器中保存一些变量,可以实现更高的占用率:这是一个线程私有的局部存储器,由全局存储器支持,速度比寄存器存储器慢得多。这种技术被称为“寄存器溢出”。

虽然观察到变量被分配到临时存储器可能是高寄存器压力的前兆,但应该从更广泛的性能背景中考虑这一点。事实上,通过保存少量寄存器来实现更高的占用率,与没有任何临时存储器使用但占用率较低相比,可以提供显著的性能提升。

在请求的寄存器数远高于硬件可用寄存器数的情况下,性能将在低占用率(最坏情况下每个计算单元1个波前)和高成本访问被“溢出”到临时存储器的寄存器变量之间受到影响。 

如何减少寄存器压力

如前所述,编译器通过采用启发式技术来最大化占用率,从而最小化某些GPU内核所需的寄存器数量。这些启发式技术有时未能接近最优解决方案,程序员需要重新构建代码以减少寄存器压力并提高性能。

在本节中,我们将介绍如何识别寄存器压力问题以及如何缓解它。

首先,可以通过两种方式检测GPU内核使用的寄存器数量:1) 使用 -Rpass-analyze=kernel-resource-usage 标志编译包含内核的文件,该标志将在编译时打印每个内核的资源使用情况;其中包括SGPRs(标量通用寄存器)、VGPRs(矢量通用寄存器)、ScratchSize(临时内存大小)、VGPR/SGPR溢出、占用率和LDS(本地数据共享)使用情况。2) 使用 --save-temps 进行编译,并检查 hip-amdgcn-amd-amdhsa-gfx90a.s 文件中的 .vgpr_spill_count。`-Rpass-analyze=kernel-resource-usage` 标志报告的所有信息也可以在这个文件中找到。

一旦评估/确认了寄存器压力情况,可以将以下几种技术应用于代码以减少寄存器压力。

  1. 为每个内核设置 __launch_bounds__ 限定符。默认情况下,编译器假设每个内核的块大小为1024个工作项。定义 __launch_bounds__ 后,编译器可以适当地分配寄存器,从而潜在地降低寄存器压力。

  2. 将变量定义/赋值移动到接近它们使用的位置。在GPU内核顶部定义一个或多个变量,并在底部使用它们,这会迫使编译器将这些变量存储在寄存器或临时内存中,直到它们被使用,从而影响使用这些寄存器处理更多性能关键变量的可能性。将定义/赋值移动到它们第一次使用的位置将有助于启发式技术对其余代码做出更有效的选择。

  3. 避免在堆栈上分配数据。在堆栈上分配的内存,例如 double array[10],默认情况下会存在于临时内存中,编译器可能会将其优化存储到寄存器中。如果你的应用程序使用在堆栈上分配的内存,看到临时内存的使用不应感到惊讶。

  4. 避免将大对象作为内核参数传递。函数参数在堆栈上分配,可能作为优化存储到寄存器中。有时,将这些参数存储为 constant 可能会有所帮助。

  5. 避免编写包含许多函数调用(包括数学函数和断言)的大型内核。当前,编译器总是内联设备函数,包括数学函数和断言。拥有许多这些函数调用会引入额外的代码和潜在的更高寄存器压力。例如,将 pow(var,2.0) 替换为简单的 var*var 可以显著减少寄存器压力。

  6. 控制循环展开。可以通过在编译时已知迭代次数的循环上添加 #pragma unroll 命令来实现循环展开。通过这样做,所有迭代都将完全展开,从而减少检查循环退出条件的开销。然而,循环展开会增加寄存器压力,因为需要同时存储更多变量。在寄存器压力成为问题的情况下,应限制使用循环展开。请注意,Clang编译器在展开循环方面往往比其他编译器更加字面化。

  7. 手动溢出到LDS。作为最后的手段,可以使用一些LDS内存手动存储变量,可能是那些生命周期最长的变量,从而为每个线程节省几个寄存器。

示例

接下来,我们将重点讨论以下代码:

__global__ void kernel (double *phi, double *laplacian_phi,
      double *grad_phi_x, double *grad_phi_y, double *grad_phi_z,
      double *f0, double *f1, double *f2, double *f3, double *f4,
      double *f5, double *f6,
      double *g0, double *g1, double *g2, double *g3, double *g4,
      double *g5, double *g6, double* g7, double *g8, double *g9,
      double *g10, double *g11, double *g12, double *g13, double *g14,
      double *g15, double *g16, double *g17, double *g18,
      int nx, int ny, int nz, int ldx, int ldy, int current, int next,
      double k, double alpha, double phi2, double gamma,
      double itauphi, double itauphi1, double ieta,
      double itaurho, double grav,
      double eg1, double eg2, double eg0, double egc0, double egc1, double egc2)
{
  int i = (threadIdx.x + blockIdx.x * blockDim.x);
  int j = (threadIdx.y + blockIdx.y * blockDim.y);
  int z = (threadIdx.z + blockIdx.z * blockDim.z);

  int m, current_pos;

  double mu_phi, current_phi, current_phi_2;
  double rho;
  double fx, fy, fz;
  double uf, ux, uy, uz, v;
  double af, ag, cf;
  double eg1ag, eg2ag, eg1rho, eg2rho;
  double tmp1, tmp2;

  if(i <= nx && j <= ny && z <= nz)
    {
      m = i + ldx * (j + ldy * z);
      current_pos = m + current;

      current_phi = phi[m];
      current_phi_2 = pow(current_phi,2.0);

      rho = g0[m] + g1[current_pos] + g2[current_pos] + g3[current_pos] + g4[current_pos] +
  g5[current_pos] + g6[current_pos] + g7[current_pos] + g8[current_pos]  + g9[current_pos] +
  g10[current_pos] + g11[current_pos] + g12[current_pos] + g13[current_pos] + g14[current_pos] +
  g15[current_pos] + g16[current_pos] + g17[current_pos] + g18[current_pos];

      mu_phi = alpha * current_phi * ( current_phi_2 - phi2 ) - k * laplacian_phi[m];

      fx = mu_phi * grad_phi_x[m];
      fy = mu_phi * grad_phi_y[m];
      fz = mu_phi * grad_phi_z[m];

      ux = ( g1[current_pos] - g2[current_pos] + g7[current_pos] - g8[current_pos] + g9[current_pos] -
       g10[current_pos] + g11[current_pos] - g12[current_pos] + g13[current_pos] - g14[current_pos] +
       0.50 * fx ) * 1.0/rho;
      uy = ( g3[current_pos] - g4[current_pos] + g7[current_pos] - g8[current_pos] - g9[current_pos] +
       g10[current_pos] + g15[current_pos] - g16[current_pos] + g17[current_pos] - g18[current_pos] +
       0.50 * fy ) * 1.0/rho;
      uz = ( g5[current_pos] - g6[current_pos] + g11[current_pos] - g12[current_pos] - g13[current_pos] +
       g14[current_pos] + g15[current_pos] - g16[current_pos] - g17[current_pos] + g18[current_pos] +
       0.50 * fz ) * 1.0/rho;

      af = 0.50 * gamma * mu_phi * itauphi;
      cf = itauphi * ieta * current_phi;

      f0[m] = itauphi1 * f0[m] + -3.0 * gamma * mu_phi * itauphi + itauphi * current_phi;

      f1[current_pos] = itauphi1 * f1[current_pos] + af + cf * ux;
      f2[current_pos] = itauphi1 * f2[current_pos] + af - cf * ux;
      f3[current_pos] = itauphi1 * f3[current_pos] + af + cf * uy;
      f4[current_pos] = itauphi1 * f4[current_pos] + af - cf * uy;
      f5[current_pos] = itauphi1 * f5[current_pos] + af + cf * uz;
      f6[current_pos] = itauphi1 * f6[current_pos] + af - cf * uz;

      ag  = 3.0 * current_phi * mu_phi + rho;
      eg1ag = eg1 * ag;
      eg2ag = eg2 * ag;
      eg1rho = eg1 * rho;
      eg2rho = eg2 * rho;
      v  = 1.50 * ( ux*ux + uy*uy + uz*uz );
      uf = ux * fx + uy * fy + uz * fz;

      g0[m] = itaurho * g0[m] + eg0 * ( (rho - 6.0 * current_phi * mu_phi) - rho*v ) - egc0*uf;

      tmp1 = eg1ag + eg1rho*( 0.50*ux*ux - v ) + egc1*( ux*fx - uf );
      tmp2 = eg1rho*ux + egc1*fx;

      g1[m+next + 1] = itaurho * g1[current_pos] + tmp1 + tmp2;
      g2[m+next - 1] = itaurho * g2[current_pos] + tmp1 - tmp2;

      tmp1 = eg1ag + eg1rho*( 0.50 * uy * uy - v ) + egc1 * ( uy * fy - uf );
      tmp2 = eg1rho * uy + egc1 * fy;

      g3[m+next + ldx] = itaurho * g3[current_pos] + tmp1 + tmp2;
      g4[m+next - ldx] = itaurho * g4[current_pos] + tmp1 - tmp2;

      tmp1 = eg1ag + eg1rho*( 0.50 * uz * uz - v ) + egc1 * ( uz * fz - uf );
      tmp2 = eg1rho * uz + egc1 * fz;

      g5[m+next + ldx*ldy] = itaurho * g5[current_pos] + tmp1 + tmp2;
      g6[m+next - ldx*ldy] = itaurho * g6[current_pos] + tmp1 - tmp2;

      tmp1 = eg2ag + eg2rho * ( 0.50 * ( ux + uy ) * ( ux + uy ) - v ) +
  egc2 * ( ( ux + uy ) * ( fx + fy ) - uf );

      tmp2 = eg2rho * ( ux + uy ) + egc2 * ( fx + fy );

      g7[m+next + 1 + ldx] = itaurho * g7[current_pos] + tmp1 + tmp2;
      g8[m+next - 1 - ldx] = itaurho * g8[current_pos] + tmp1 - tmp2;

      tmp1 = eg2ag + eg2rho * ( 0.50 * ( ux - uy ) * ( ux - uy ) - v ) +
  egc2 * ( ( ux - uy )*( fx - fy ) - uf );
      tmp2 = eg2rho * ( ux - uy ) + egc2 * ( fx - fy );

      g9[m+next + 1 - ldx]  = itaurho * g9[current_pos]  + tmp1 + tmp2;
      g10[m+next - 1 + ldx] = itaurho * g10[current_pos] + tmp1 - tmp2;

      tmp1 = eg2ag + eg2rho * ( 0.50 * ( ux + uz ) * ( ux + uz ) - v ) +
  egc2 * ( ( ux + uz ) * ( fx + fz ) - uf );
      tmp2 = eg2rho * ( ux + uz ) + egc2 * ( fx + fz );

      g11[m+next + 1 + ldx*ldy] = itaurho * g11[current_pos] + tmp1 + tmp2;
      g12[m+next - 1 - ldx*ldy] = itaurho * g12[current_pos] + tmp1 - tmp2;

      tmp1 = eg2ag + eg2rho * ( 0.50 * ( ux - uz ) * ( ux - uz ) - v ) +
  egc2 * ( ( ux - uz ) * ( fx - fz ) - uf );
      tmp2 = eg2rho * ( ux - uz ) + egc2 * ( fx - fz );

      g13[m+next + 1 - ldx*ldy] = itaurho * g13[current_pos] + tmp1 + tmp2;
      g14[m+next - 1 + ldx*ldy] = itaurho * g14[current_pos] + tmp1 - tmp2;

      tmp1 = eg2ag + eg2rho * ( 0.50 * ( uy + uz ) * ( uy + uz ) - v ) +
  egc2 * ( ( uy + uz ) * ( fy + fz ) - uf );
      tmp2 = eg2rho * ( uy + uz ) + egc2 * ( fy + fz );

      g15[m+next + ldx + ldx*ldy] = itaurho * g15[current_pos] + tmp1 + tmp2;
      g16[m+next - ldx - ldx*ldy] = itaurho * g16[current_pos] + tmp1 - tmp2;

      tmp1 = eg2ag + eg2rho * ( 0.50 * ( uy - uz ) * ( uy - uz ) - v ) +
  egc2 * ( ( uy - uz ) * ( fy - fz ) - uf );
      tmp2 = eg2rho * ( uy - uz ) + egc2 * ( fy - fz );

      g17[m+next + ldx - ldx*ldy] = itaurho * g17[current_pos] + tmp1 + tmp2;
      g18[m+next - ldx + ldx*ldy] = itaurho * g18[current_pos] + tmp1 - tmp2;

    }
}

我们注意到,使用大量的双精度变量来存储暂时的数学运算结果以及有意义的物理数量,可能会导致该核函数的性能受寄存器压力影响。为了验证这个假设,我们可以如下编译核函数,以获取核函数的资源使用情况:

hipcc --offload-arch=gfx90a lbm.cpp -Rpass-analysis=kernel-resource-usage -c
lbm.cpp:16:1: remark: Function Name: _Z6kernelPdS_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_iiiiiiiddddddddddddddd [-Rpass-analysis=kernel-resource-usage]
{
^
lbm.cpp:16:1: remark:     SGPRs: 98 [-Rpass-analysis=kernel-resource-usage]
lbm.cpp:16:1: remark:     VGPRs: 102 [-Rpass-analysis=kernel-resource-usage]
lbm.cpp:16:1: remark:     AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
lbm.cpp:16:1: remark:     ScratchSize [bytes/lane]: 0 [-Rpass-analysis=kernel-resource-usage]
lbm.cpp:16:1: remark:     Occupancy [waves/SIMD]: 4 [-Rpass-analysis=kernel-resource-usage]
lbm.cpp:16:1: remark:     SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm.cpp:16:1: remark:     VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm.cpp:16:1: remark:     LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

虽然没有寄存器溢出,但我们注意到占用率仅为每SIMD单元4个波浪(wave);约为最佳可实现情况的一半。查看之前显示的占用率表格,我们发现需要将使用的VGPRs数量从102减少到96或以下,以达到每个SIMD单元5个波浪的占用率。

优化1:移除不必要的数学函数调用

查看以下代码,我们注意到变量 current_phi 进行平方时使用了 pow 函数。

 if(i <= nx && j <= ny && z <= nz)
    {
      m = i + ldx * (j + ldy * z);
      current_pos = m + current;

      current_phi = phi[m];
      current_phi_2 = pow(current_phi,2.0);

如前所述,编译器会内联所有对设备函数的调用,包括数学函数。一个可能的优化是,将通用函数 pow 替换为用于对变量进行平方的具体代码,如下所示:

 if(i <= nx && j <= ny && z <= nz)
    {
      m = i + ldx * (j + ldy * z);
      current_pos = m + current;

      current_phi = phi[m];
      current_phi_2 = current_phi * current_phi;

重新编译新代码,我们观察到这些改变将VGPRs使用量从102减少到100:

hipcc --offload-arch=gfx90a lbm_nopow_1.cpp -Rpass-analysis=kernel-resource-usage -c
lbm_nopow_1.cpp:16:1: remark: Function Name: _Z6kernelPdS_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_iiiiiiiddddddddddddddd [-Rpass-analysis=kernel-resource-usage]
{
^
lbm_nopow_1.cpp:16:1: remark:     SGPRs: 98 [-Rpass-analysis=kernel-resource-usage]
lbm_nopow_1.cpp:16:1: remark:     VGPRs: 100 [-Rpass-analysis=kernel-resource-usage]
lbm_nopow_1.cpp:16:1: remark:     AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_nopow_1.cpp:16:1: remark:     ScratchSize [bytes/lane]: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_nopow_1.cpp:16:1: remark:     Occupancy [waves/SIMD]: 4 [-Rpass-analysis=kernel-resource-usage]
lbm_nopow_1.cpp:16:1: remark:     SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_nopow_1.cpp:16:1: remark:     VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_nopow_1.cpp:16:1: remark:     LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

虽然减少的幅度看起来并不显著,但这为后续优化提供了更大的空间。

优化 n.2:将变量定义尽量靠近其首次使用的位置

一旦一个变量被定义,它的值会存储在寄存器中以备将来使用。如果在内核开头定义变量并在结尾使用,会显著增加寄存器的使用量。一个可能带来显著收益的优化是查找这些变量定义与其首次使用之间“距离较远”的情况,并手动重新排列代码。

经过快速的目视检查,我们可以看到数组位置 f[m] 的定义并不依赖于 ux、`uy` 或 uz,与其他数组 f1 到 f6 不同。

      mu_phi = alpha * current_phi * ( current_phi_2 - phi2 ) - k * laplacian_phi[m];

      fx = mu_phi * grad_phi_x[m];
      fy = mu_phi * grad_phi_y[m];
      fz = mu_phi * grad_phi_z[m];

      ux = ( g1[current_pos] - g2[current_pos] + g7[current_pos] - g8[current_pos] + g9[current_pos] -
       g10[current_pos] + g11[current_pos] - g12[current_pos] + g13[current_pos] - g14[current_pos] +
       0.50 * fx ) * 1.0/rho;
      uy = ( g3[current_pos] - g4[current_pos] + g7[current_pos] - g8[current_pos] - g9[current_pos] +
       g10[current_pos] + g15[current_pos] - g16[current_pos] + g17[current_pos] - g18[current_pos] +
       0.50 * fy ) * 1.0/rho;
      uz = ( g5[current_pos] - g6[current_pos] + g11[current_pos] - g12[current_pos] - g13[current_pos] +
       g14[current_pos] + g15[current_pos] - g16[current_pos] - g17[current_pos] + g18[current_pos] +
       0.50 * fz ) * 1.0/rho;

      af = 0.50 * gamma * mu_phi * itauphi;
      cf = itauphi * ieta * current_phi;

      f0[m] = itauphi1 * f0[m] + -3.0 * gamma * mu_phi * itauphi + itauphi * current_phi;

      f1[current_pos] = itauphi1 * f1[current_pos] + af + cf * ux;
      f2[current_pos] = itauphi1 * f2[current_pos] + af - cf * ux;
      f3[current_pos] = itauphi1 * f3[current_pos] + af + cf * uy;
      f4[current_pos] = itauphi1 * f4[current_pos] + af - cf * uy;
      f5[current_pos] = itauphi1 * f5[current_pos] + af + cf * uz;
      f6[current_pos] = itauphi1 * f6[current_pos] + af - cf * uz;

将 f[m] 的定义移动到 ux 定义之前:

      mu_phi = alpha * current_phi * ( current_phi_2 - phi2 ) - k * laplacian_phi[m];

      f0[m] = itauphi1 * f0[m] + -3.0 * gamma * mu_phi * itauphi + itauphi * current_phi;

      fx = mu_phi * grad_phi_x[m];
      fy = mu_phi * grad_phi_y[m];
      fz = mu_phi * grad_phi_z[m];

      ux = ( g1[current_pos] - g2[current_pos] + g7[current_pos] - g8[current_pos] + g9[current_pos] -
       g10[current_pos] + g11[current_pos] - g12[current_pos] + g13[current_pos] - g14[current_pos] +
       0.50 * fx ) * 1.0/rho;
      uy = ( g3[current_pos] - g4[current_pos] + g7[current_pos] - g8[current_pos] - g9[current_pos] +
       g10[current_pos] + g15[current_pos] - g16[current_pos] + g17[current_pos] - g18[current_pos] +
       0.50 * fy ) * 1.0/rho;
      uz = ( g5[current_pos] - g6[current_pos] + g11[current_pos] - g12[current_pos] - g13[current_pos] +
       g14[current_pos] + g15[current_pos] - g16[current_pos] - g17[current_pos] + g18[current_pos] +
       0.50 * fz ) * 1.0/rho;

      af = 0.50 * gamma * mu_phi * itauphi;
      cf = itauphi * ieta * current_phi;

      f1[current_pos] = itauphi1 * f1[current_pos] + af + cf * ux;
      f2[current_pos] = itauphi1 * f2[current_pos] + af - cf * ux;
      f3[current_pos] = itauphi1 * f3[current_pos] + af + cf * uy;
      f4[current_pos] = itauphi1 * f4[current_pos] + af - cf * uy;
      f5[current_pos] = itauphi1 * f5[current_pos] + af + cf * uz;
      f6[current_pos] = itauphi1 * f6[current_pos] + af - cf * uz;

我们注意到新的 VGPRs 使用量为 96,这使我们在 SIMD 上的占用从四个波提高到五个波:

hipcc --offload-arch=gfx90a lbm_rearrage_2.cpp -Rpass-analysis=kernel-resource-usage -c
lbm_rearrage_2.cpp:16:1: remark: Function Name: _Z6kernelPdS_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_iiiiiiiddddddddddddddd [-Rpass-analysis=kernel-resource-usage]
{
^
lbm_rearrage_2.cpp:16:1: remark:     SGPRs: 94 [-Rpass-analysis=kernel-resource-usage]
lbm_rearrage_2.cpp:16:1: remark:     VGPRs: 96 [-Rpass-analysis=kernel-resource-usage]
lbm_rearrage_2.cpp:16:1: remark:     AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_rearrage_2.cpp:16:1: remark:     ScratchSize [bytes/lane]: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_rearrage_2.cpp:16:1: remark:     Occupancy [waves/SIMD]: 5 [-Rpass-analysis=kernel-resource-usage]
lbm_rearrage_2.cpp:16:1: remark:     SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_rearrage_2.cpp:16:1: remark:     VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_rearrage_2.cpp:16:1: remark:     LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

使用 restrict 关键字的说明

在类似 C++ 的 C 语言类型中,别名是实现高性能的主要限制之一。为了避免这个问题,C99 标准引入了“受限指针”:一种用户告知编译器不同的对象指针类型和函数参数数组不指向重叠内存区域的方式。这允许编译器执行更激进的优化,否则由于别名问题,这些优化可能无法进行。使用受限指针可能会增加寄存器压力,因为编译器会尝试通过将更多数据存储在寄存器中来重用数据。在 AMD 硬件上并非总是如此,有时使用 restrict 有助于减少 SGPR 和 VGPR 的压力。作为经验法则,在函数参数上使用 restrict 会倾向于减少 SGPR 的使用,同时可能会增加 VGPR 的使用。

例如,让我们在 g14 数组上添加 restrict 关键字,因为它在其余代码中多次重复使用,重新使用可能会带来更高的性能。

__global__ void kernel (double *  phi, double *  laplacian_phi,
              double *  grad_phi_x, double * grad_phi_y, double *  grad_phi_z,
              double *  f0, double *  f1, double *  f2, double *  f3, double *  f4,
              double *  f5, double *  f6,
              double *  g0, double *  g1, double *  g2, double *  g3, double *  g4,
              double *  g5, double *  g6, double*  g7, double *  g8, double *  g9,
              double *  g10, double *  g11, double *  g12, double *  g13, double * __restrict__ g14,
              double *  g15, double *  g16, double *  g17, double *  g18,
              int nx, int ny, int nz, int ldx, int ldy, int current, int next,
              double k, double alpha, double phi2, double gamma,
              double itauphi, double itauphi1, double ieta,
              double itaurho, double grav,
              double eg1, double eg2, double eg0, double egc0, double egc1, double egc2)

结果是 SGPR 和 VGPR 的寄存器压力都减少了:

lbm_2_restrict.cpp:16:1: remark: Function Name: _Z6kernelPdS_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_iiiiiiiddddddddddddddd [-Rpass-analysis=kernel-resource-usage]
{
^
lbm_2_restrict.cpp:16:1: remark:     SGPRs: 86 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     VGPRs: 94 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     ScratchSize [bytes/lane]: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     Occupancy [waves/SIMD]: 5 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

通过将 restrict 添加到变量 g7,我们观察到 SGPR 的使用量进一步减少,但 VGPR 略有增加,但仍保持在每 SIMD 5 波的占用率:

lbm_2_restrict.cpp:16:1: remark: Function Name: _Z6kernelPdS_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_S_iiiiiiiddddddddddddddd [-Rpass-analysis=kernel-resource-usage]
{
^
lbm_2_restrict.cpp:16:1: remark:     SGPRs: 78 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     VGPRs: 96 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     ScratchSize [bytes/lane]: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     Occupancy [waves/SIMD]: 5 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
lbm_2_restrict.cpp:16:1: remark:     LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

结论

在这篇文章中,我们高层次地描述了在AMD的CDNA™2架构上运行的HPC应用和算法中,寄存器压力的性质和影响。我们还提供了一组被证明可以有效减少寄存器压力并提高占用率的规则。需要强调的是,这篇博客文章中所展示的结果仅能在使用ROCm 5.4的基于CDNA™2的GPU上完全复制。由于编译器及其启发式方法的不断变化,当使用不同于5.4版本的ROCm时,代码示例的结果可能会有所不同。我们鼓励读者尝试这些代码示例,并在不同的ROCm版本上评估每次更改后的性能。

配套代码示例

作者们感谢Justin Chang、Maria Ruiz Varela和Gina Sitaraman的有益评论和建议。如果您有任何问题或意见,请在GitHub的讨论版联系我们。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2182240.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决银河麒麟中`/etc/sudoers`权限问题

解决银河麒麟中/etc/sudoers权限问题 1、问题描述2、解决方法 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 当你在银河麒麟操作系统中使用sudo命令时&#xff0c;如果遇到“/etc/sudoers可被任何人写”的错误&#xff0c;说明/etc/sudoer…

国庆节快乐

葡萄城在这里祝大家国庆快快乐&#xff1a; 10月葡萄城活动&#xff1a; 公开课 【从软件应用走向数据应用——葡萄城技术赋能数据挖掘】 新版本发布&#xff1a; 活字格 V10.0 Update1新版本发布

Linux相关概念和重要知识点(8)(操作系统、进程属性)

1.操作系统&#xff08;OS&#xff09; &#xff08;1&#xff09;基本结构的认识 任何计算机系统都包含一个基本的程序集合&#xff0c;用于实现计算机最基本最底层的操作&#xff0c;这个软件称为操作系统。操作系统大部分使用C语言编写&#xff0c;少量使用汇编语言。 从…

【数学分析笔记】第4章第2节 导数的意义和性质(1)

4. 微分 4.2 导数的意义与性质 4.2.1 导数在物理中的背景 物体在OS方向上运动&#xff0c;位移函数为 s s ( t ) ss(t) ss(t)&#xff0c;求时刻 t t t的瞬时速度&#xff0c;找一个区间 [ t , t △ t ] [t,t\bigtriangleup t] [t,t△t]&#xff0c;从时刻 t t t变到时刻 t…

闭源与开源嵌入模型比较以及提升语义搜索效果的技术探讨

上图为执行语义搜索前的聚类演示 &#xff0c;嵌入技术是自然语言处理的核心组成部分。虽然嵌入技术的应用范围广泛&#xff0c;但在检索应用中的语义搜索仍是其最常见的用途之一。 尽管知识图谱等可以提升检索的准确率和效率&#xff0c;但标准向量检索技术仍然具有其实用价值…

基于SSM的农产品仓库管理系统【附源码】

基于SSM的农产品仓库管理系统&#xff08;源码L文说明文档&#xff09; 目录 4 系统设计 4.1 系统概要设计 4.2 系统功能结构设计 4.3 数据库设计 4.3.1 数据库E-R图设计 4.3.2 数据库表结构设计 5 系统实现 5.1 管理员功能介绍 5.1.1 用户管…

CSS外边距

元素的外边距&#xff08;margin&#xff09;是围绕在元素边框以外&#xff08;不包括边框&#xff09;的空白区域&#xff0c;这片区域不受 background 属性的影响&#xff0c;始终是透明的。 为元素设置外边距 默认情况下如果不设置外边距属性&#xff0c;HTML 元素就是不会…

通信工程学习:什么是MAC媒体接入控制

MAC&#xff1a;媒体接入控制 MAC&#xff08;Medium Access Control&#xff09;&#xff0c;即媒体接入控制&#xff0c;是计算机网络中数据链路层的一个重要组成部分&#xff0c;负责协调多个发送和接收站点对一个共享传输媒体的占用。以下是关于MAC的详细解释&#xff1a; …

括号序列C++

题目&#xff1a; 样例解释&#xff1a; 如下几种方案是符合规范的&#xff1a; (**)*() (**(*)) (*(**)) (*)**() (*)(**) 思路&#xff1a; 首先肯定是区间dp&#xff0c;令 dpi,jdpi,j​ 表示从位置 ii 到位置 jj 一共的合法序列总情况数量。 但是不同的形态可能会有不同的转…

【开源项目】CException 为C语言提供简洁高效的异常处理机制

CException&#xff1a;为C语言提供简洁高效的异常处理机制 在C语言中进行异常处理并不像C中那样方便。为了实现高效的异常处理&#xff0c;很多开发者选择了CException&#xff0c;一个基于C标准库 setjmp 和 longjmp 的轻量级异常处理框架。本文将带你了解 CException 的特点…

【Power Query】M函数-List.Sum

M函数-List 列表求和 &#xff08;List.Sum&#xff09;&#xff1a;1&#xff09;横向求和2&#xff09;列求和★思路★</font>★实操★</font> 3&#xff09;求总和4&#xff09;求部分占总体的比重★横向★</font>★竖向★</font> 列表求和 &#x…

C++和OpenGL实现3D游戏编程【连载12】——游戏中音效的使用

1、游戏中音效的使用 前面我们实现了图片纹理的显示功能,是不是感觉到非常的简单。那么今天我们就继续说下游戏声音的实现。音效也是游戏的灵魂,只有搭配了美妙动听的音效以后,游戏才能令人耳目一新,与玩家产生良好的效果。 音效文件最常用的可分为两种,分别为.wav和.mp3…

基于SSM的线上旅行信息管理系统【附源码】

基于SSM的线上旅行信息管理系统&#xff08;源码L文说明文档&#xff09; 目录 4.1 系统概述 4.2 数据库E-R图设计 4.3 数据库表设计 5 系统的实现 5.1 管理员功能模块的实现 5.1.1管理员登录界面 5.1.2用户管理界面 5.1.3景点分类管理界面 5.1.4…

(最新已验证)stm32 + 新版 onenet +dht11+esp8266/01s + mqtt物联网(含微信小程序)上报温湿度和控制单片机(保姆级教程)

物联网实践教程&#xff1a;微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制 远程上报和接收数据——汇总 前言 之前在学校获得了一个新玩意&#xff1a;ESP-01sWIFI模块&#xff0c;去搜了一下这个小东西很有玩点&#xff0c;远程控制LED啥的&#xff0c;然后我就想…

【Linux】【操作】Linux操作集锦系列之七——Linux环境下如何查看CPU使用情况(利用率等)

&#x1f41a;作者简介&#xff1a;花神庙码农&#xff08;专注于Linux、WLAN、TCP/IP、Python等技术方向&#xff09;&#x1f433;博客主页&#xff1a;花神庙码农 &#xff0c;地址&#xff1a;https://blog.csdn.net/qxhgd&#x1f310;系列专栏&#xff1a;Linux技术&…

Air201资产定位模组LuatOS:录音播放录音功能的操作方法

一直有小伙伴们问&#xff0c;迷你小巧的合宙Air201虽然有很多优点&#xff0c;超低功耗、精准定位&#xff0c;那么它是否支持录音、播放录音功能&#xff1f; 那必须能&#xff01;高集成化设计的Air201自带了ES8311音频解码芯片&#xff08;Audio Codec&#xff09;及MIC麦…

Could not retrieve https://npm.taobao.org/mirrors/node/index.json. 报错解决

Could not retrieve https://npm.taobao.org/mirrors/node/index.json. 报错解决 1.问题原因及解约 今天使用nvm下载不同版本的nodejs的时候报错了 C:\Users\1> nvm list availableCould not retrieve https://npm.taobao.org/mirrors/node/index.json.提示无法检索地址&…

ARM Process state -- CPSR

Holds PE status and control information. 保存PE状态和控制信息。 N, bit [31] Negative condition flag. Set to bit[31] of the result of the last flag-setting instruction. If the result is regarded as a twos complement signed integer, then N is set to 1 if…

AtCoder ABC371 A-D题解

省流&#xff1a;赛场上不会 C。 比赛链接:ABC371 Problem A: Sol if 暴力判断即可。 Code #include <bits/stdc.h> using namespace std; int main(){char SAB,SAC,SBC;cin>>SAB>>SAC>>SBC;if(SAB> && SBC>)cout<<"b&…

【Unity踩坑】使用内购时获取Google Play license key

在Unity中使用了IAP&#xff08;内购&#xff09;后&#xff0c;需要设置Google Play license key。 这个key需要在Google Play Console中&#xff08;https://play.google.com/console&#xff09;&#xff0c;找到相应的应用&#xff0c;在左侧“创收设置”里可以找到license…