数据中心/云端

借助 NVIDIA Grace Hopper 超级芯片简化 HPC 的 GPU 编程

 

NVIDIA RTX GPU 中的 NVIDIA Grace Hopper 超级芯片 系统为开发者处理 GPU 编程的方式带来了一些戏剧性的变化。最值得注意的是,CPU 和 GPU 显存之间的双向、高带宽和缓存一致性连接意味着用户可以在使用单个统一地址空间的同时为这两个处理器开发应用程序。

每个处理器都保留自己的物理内存,该内存的设计具有与最适合每个处理器的工作负载相匹配的带宽、延迟和容量特性。针对现有的独立显存 GPU 系统编写的代码将继续保持高性能运行,而无需针对新的 NVIDIA Grace Hopper 架构进行修改。

我们最近的博文 借助异构内存管理简化 GPU 应用程序开发 详细介绍了单地址空间为开发者带来的一些优势,以及它在通过 PCIe 连接至 x86_64 CPU 的 NVIDIA GPU 系统上的工作原理。所有应用程序线程(GPU 或 CPU)都可以直接访问应用程序的所有系统分配显存,从而消除了在处理器之间复制数据的需求。

这种直接读取或写入整个应用程序内存地址空间的新功能可显著提高基于 CUDA 构建的所有编程模型的工作效率:CUDA C++、CUDA Fortran、ISO C++和 ISO Fortran 中的标准并行性、OpenACC、OpenMP 等。

本文将继续关于 Grace Hopper 硬件的异构内存管理 (HMM) 讨论,该硬件提供与支持 HMM 的系统相同的所有编程模型改进,但增加了硬件支持,使其更加出色。

值得注意的是,任何受到主机到设备或设备到主机传输限制的工作负载,都可以在 Grace Hopper 系统中通过 芯片到芯片 (C2C) 互连 获得高达 7 倍的加速。这种性能的实现得益于缓存一致性,而且无需固定内存(例如使用 cudaHostRegister),尤其是在使用大型页面时。虽然过去 HMM 和 CUDA 托管内存仅限于在页面错误时被动迁移整个数据页面,但 Grace Hopper 能够更加精准地决定数据应位于何处以及何时进行迁移。

我们将在本文中详细介绍 NVIDIA HPC 编译器如何利用这些新的硬件功能,通过 ISO C++、ISO Fortran、OpenACC 和 CUDA Fortran 简化 GPU 编程。

NVIDIA Grace Hopper 系统结合简化的 GPU 开发者体验,可提供最佳性能。HMM 还将这种简化的开发者体验引入非 Grace Hopper 系统,同时在使用 PCIe 时提供最佳性能。开发者可以以便携方式使用这些改进和简化的编程模型,以便在使用 NVIDIA GPU 的各种系统上获得出色性能。

使用 Grace Hopper 统一内存扩展 stdpar

标准语言如 ISO C++ 和 ISO Fortran 近年来不断增加功能,使得开发者能够直接利用基础语言本身来表达应用程序中的并行性,而无需依赖于语言扩展或编译器指令。NVIDIA HPC 编译器 能够构建这些应用程序,并确保它们在 NVIDIA GPU 上高效运行。有关这些功能的更多信息,请参阅我们之前的文章

更具体地说,我们展示了如何使用标准语言并行(也称为 stdpar)来大大提高开发者的工作效率并简化 GPU 应用开发。但是,我们还指出了由于 CPU 和 GPU 的不同内存空间的性质而造成的一些限制,包括无法使用 C++并行算法中的某些类型的数据,例如在堆栈上分配的数据、全局数据或在 lambda 捕获中通过引用捕获的数据。

对于 Fortrando concurrent循环,全局变量不能用于从内部调用的例程do concurrent循环和编译器对假设大小数组的数据大小检测受到限制。现在,Grace Hopper 及其统一内存功能消除了这些限制,使用 stdpar 开发应用程序变得更加简单。

简化 OpenACC 和 CUDA Fortran

长期以来,GPU 应用程序开发者一直倾向于使用 OpenACCCUDA Fortran,因为它们的便利性和强大功能。 CUDA C++ 也同样经受了时间的考验,并被全球 HPC 中心的大量应用程序用于生产。这两种模型都提供了管理数据驻留以及优化数据传输和移动的可靠方法。

现在,借助 Grace Hopper 的统一显存功能,系统可以自动处理数据位置和移动的这些考虑因素,从而大幅简化应用开发。这减少了将应用移植到 GPU 上运行的工作量,并为算法开发留出了更多时间。

为了微调性能和优化,开发者可以选择使用 OpenACC 和 CUDA Fortran 中已有的设施选择性地添加有关数据局部性的信息。现有应用程序中为独立显存设备编写的数据信息可用于优化 Grace Hopper 的统一显存,而无需更改代码。

使用统一内存评估应用程序性能

以下各节将探讨多个基准测试和应用,以了解这些新功能如何不仅简化代码开发,而且会影响预期的运行时性能。

SPECaccel 2023 基准测试

我们的SPECaccel® 2023 基准测试套件专注于评估使用 CUDA、OpenACC 和 OpenMP 的单个加速器性能。这些基准测试旨在展示通用 GPU 的性能,并能很好地代表众多 HPC 应用程序如何利用 Grace Hopper 的新统一内存功能。

图 1 比较了 OpenACC 数据指令与通过 NVHPC SDK 编译器标志启用的统一内存的性能-gpu=unified.虽然结果遵循基准测试的运行规则要求,但它们是在预生产硬件上测量的,因此被视为估计结果。

A bar chart comparing the estimated performance of 13 benchmarks in the SPECaccel 2023 suite when using data directives compared to unified memory. The majority of the bars show very little performance difference between the two versions.
图 1.与统一显存相比,使用数据指令的多个 SPECaccel 2023 基准测试的预估性能

大多数基准测试表明,使用统一显存与使用 OpenACC 数据指令管理的显存之间几乎没有什么区别,但整体延迟仅为+1%.463.swim 主要用于测量内存性能,在使用统一显存时获得 28%的性能。使用数据指令,每个时间周期都会复制整个数组,尽管主机上仅使用数组的内部三角形部分。

如果使用数据指令打印的数据不连续,则最好将整个数组复制为一个大型块,而不是许多较小的块。使用统一显存时,在主机上访问的数据要少得多,并且仅从 GPU 显存中获取阵列的一部分。

唯一显著下降的情况是,404.lbm 基准测试为 22%.在使用统一显存时,每次迭代的核函数时间会产生 2 毫秒的轻微开销。假设核函数执行了 2000 次,则开销累计约占差值的 3%.更大的问题是,整个 5 GB 结果数组每进行 63 次迭代(需要从主机访问)就会受到检查。在这种情况下,CPU 访问 GPU 显存的时间大致翻了一番,占差值的剩余 19%.

统一内存显著简化了代码的移植,并且与 SPECaccel 的情况一样,统一内存通常会提供与使用数据指令相同的性能。与具有非统一内存访问 (NUMA) 特性的任何其他多插槽系统一样,程序员仍然需要注意数据放置。但是,在大多数情况下,对于在 CPU 和 GPU 上访问大量数据的情况,数据指令现在可以被视为性能调整选项,例如 HTTP.lbm.

SPEC 和 SPECaccel 是 标准性能评估公司

LULESH

LULESH 是一款迷你应用程序,旨在模拟简化版的冲击流体动力学,代表 LLNL ALE3D 应用程序。十多年来,它一直被用于理解 C++ 并行编程模型及其与编译器和内存分配器的交互。

LULESH 的 stdpar 实现对 GPU 上的所有数据结构使用 C++标准库容器,并且它们依赖于 CPU 和 GPU 之间的内存自动迁移。

图 2 显示,使用统一显存不会影响 LULESH 的性能,这是有意义的。无论是托管显存还是统一显存选项,LULESH 的性能指标 (FOM) 均为 2.09 e5,NVIDIA DGX GH200 比使用 FOM 时高出 40% 的 NVIDIA H100 PCIe GPU,比 56 核 Intel Xeon 8480* CPU 系统快 6.5 倍。

A bar chart comparing the performance of LULESH when run in multiple ways. The performance on an Intel Xeon 8480+ is the baseline. The H100 PCIe bar is 4.61x faster. The performance of the GH200 using managed memory is 6.51x and with the compiler’s unified mode is 6.49x.
图 2.在搭载 NVIDIA H100 PCIe 和现代 CPU 的 NVIDIA GH200 上使用托管和统一显存选项的 LULESH 性能比较

POT3D

POT3D 通过计算势场解来近似计算太阳冠状磁场。它由 Predictive Science Inc.使用现代 Fortran 开发。该应用程序过去一直使用 OpenACC 在 GPU 上运行,但作者现在采用了 Fortran 的混合体do concurrent来表示数据并行循环和 OpenACC,从而使用 GPU 优化数据移动。

在 GTC 会议上,从指令到 DO CONCURRENT:标准并行的案例研究中提到,代码的 stdpar 版本的执行速度大约比优化的 OpenACC 代码慢 10%。如果使用 OpenACC 来优化 stdpar 版本的数据移动,性能几乎相同。这意味着在实现相同的性能的同时,保留的代码行数减少了大约 2000 行。统一显存是否会改变这一点呢?

图 3 展示了 POT3D 在 Grace Hopper 上以两种方式构建的性能。蓝色条是性能基准,即 Fortrando concurrent用于并行性的循环和 OpenACC 数据指令,以优化数据移动。绿色条使用-gpu=unifiedGrace Hopper 上的选项,并删除所有 OpenACC 指令。

代码的性能现在与完全优化的代码相同,而不需要任何 OpenACC.随着统一内存带来的性能和生产力增强,POT3D 现在可以使用纯 Fortran 编写,并获得与先前调整的 OpenACC 代码相同的性能。

A bar chart comparing performance of managing memory explicitly in POT3D and using the new unified memory mode. The performance using OpenACC for data management and building without any data directives is equal.
图 3.与 Grace Hopper 统一显存相比,使用 OpenACC 数据指令的 POT3D 性能

如何在 NVIDIA HPC SDK 中启用和使用统一显存

从 NVHPC SDK 版本 23.11 开始,旨在使用具有统一显存功能的 GPU 的开发者可以从简化的编程接口中受益。此版本引入了一种新的编译模式,nvc++, nvc以及nvfortran编译器,可以通过传递标志来启用-gpu=unified.

本节将深入探讨 NVHPC SDK 支持的各种编程模型中统一内存的具体增强功能,该模型利用底层硬件和 CUDA 运行时的功能,自动处理 CPU 和 GPU 物理内存之间的数据放置和内存迁移。

标准参数

对于 stdpar,已删除所有数据访问限制。这意味着可以从 CPU 或 GPU 访问全局变量,并且统一内存编译现在是兼容机器上的默认设置。但是,当针对不同目标进行交叉编译时,-gpu=unified需要显式传递 flag 以启用新的编程接口。

使用 nvc++ 加速 C++ 标准库中的并行算法(stdpar) 的原始版本中,lambda 函数在并行算法中有一些限制。现在,这些限制已经完全取消。开发者可以在不同的并行算法和顺序代码中自由使用数据,这允许通过引用捕获变量并访问并行算法中的全局变量。

int init_val = 123;
void foo() {
  int my_array[ARRAY_SIZE];
  auto r = std::views::iota(0, ARRAY_SIZE);
  std::for_each(std::execution::par_unseq, r.begin(), r.end(),
                [&](auto i) { my_array[i] = init_val; });
}

如果按如下所示编译此代码,则数组my_array可以在 GPU 上安全地初始化,同时使用全局变量的值并行设置每个元素init_val.之前,您可以同时访问my_arrayinit_val不受支持。

nvc++ -std=c++20 -stdpar -gpu=unified example.cpp

现在还可以使用std::array安全地使用并行算法,如示例所示:

std::array<int, 10000> my_array = ...;
std::sort(std::execution::par, my_array.begin(), my_array.end());

消除数据访问限制是一项显著的改进,但请记住,数据竞争仍然是可能的。例如,在并行算法中访问全局变量,同时在 GPU 上运行的不同 lambda 实例中进行更新。

将现有代码移植到 stdpar C++和集成第三方库也得到了简化。当并行算法中使用的数据指针源自单独文件的分配语句时,这些文件不再需要使用nvc++-stdpar.

对于标准 Fortran,以前不支持某些变量用途。现在,可以在调用的例程中访问全局变量do concurrent循环。此外,在一些情况下,编译器无法准确确定 GPU 和 CPU 之间隐式数据移动的变量大小。现在可以在具有统一内存的目标上正确处理这些情况:

subroutine r(a, b)
  integer :: a(*)
  integer :: b(:)
  do concurrent (i = 1 : size(b))
    a(b(i)) = i 
  enddo
end subroutine

在上面的示例中,假设大小的数组的访问区域a我们的do concurrent无法在编译时确定结构,因为元素索引位置取自另一个数组b在例程之外初始化。当此类代码按以下方式编译时,这不再是问题:

nvfortran -stdpar -gpu=unified example.f90

关键的一点是,编译器不再需要对在循环中访问的数据段有精确的了解。GPU 和 CPU 之间的自动数据传输现在由 CUDA 运行时无缝处理。

OpenACC

现在,在统一内存模式下,OpenACC 程序不再需要显式数据子句和指令。现在,所有变量都可以从 OpenACC 计算区域访问。此实现严格遵循 OpenACC 规范中详细说明的共享内存模式。

以下 C 语言示例展示了 OpenACC 并行循环区域,该区域现在无需任何数据子句即可在 GPU 上正确执行:

void set(int* ptr, int i, int j, int dim){
  int idx = i * dim + j;
  ptr[idx] = someval(i, j);
}

void fill2d(int* ptr, int dim){
#pragma acc parallel loop
  for (int i = 0; i < dim; i++)
    for (int j = 0; j < dim; j++)
      set(ptr, i, j, dim);
}

在 C/C++中,当传递给函数时,原生语言数组会隐式衰减为指针。因此,在函数调用期间不会保留原始数组的形状和大小信息。此外,具有动态大小的数组由指针表示。使用指针会给自动代码优化带来重大挑战,因为编译器缺乏有关原始数据的基本信息。

虽然 OpenACC 编译器大力支持检测在循环中访问的数据段,以隐式将数据移动到 GPU,但在这种情况下,它无法确定数据段,因为数组是通过ptr在另一个函数中set在循环内调用。以前,无法在 C 中支持此类情况。但是,启用#unified memory mode (统一内存模式)后,此类示例现已完全支持,如下所示:

nvc -acc -gpu=unified example.c

没有-gpu=unified确保此示例正确性的唯一方法是使用 pragma 指令更改该行:

#pragma acc parallel loop create(ptr[0:dim*dim]) copyout(ptr[0:dim*dim])

这将明确指示 OpenACC 实现在并行循环中使用的精确数据段。

下面的 Fortran 示例说明了如何在 OpenACC 例程中访问全局变量,而无需任何显式注释。

module m
integer :: globmin = 1234
contains
subroutine findmin(a)
!$acc routine seq
  integer, intent(in)  :: a(:)
  integer :: i
  do i = 1, size(a)
    if (a(i) .lt. globmin) then
      globmin = a(i)
    endif
  end do
end subroutine
end module m

编译此示例后,如下所示,源代码不需要任何 OpenACC 指令即可访问模块变量globmin在从 CPU 和 GPU 调用的例程中读取或更新其值。

nvfortran -acc -gpu=unified example.f90

此外,globmin将从 CPU 和 GPU 创建到完全相同的变量实例,使其值自动同步。以前,只能通过添加 OpenACC 的组合来实现这种行为declareupdate源代码中的指令。

在使用 -gpu=unified OpenACC 运行时,利用数据操作信息(如 create/delete 或 copyin/copyout)作为优化,通过内存提示 API 指示 CUDA 运行时的首选数据放置位置。有关更多详细信息,请参阅借助异构内存管理简化 GPU 应用程序开发

此类操作可以来自源代码中的显式数据子句,也可以由编译器隐式确定。这些优化可通过最大限度地减少%自动数据迁移量来微调应用程序性能。

对于上面的 C 示例,在添加数据子句时create(ptr[0:dim*dim])copyout(ptr[0:dim*dim])是可选的,-gpu=unified、在 OpenACC 并行循环指令中使用它们可能会导致性能提升。

CUDA Fortran

添加 -gpu=unified 还通过消除对 CPU 声明的变量的限制来简化 CUDA Fortran 编程,这些变量作为参数传递给在 GPU 上执行的全局或设备例程。此外,它现在允许在此类例程中引用模块或通用块变量,而无需显式属性。此更改不会影响使用现有数据属性(设备、托管、常量、共享或固定)显式标注的变量。

module m
integer :: globval
contains 
attributes(global) subroutine fill(a)
  integer :: a(*)
  i = threadIdx%x
  a(i) = globval
end subroutine
end module m
program example
  use m
  integer :: a(N)
  globval = 123
  call fill<<<1, N>>> (a)
  e = cudaDeviceSynchronize()
end program

在上述示例中,使用 CPU 代码中分配的全局变量 globval 的值,在 GPU 的内核填充中初始化 CPU 堆栈分配数组 a.如图所示,作为在 GPU 上执行的入口点的内核例程现在可以直接访问常规 CPU 主机中声明的变量。

编程模型中常见的详细信息

未使用新的-gpu=unified在具有和不具有统一显存的系统上,flag 将保留其现有的性能特征。但是,编译时使用的二进制文件-gpu=unified无法保证在没有统一显存能力的情况下正确执行目标。在链接统一显存目标的最终二进制文件时,-gpu=unified必须在 linker 命令行中执行。

许多应用程序过渡到具有统一显存的架构后,可以使用-gpu=unified此外,stdpar C++和 CUDA Fortran 目标文件(无论是否编译)-gpu=unified可以链接在一起。但是,链接包含 OpenACC 指令或 Fortran DC 的目标文件的编译方式有所不同,有和没有-gpu=unified目前不受支持。

目前,通过适用于所有支持统一内存的编程模型的 CUDA 显存提示 API,以及适用于 OpenACC 程序的数据指令,可以对显存使用情况进行手动性能调优。

HPC SDK 将在即将发布的版本中继续增强对统一内存的支持。有关此新功能的当前状态、限制和未来更新的详细信息,请参阅 NVIDIA HPC SDK 文档

总结

本文中介绍的功能和性能只是 NVIDIA Grace Hopper 超级芯片架构和 NVIDIA 软件堆栈为开发者带来的开端。驱动程序、CUDA 软件堆栈和 NVIDIA HPC 编译器的未来开发有望消除对用户编写代码方式的更多限制,并提高生成应用程序的性能。

SPEC 和 SPECaccel 是 标准性能评估公司

 

Tags