OpenACC MPI 教程

5 倍速,5 小时完成:使用 OpenACC 将 3D 弹性波模拟器移植到 GPU

石油和天然气行业的科学家经常研究大型但高度可并行化的问题,这些问题可以很好地适应加速器。SEISMIC_CPML 就是一个例子。它由法国波城大学的 Dimitri Komatitsch 和 Roland Martin 开发。从他们的网站上摘录:“SEISMIC_CPML 是一组十个开源 Fortran 90 程序,用于求解二维或三维各向同性或各向异性弹性、粘弹性或孔隙弹性波动方程,使用有限差分法和卷积或辅助完全匹配层(C-PML 或 ADE-PML)条件。”

特别是,我们决定加速 3D 各向同性应用程序,它是一个“具有卷积-PML (C-PML) 吸收条件的速度和应力公式的 3D 弹性有限差分代码”。除了计算密集型之外,该代码还使用 MPI 和 OpenMP 来执行域分解,这为我们提供了一个展示多 GPU 编程用例的机会。

本教程将帮助您了解使用 OpenACC 将应用程序移植到 GPU 所涉及的步骤、一些优化技巧以及识别一些潜在陷阱的方法。它应该可以作为指导,帮助您将 OpenACC 指令应用于您自己的代码。本教程的所有源代码都可以作为 此 tarball 的一部分下载。下载后,解压缩并解压该文件,这将创建一个目录,您可以在其中跟随本教程进行操作(如果您愿意)。

1. 步骤 0:评估

加速器编程中最困难的部分始于编写第一行代码之前。拥有正确的算法对于成功至关重要。加速器就像一群蚂蚁。单个核心很弱,但合在一起可以做伟大的事情。如果您的程序不是高度并行的,那么加速器就没什么用处。因此,第一步是确保您的算法是并行的。如果不是,您应该确定是否有可以使用的替代算法,或者是否可以重新设计您的算法以使其更具并行性。

在评估 SEISMIC_CPML 3-D 各向同性代码时,我们看到主时间步循环包含八个并行循环。但是,仅仅并行可能还不够,如果循环的计算密集度不高。虽然计算强度不应该是您唯一的衡量标准,但它是潜在成功的一个良好指标。使用编译器标志 -⁠Minfo=intensity,我们可以看到各种循环的计算强度(计算与数据移动的比率)在 2.5 到 2.64 之间。这些是平均强度。一般来说,除非它是一个更大程序的一部分,否则任何低于 1.0 的值通常都不值得加速。理想情况下,我们希望平均强度高于 4,但 2.5 也足以继续前进。

SEISMIC_CPML 使用 MPI 在 Z 维度上分解问题空间。这将允许我们使用多个 GPU,但也会增加额外的数据移动,因为程序需要传递光环(域的区域,在进程之间重叠)。我们也可以使用 OpenMP 线程,但这会增加更多的编程工作和我们示例的复杂性。因此,我们选择从 GPU 版本中删除 OpenMP 代码。我们可能会在以后的文章中重新审视这个决定。

顺便说一句,对于代码的 MPI 部分,程序员手动分解域。对于 OpenMP,如果程序在共享内存环境中运行,编译器会自动执行此操作。目前,OpenMP 编译器无法像使用多个设备时那样自动跨多个离散内存空间分解问题。因此,程序员必须像使用 MPI 一样手动分解问题。因为这基本上需要我们重复工作,所以我们决定在此处放弃使用 OpenMP。

要在您的系统上构建和运行原始 MPI/OpenMP 代码,请执行以下操作(确保编译器 (pgfortran) 和 MPI 包装器 (mpif90) 在构建和运行之前位于您的路径中)

cd step0
make build
make run
make verify

2. 步骤 1:添加设置代码

因为这是一个 MPI 代码,其中每个进程将使用自己的 GPU,所以我们需要添加一些实用程序代码来确保发生这种情况。setDevice 例程首先确定进程所在的节点(通过调用 hostid),然后从所有其他进程收集 hostid。然后,它确定节点上有多少个 GPU 可用,并将设备分配给每个进程。

请注意,为了保持与 CPU 版本的可移植性,代码的此部分受预处理器宏 _OPENACC 保护,当通过使用 -⁠acc 命令行编译器选项在 HPC Fortran 编译器中启用 OpenACC 指令时,将定义该宏。

#ifdef _OPENACC
#
function setDevice(nprocs,myrank)

  use iso_c_binding
  use openacc
  implicit none
  include 'mpif.h'

  interface
    function gethostid() BIND(C)
      use iso_c_binding
      integer (C_INT) :: gethostid
    end function gethostid
  end interface

  integer :: nprocs, myrank
  integer, dimension(nprocs) :: hostids, localprocs
  integer :: hostid, ierr, numdev, mydev, i, numlocal
  integer :: setDevice

! get the hostids so we can determine what other processes are on this node
  hostid = gethostid()
  CALL mpi_allgather(hostid,1,MPI_INTEGER,hostids,1,MPI_INTEGER, &
                     MPI_COMM_WORLD,ierr)

! determine which processors are on this node
  numlocal=0
  localprocs=0
  do i=1,nprocs
    if (hostid .eq. hostids(i)) then
      localprocs(i)=numlocal
      numlocal = numlocal+1
    endif
  enddo

! get the number of devices on this node
  numdev = acc_get_num_devices(ACC_DEVICE_NVIDIA)

  if (numdev .lt. 1) then
    print *, 'ERROR: There are no devices available on this host.  &
              ABORTING.', myrank
    stop
  endif

! print a warning if the number of devices is less then the number
! of processes on this node.  Having multiple processes share devices is not
! recommended.
  if (numdev .lt. numlocal) then
   if (localprocs(myrank+1).eq.1) then
     ! print the message only once per node
   print *, 'WARNING: The number of process is greater then the number  &
             of GPUs.', myrank
   endif
   mydev = mod(localprocs(myrank+1),numdev)
  else
   mydev = localprocs(myrank+1)
  endif

 call acc_set_device_num(mydev,ACC_DEVICE_NVIDIA)
 call acc_init(ACC_DEVICE_NVIDIA)
 setDevice = mydev

end function setDevice
#endif

要在您的系统上构建和运行 step1 代码,请执行以下操作(确保编译器 (pgfortran) 和 MPI 包装器 (mpif90) 在构建和运行之前位于您的路径中)

cd step1
make build
make run
make verify

3. 步骤 2:添加计算区域

接下来,我们花了幾分鐘在八个并行循环周围添加了六个计算区域。例如,这是最终的归约循环。

!$acc kernels
  do k = kmin,kmax
    do j = NPOINTS_PML+1, NY-NPOINTS_PML
      do i = NPOINTS_PML+1, NX-NPOINTS_PML

! compute kinetic energy first, defined as 1/2 rho ||v||^2
! in principle we should use rho_half_x_half_y instead of rho for vy
! in order to interpolate density at the right location in the staggered grid
! cell but in a homogeneous medium we can safely ignore it

      total_energy_kinetic = total_energy_kinetic + 0.5d0 * rho*( &
              vx(i,j,k)**2 + vy(i,j,k)**2 + vz(i,j,k)**2)

! add potential energy, defined as 1/2 epsilon_ij sigma_ij
! in principle we should interpolate the medium parameters at the right location
! in the staggered grid cell but in a homogeneous medium we can safely ignore it

! compute total field from split components
      epsilon_xx = ((lambda + 2.d0*mu) * sigmaxx(i,j,k) - lambda *  &
      sigmayy(i,j,k) - lambda*sigmazz(i,j,k)) / (4.d0 * mu * (lambda + mu))
      epsilon_yy = ((lambda + 2.d0*mu) * sigmayy(i,j,k) - lambda *  &
          sigmaxx(i,j,k) - lambda*sigmazz(i,j,k)) / (4.d0 * mu * (lambda + mu))
      epsilon_zz = ((lambda + 2.d0*mu) * sigmazz(i,j,k) - lambda *  &
          sigmaxx(i,j,k) - lambda*sigmayy(i,j,k)) / (4.d0 * mu * (lambda + mu))
      epsilon_xy = sigmaxy(i,j,k) / (2.d0 * mu)
      epsilon_xz = sigmaxz(i,j,k) / (2.d0 * mu)
      epsilon_yz = sigmayz(i,j,k) / (2.d0 * mu)

      total_energy_potential = total_energy_potential + &
        0.5d0 * (epsilon_xx * sigmaxx(i,j,k) + epsilon_yy * sigmayy(i,j,k) + &
        epsilon_yy * sigmayy(i,j,k)+ 2.d0 * epsilon_xy * sigmaxy(i,j,k) + &
        2.d0*epsilon_xz * sigmaxz(i,j,k)+2.d0*epsilon_yz * sigmayz(i,j,k))

      enddo
    enddo
  enddo
!$acc end kernels

HPC 加速器 Fortran 编译器的 -⁠acc 命令行选项启用 OpenACC 指令。请注意,OpenACC 旨在模拟通用类别的设备。虽然 NVIDIA 是 HPC 加速器当前的市场领导者,也是 NVIDIA OpenACC 实现的默认目标,但该模型可以而且将来也将以其他设备为目标。

您在开发期间想要使用的另一个编译器选项是 -⁠Minfo,它使编译器输出有关对您的代码执行的优化和转换的反馈。对于特定于加速器的信息,请使用 -⁠Minfo=accel 子选项。编译 SEISMIC_CPML 时生成的反馈消息示例包括

1113, Generating copyin(vz(11:91,11:631,kmin:kmax))
      Generating copyin(vy(11:91,11:631,kmin:kmax))
      Generating copyin(vx(11:91,11:631,kmin:kmax))
      Generating copyin(sigmaxx(11:91,11:631,kmin:kmax))
      Generating copyin(sigmayy(11:91,11:631,kmin:kmax))
      Generating copyin(sigmazz(11:91,11:631,kmin:kmax))
      Generating copyin(sigmaxy(11:91,11:631,kmin:kmax))
      Generating copyin(sigmaxz(11:91,11:631,kmin:kmax))
      Generating copyin(sigmayz(11:91,11:631,kmin:kmax))

要在 GPU 上计算,第一步是将数据从主机内存移动到 GPU 内存。在上面的示例中,编译器告诉您它正在复制九个数组。请注意 copyin 语句。这意味着编译器只会将数据复制到 GPU,而不会将其复制回主机。这是因为第 1113 行对应于归约循环计算区域的开始,其中使用了这些数组但从未修改过。您可能会看到的其他数据移动子句包括 copy,其中数据在区域开始时复制到设备,在区域结束时复制回来,以及 copyout,其中数据仅复制回主机。

请注意,编译器仅复制数组的内部子部分。默认情况下,编译器是保守的,仅复制执行必要计算实际所需的数据。不幸的是,由于内部子数组在主机内存中不是连续的,因此编译器需要为每个数组生成多个数据传输。总体 GPU 性能在很大程度上取决于我们优化内存传输的能力。这意味着不仅要传输多少数据,还要发生多少次传输。传输多个子数组非常昂贵。现在我们只需注意它。稍后,我们将研究如何通过覆盖编译器默认值并在一个大的连续块中复制整个数组来提高性能。

1114, Loop is parallelizable
1115, Loop is parallelizable
1116, Loop is parallelizable
      Accelerator kernel generated

在这里,编译器对第 1114、1115 和 1116 行的循环(前面显示的归约循环)执行了依赖性分析。它发现所有三个循环都是可并行化的,因此它生成了一个加速器内核。当编译器遇到无法并行化的循环时,它通常会报告原因,以便您可以相应地调整代码。如果内部循环不可并行化,则仍可能为外部循环生成内核;在这些情况下,内部循环将在 GPU 核心上顺序运行。

编译器可能会尝试通过交换循环(即更改顺序)来解决阻止并行化的依赖性(如果这样做是安全的)。至少必须有一个外部或交换循环是并行的才能生成加速器内核。在某些情况下,您可以使用循环指令 independent 子句来解决潜在的依赖性,或者使用 private 子句来完全消除依赖性。在其他情况下,可能需要显着重构代码才能实现并行化。

生成的加速器内核只是一个串行代码片段,由许多线程同时在 GPU 上执行。每个线程将执行相同的代码,但操作不同的数据。线程的组织方式称为循环调度。下面我们可以看到归约循环的循环调度。do 循环已被三维 gang 替换,而三维 gang 又由二维向量部分组成。

1114, !$acc loop gang ! blockidx%y
1115, !$acc loop gang, vector(4) ! blockidx%z threadidx%y
1116, !$acc loop gang, vector(32) ! blockidx%x threadidx%x

在 CUDA 术语中,gang 子句对应于网格维度,而 vector 子句对应于线程块维度。对于新的或非 CUDA 程序员,我们强烈建议阅读 Michael Wolfe 的 PGInsider 文章理解 GPU 的数据并行线程模型

不要对循环调度感到过于不知所措。这只是一种组织 GPU 线程如何作用于数组数据元素的方式。因此,这里我们有一个 3-D 数组,它被分组为 32×4 个元素的块,其中单个线程正在处理特定元素。由于循环调度中未指定 gang 的数量,因此将在内核启动时动态确定。如果 gang 子句具有固定宽度,例如 gang(16),则将编写每个内核以循环访问多个元素。

使用 CUDA,编程归约和管理共享内存可能是一项相当困难的任务。在下面的示例中,编译器已自动生成使用这些功能的最佳代码。顺便说一句,编译器始终在寻找优化您的代码的机会。

1122, Sum reduction generated for total_energy_kinetic
1140, Sum reduction generated for total_energy_potential

好的,我们做得怎么样?

要在您的系统上构建和运行 step2 代码,请执行以下操作(如上所述,确保编译器 (pgfortran) 和 MPI 包装器 (mpif90) 在构建和运行之前位于您的路径中)

cd step2
make build
make run
make verify

经过十分钟的编程时间,我们设法使程序变得非常非常慢!请记住,这是一篇关于在 5 小时内将 SEISMIC_CPML 加速 5 倍的文章,而不是在十分钟内将其减速 3 倍。我们很快将克服这种减速,但在 GPU 编程时看到这种后退一步的经历并不罕见。

如果这种情况发生在您身上,请不要气馁。那么,为什么会减速,我们该如何解决它呢?

4. 步骤 3:添加数据区域

您可能已经猜到,减速是由主机内存和 GPU 内存之间过多的数据移动引起的。在查看我们 CUDA 配置文件信息的一部分时,我们看到每个计算区域都花费大量时间在主机和设备之间来回复制数据。由于每个计算区域执行 2500 次,因此会产生大量数据移动。将步骤 2 的配置文件输出中所有数据传输时间加起来表明,绝大多数时间(超过 99%)都花在了复制数据上,而只有一小部分时间花在了计算内核中。剩余时间要么花在主机代码中,要么花在等待数据传输的阻塞上(两个 MPI 进程都必须使用相同的 PCIe 总线来传输数据)。

请注意,在设备上复制数据或计算所花费的确切时间在不同系统之间会有所不同。要查看您系统的时间,请设置环境变量 PGI_ACC_TIME=1 并运行您的可执行文件。此选项打印基本配置文件信息,例如内核执行时间、数据传输时间、初始化时间、实际启动配置以及在计算区域中花费的总时间。请注意,总时间是从主机测量的,并且包括在区域内执行主机代码所花费的时间。

为了提高性能,我们需要找到一种方法来最大限度地减少传输数据的时间。输入 data 指令。您可以使用数据区域来指定程序中数据应从主机内存复制到 GPU 内存以及再次复制回来的确切点。数据区域内包含的任何计算区域都将使用先前复制的数据,而无需在计算区域的边界处复制。数据区域可以跨越主机代码和多个计算区域,甚至可以跨越子例程边界。

在查看 SEISMIC_CMPL 中的数组时,有 18 个数组具有常量值。另有 21 个仅在计算区域内使用,因此永远不需要在主机上。让我们首先在外部时间步循环周围添加一个数据区域。最后三个数组确实需要复制回主机以传递它们的光环。对于这些情况,我们使用 update 指令。

!---
!---  beginning of time loop
!---
!$acc data &
!$acc copyin(a_x_half,b_x_half,k_x_half,                       &
!$acc        a_y_half,b_y_half,k_y_half,                       &
!$acc        a_z_half,b_z_half,k_z_half,                       &
!$acc        a_x,a_y,a_z,b_x,b_y,b_z,k_x,k_y,k_z,              &
!$acc        sigmaxx,sigmaxz,sigmaxy,sigmayy,sigmayz,sigmazz,  &
!$acc        memory_dvx_dx,memory_dvy_dx,memory_dvz_dx,        &
!$acc        memory_dvx_dy,memory_dvy_dy,memory_dvz_dy,        &
!$acc        memory_dvx_dz,memory_dvy_dz,memory_dvz_dz,        &
!$acc        memory_dsigmaxx_dx, memory_dsigmaxy_dy,           &
!$acc        memory_dsigmaxz_dz, memory_dsigmaxy_dx,           &
!$acc        memory_dsigmaxz_dx, memory_dsigmayz_dy,           &
!$acc        memory_dsigmayy_dy, memory_dsigmayz_dz,           &
!$acc        memory_dsigmazz_dz)

  do it = 1,NSTEP

...

!$acc update host(sigmazz,sigmayz,sigmaxz)
! sigmazz(k+1), left shift
  call MPI_SENDRECV(sigmazz(:,:,1),number_of_values,MPI_DOUBLE_PRECISION, &
         receiver_left_shift,message_tag,sigmazz(:,:,NZ_LOCAL+1), &
         number_of_values,

...

!$acc update device(sigmazz,sigmayz,sigmaxz)

...

  ! --- end of time loop
  enddo
!$acc end data

数据区域可以嵌套,事实上,我们在时间循环体中为数组 vxvyvz 使用了此功能,如下所示。虽然这些数组在内部数据区域边界处来回复制,因此比在外部数据区域中移动的数组移动得更频繁,但它们在多个计算区域中使用,而不是在每个计算区域边界处复制。请注意,我们在 copy 子句中未指定任何数组维度。这指示编译器将每个数组作为一个连续块完整复制,并消除我们先前注意到内部子数组在多个块中复制时的低效率。

!$acc data copy(vx,vy,vz)

... data region spans over 5 compute regions and host code

!$acc kernels

...

!$acc end kernels

!$acc end data

我们的计时结果如何?

要在您的系统上构建和运行 step3 代码,请执行以下操作

cd step3
make build
make run
make verify

此步骤花费了大约一个小时的编码时间,并显着减少了我们的执行时间。我们正在取得良好的进展,但我们可以进一步提高性能。

5. 步骤 4:优化数据传输

对于我们的下一步,我们将致力于进一步优化数据传输,方法是将尽可能多的计算迁移到 GPU,并且仅移动绝对最少量所需的数据。第一步是将外部数据区域的开始向上移动,使其在代码中更早出现,并将数据初始化循环放入计算内核中。这包括 vxvyvz 数组。使用此方法使我们能够删除我们在先前优化步骤中使用的内部数据区域。

在以下示例代码中,请注意 create 子句的使用。这指示编译器在 GPU 内存中为局部使用分配变量空间,但不执行对这些变量的数据移动。本质上,它们在 GPU 内存中用作临时变量。

!$acc data                                                     &
!$acc copyin(a_x_half,b_x_half,k_x_half,                       &
!$acc        a_y_half,b_y_half,k_y_half,                       &
!$acc        a_z_half,b_z_half,k_z_half,                       &
!$acc        ix_rec,iy_rec,                                    &
!$acc        a_x,a_y,a_z,b_x,b_y,b_z,k_x,k_y,k_z),             &
!$acc copyout(sisvx,sisvy),                                    &
!$acc create(memory_dvx_dx,memory_dvy_dx,memory_dvz_dx,        &
!$acc        memory_dvx_dy,memory_dvy_dy,memory_dvz_dy,        &
!$acc        memory_dvx_dz,memory_dvy_dz,memory_dvz_dz,        &
!$acc        memory_dsigmaxx_dx, memory_dsigmaxy_dy,           &
!$acc        memory_dsigmaxz_dz, memory_dsigmaxy_dx,           &
!$acc        memory_dsigmaxz_dx, memory_dsigmayz_dy,           &
!$acc        memory_dsigmayy_dy, memory_dsigmayz_dz,           &
!$acc        memory_dsigmazz_dz,                               &
!$acc        vx,vy,vz,vx1,vy1,vz1,vx2,vy2,vz2,                 &
!$acc        sigmazz1,sigmaxz1,sigmayz1,                       &
!$acc        sigmazz2,sigmaxz2,sigmayz2)                       &
!$acc copyin(sigmaxx,sigmaxz,sigmaxy,sigmayy,sigmayz,sigmazz)

...

! Initialize vx, vy and vz arrays on the device
!$acc kernels
  vx(:,:,:) = ZERO
  vy(:,:,:) = ZERO
  vz(:,:,:) = ZERO
!$acc end kernels

...

使用数据区域的一个注意事项是,您必须了解在给定的循环或计算中实际使用的是数据的哪个副本(主机或设备)。数据的host和device副本不会自动保持一致。这是程序员在使用数据区域时的责任。例如,对设备内存中变量副本的任何更新都不会反映在主机副本中,直到您指定应使用 update 指令或数据或计算区域边界处的 copy 子句来更新它。

主机和设备变量副本之间意外失去一致性是 OpenACC 程序中验证错误的最常见原因之一。在对 SEISMIC_CPML 进行上述更改后,代码生成了不正确的结果。经过近半小时的调试,我们确定时间步循环中初始化边界条件的部分被遗漏在 OpenACC 计算区域之外。结果,我们初始化了数据的主机副本,而不是预期的设备副本,这导致设备内存中的变量未初始化。

优化数据传输的下一个挑战与光环区域的处理有关。在计算过程中,SEISMIC_CPML 在 MPI 进程之间传递来自六个 3-D 数组的光环。理想情况下,我们只需使用 update 指令或 copy 子句复制回 2-D 光环子数组,但正如我们之前看到的,在主机和设备内存之间复制非连续数组部分非常低效。作为第一步,我们尝试在传递光环之前将整个数组从设备内存复制回主机。考虑到最终 MPI 传输中只需要在主机和设备内存之间移动少量数据,这也是非常低效的。

经过一些实验,我们确定了一种方法,即添加六个新的临时 2-D 数组来保存光环数据。在计算区域内,我们将 2-D 光环从主 3-D 数组收集到新的临时数组中,将临时数组以一个连续块复制回主机,在 MPI 进程之间传递光环,最后将交换的值复制回设备内存,并将光环分散回 3-D 数组中。虽然这种方法确实增加了内核执行时间,但它节省了大量数据传输时间。

在下面的示例代码中,请注意为支持光环收集和传输而添加的源代码受预处理器 _OPENACC 宏保护,并且仅当代码由启用 OpenACC 的编译器编译时才会执行。

#ifdef _OPENACC
#
! Gather the sigma 3D arrays to a 2D slice to allow for faster
! copy from the device to host
!$acc kernels
   do i=1,NX
    do j=1,NY
      vx1(i,j)=vx(i,j,1)
      vy1(i,j)=vy(i,j,1)
      vz1(i,j)=vz(i,j,NZ_LOCAL)
    enddo
  enddo
!$acc end kernels
!$acc update host(vxl,vyl,vzl)

! vx(k+1), left shift
  call MPI_SENDRECV(vx1(:,:), number_of_values, MPI_DOUBLE_PRECISION, &
       receiver_left_shift, message_tag, vx2(:,:), number_of_values, &
       MPI_DOUBLE_PRECISION, sender_left_shift, message_tag, MPI_COMM_WORLD,&
       message_status, code)

! vy(k+1), left shift
  call MPI_SENDRECV(vy1(:,:), number_of_values, MPI_DOUBLE_PRECISION, &
       receiver_left_shift,message_tag, vy2(:,:),number_of_values,   &
       MPI_DOUBLE_PRECISION, sender_left_shift, message_tag, MPI_COMM_WORLD,&
       message_status, code)

! vz(k-1), right shift
  call MPI_SENDRECV(vz1(:,:), number_of_values, MPI_DOUBLE_PRECISION, &
       receiver_right_shift, message_tag, vz2(:,:), number_of_values, &
       MPI_DOUBLE_PRECISION, sender_right_shift, message_tag, MPI_COMM_WORLD, &
       message_status, code)

!$acc update device(vx2,vy2,vz2)
!$acc kernels
  do i=1,NX
    do j=1,NY
      vx(i,j,NZ_LOCAL+1)=vx2(i,j)
      vy(i,j,NZ_LOCAL+1)=vy2(i,j)
      vz(i,j,0)=vz2(i,j)
    enddo
  enddo
!$acc end kernels

#else

上述修改需要大约两个小时的编码时间,但总执行时间已显着减少,只有一小部分时间用于复制数据!

要在您的系统上构建和运行 step4 代码,请执行以下操作

cd step4
make build
make run
make verify

6. 步骤 5:循环调度调优

我们调优过程的最后一步是使用 gang、worker 和 vector 子句调优 OpenACC 计算区域循环调度。在许多情况下,包括此代码,NVIDIA OpenACC 编译器选择的默认内核调度都非常好。手动调优工作通常不会显着改善计时。但是,在某些情况下,编译器做得不太好。始终值得花一点时间检查一下,看看您是否可以通过使用显式循环调度子句覆盖编译器生成的循环调度来做得更好。您通常可以很快判断出这些子句是否正在产生影响。

不幸的是,没有明确定义的方法来找到最佳内核调度(除非尝试所有可能的调度)。最好的建议是从编译器的默认调度开始,并尝试进行小的调整,以查看它们是否以及如何影响执行时间。您选择的内核调度将影响是否以及如何使用共享内存、全局数组访问以及各种类型的优化。通常,最好对具有大量迭代次数的循环执行 gang 调度。

!$acc loop gang
  do k = k2begin,NZ_LOCAL
    kglobal = k + offset_k
!$acc loop worker vector collapse(2)
    do j = 2,NY
      do i = 2,NX

要在您的系统上构建和运行 step5 代码,请执行以下操作

cd step5
make build
make run
make verify

7. 结论

在略多于五个小时的编程时间里,我们在我们的测试系统上实现了比原始 MPI/OpenMP 版本大约 7 倍的速度提升。在运行更大数据集的更大集群上,速度提升没有那么好。节点间通信的形式存在额外的开销,并且系统上的 CPU 具有更多的内核并以更高的时钟频率运行。尽管如此,速度提升仍然接近 5 倍。

声明

声明

所有 NVIDIA 设计规范、参考板、文件、图纸、诊断程序、列表和其他文档(统称为“材料”)均按“原样”提供。NVIDIA 对材料不作任何明示、暗示、法定或其他方面的保证,并明确否认所有关于不侵权、适销性以及特定用途适用性的暗示保证。

所提供的信息据信是准确可靠的。但是,NVIDIA 公司对使用此类信息或因其使用而可能导致的侵犯第三方专利或其他权利的后果不承担任何责任。未通过暗示或其他方式授予 NVIDIA 公司专利权下的任何许可。本出版物中提及的规格如有更改,恕不另行通知。本出版物取代并替换以前提供的所有其他信息。未经 NVIDIA 公司明确书面批准,NVIDIA 公司产品不得用作生命支持设备或系统中的关键组件。

商标

NVIDIA、NVIDIA 徽标、CUDA、CUDA-X、GPUDirect、HPC SDK、NGC、NVIDIA Volta、NVIDIA DGX、NVIDIA Nsight、NVLink、NVSwitch 和 Tesla 是 NVIDIA Corporation 在美国和其他国家/地区的商标和/或注册商标。其他公司和产品名称可能是与其关联的各自公司的商标。