CUDA优化:最大化内存吞吐量(官方文档翻译)

毕业设计要翻译技术资料 3000 字,这里找了英伟达 CUDA TOOLKIT DOCUMENTATION 的 5.3 节“最大化内存吞吐量来”翻译一下,供参考,并希望此文对诸位的 CUDA 程序优化有所帮助。

 

 

5.3. 最大化内存吞吐量

最大化应用程序总内存吞吐量的第一步当是最大限度地减少低带宽的数据传输。

 

这意味着最小化主机(内存)和设备(显存)之间的数据传输,因为正如主机和设备间的数据传输中详述的那样——这样的数据传输的带宽远远低于全局内存和设备之间的数据传输。

 

这也意味着通过尽可能通过使用片上内存(on-chip memory):共享内存和缓存(即在计算能力大于等于 2.x 的设备上可用的L1 缓存和 L2 缓存,及所有设备上可用的纹理缓存(texture cache) 和常量缓存(constant cache))以最大限度地减少全局内存和设备之间的数据传输。

 

共享内存等价于“用户管理的缓存”:应用程序显式地分配和访问它。如 CUDA 运行时 所述,一个典型的编程模式是将来自设备内存的数据组织编排到共享内存中:换句话说,让一个块的每一个线程:

  1. 将数据从设备内存加载到共享内存,
  2. 与块的所有其他线程同步,以便每个线程可以安全地读取由不同线程填充的共享内存位置,
  3. 在共享内存中处理数据,
  4. 必要时再次同步,以确保共享内存已与结果一起更新,
  5. 将结果写回设备内存。

 

对于某些应用程序(例如那些全局内存访问模式依赖于数据的程序),传统的硬件管理缓存更适合利用数据的局域性。如计算能力-3-x、计算能力 7.x 和计算能力 8.x所述,对于计算能力 3.x、7.x 和 8.x 的设备, L1缓存和共享内存使用的是相同的空间,并且每次内核调用都可配置用于 L1 与共享内存的比值。

 

内核访问的吞吐量可能因不同内存的访问模式而异。因此,最大化内存吞吐量的下一步是根据 设备内存访问 中描述的最佳内存访问模式尽可能最佳地组织内存访问。这种优化对于全局内存访问尤为重要,因为与片上内存的带宽和算术指令吞吐量相比,全局内存带宽较低,因此未经优化的全局内存访问通常对性能有很高的(负面)影响。

 

5.3.1. 主机和设备之间的数据传输

应用程序应努力最大限度地减少主机和设备之间的数据传输。实现此目的的一种方法是将更多代码(计算过程)从主机移动到设备,即使这意味着运行没有展现出足够的并行性(以获得最高效能)的内核函数。你可以在设备内存中创建、在设备上运算、并销毁,而无需在主机产生映射或复制到主机内存的中间数据结构。

 

此外,由于每次传输的经常性开销(overhead),将许多次小的传输组合成单个大的数据传输中总是比单独地进行每次传输效果更好。

 

在具有前端总线(front-side bus)的系统上,使用页锁定主机内存(page-locked host memory)中描述的页面锁定主机内存可实现主机和设备之间的数据传输的更高性能。

 

此外,在使用映射的页面锁定内存(映射内存)时,无需分配任何设备内存,或明确在设备和主机内存之间拷贝数据。每次核函数访问映射内存时,都会隐式执行数据传输。若要获得最大性能,这些内存访问必须与访问全局内存一样聚合(将小访问聚合成大访问)(请参阅设备内存访问 )。假设它们这些映射内存仅读或写一次,则使用映射的页面锁定内存,相较于设备和主机内存之间的显式地拷贝,可能带来性能的提升。

 

在设备内存和主机内存实质上相同的集成系统中,主机和设备内存之间的任何拷贝都是多余的,应改为使用映射的页面锁定内存。应用可以通过检查集成设备属性(见设备枚举)是否等于 1 来查询设备是否为集成设备。

 

 

5.3.2. 设备内存访问

获取可地址指示的内存(即全局、局部、共享、常数或纹理内存)的指令可能需要多次重新发布,具体取决于线程束(warp)内线程的内存地址的分布。分布如何以这种方式影响指令吞吐量,取决于每种类型的内存,这将在以下部分进行描述。例如,对于全局存储器,一般来说,地址越分散,吞吐量就越低。

 

全局内存

全局内存在设备内存中,可通过 32、64 或 128 个字节的规格进行内存访问。这些内存规格必须天然地对齐:只有与其大小对齐的 32、64 或 128 字节(即其第一个地址是其大小的倍数)的设备内存段才能通过内存事务进行读取或写入。

 

当线程束执行访问全局内存的指令时,它会根据每个线程访问的字大小和所有线程访问的内存地址的分布,将线程束内线程的内存访问汇合成这些内存事务中的一个或多个。一般来说,传输次数越多,未被使用但被线程访问的字越多,从而相应地降低了指令的吞吐量。例如,如果一个32字节的内存访问被每个线程用4字节访问完成,则吞吐量缩减为原来的八分之一。

 

需要多少访问以及最终影响多少吞吐量因设备的计算能力不同。计算能力 3.x计算能力 5.x计算能力 6.x计算能力 7.x 和计算能力 8.x 提供了有关处理各种计算能力的全局内存访问方式的更多详细信息。

 

因此,要最大限度地提高全局内存吞吐量,必须通过:

  1. 遵循基于计算能力 3.x 计算能力 5.x计算能力 6.x计算能力 7.x 和计算能力 8.x 的最佳访问模式
  2. 使用符合下面大小和对齐要求部分中详细说明的大小和对齐要求的数据类型,
  3. 在某些情况下,例如,在访问下面的二维矩阵部分中描述的二维矩阵时,应修补数据。

 

尺寸和对齐要求

 

全局内存指令支持读取或写入大小为 1、2、4、8 或 16 字节的字。如果数据类型大小为 1、2、4、8 或 16 字节且数据自然对齐(即其地址是该大小的倍数),则(通过变量或指针)对存储于全局内存中的数据的任何访问都可编译为单个全局内存指令。

 

如果此大小和对齐要求未实现,访问将被编译为多个指令,并使用交错访问模式,以防止这些指令完全结合。因此,我们建议存储在全局内存中的数据,都符合此要求。

 

对于 内置矢量类型 而言,程序自动实现其对齐要求。

 

对于结构体,通过可以使用__align__ (8) 或__align__ (16) 的对齐指示,编译器将使之满足大小和对齐要求,例如

 

struct __align__(8) {

    float x;

    float y;

};

或者

struct __align__(16) {

    float x;

    float y;

float z;

};

存储在全局内存中的变量的任何地址,或由驱动程序或运行时 API 的内存分配函数返回的地址始终与至少 256 字节对齐。

 

读取不自然对齐的 8 字节或 16 字节字会产生不正确的结果(相差几个字),因此必须特别小心地保证这些类型的任何值或值矩阵的起始地址的对齐。一个容易忽略这种情况的典型案例是使用一些自定义的全局内存分配方案,即将多个的分配(多次调用 cudaMalloc())或 cuMemAlloc() 替换为可分区为多个矩阵的单个大块内存的分配,在这种情况下,每个矩阵的起始地址的偏移与块的起始地址的偏移一致。

 

二维矩阵

常见的全局内存访问模式是,当每个索引线程 (tx,ty) 使用以下地址访问位于类型* 的地址在 BaseAddress 的宽度 width 的二维矩阵的一个元素时(其中“类型”符合 最大化利用 中描述的要求):

BaseAddress + width * ty + tx

要使这些访问完全结合,线程格的宽度和矩阵的宽度必须是线程数大小的倍数。

特别地,这意味着,宽度不是此大小的倍数的数组,如果实际分配的宽度补足到此大小的最接近的倍数,并且按行相应地填充后,将可被更有效地访问。参考手册中描述的 cudaMallocPitch() 和 cuMemAllocPitch() 函数和相关内存拷贝函数允许程序员编写非硬件依赖的代码来分配符合这些限制的矩阵。

 

局部内存

局部内存存取只在某些自动变量出现时存在,其中自动变量在 变量内存空间指示 提及。编译器可能放置在局部内存中的自动变量有:

  1. 无法确定它们与常量大小的矩阵,
  2. 消耗太多的寄存器空间大结构体或矩阵,
  3. 任何使得内核使用超出可用的寄存器数量的变量(这也称为寄存器溢出)。

 

对 PTX 装配代码的检查(通过使用 -ptx 或 -keep 选项进行编译获得)将展示某一变量是否在第一个编译阶段被放置在局部内存中,因为它会被标记上 .local 助记符并被通过 ld.local 核st.local 助记符访问。即使没有存在于局部内存,如果发现它在所处的计算架构中消耗太多的寄存器空间,后续的编译阶段仍可能使之变为局部内存:使用 cuobjdump 对cubin对象的检查将判断是否是这种情况。此外,在使用  --ptxas-options=-v 选项进行编译时,编译器会报告每个内核 (lmem) 的局部内存总用量。请注意,某些数学函数具有可能访问局部内存的实现。

 

局部内存空间位于设备内存中,因此局部内存访问具有与全局内存访问相同的高延迟和低带宽的特性,并且受制于 设备内存访问 中描述的存储器合并的类似要求。但是,局部内存被组织为连续的 32 位字被连续的线程 ID 访问。因此,只要线程束中的所有线程访问一致的相对地址(例如,矩阵变量中的相同索引、结构体中的相同成员),访问就完全合并在一起。

 

在某些计算能力设备3.x 的设备上,局部内存访问始终以与全局内存访问相同的方式缓存在 L1 和 L2 中(参见计算能力 3.x)。

 

在计算能力 5.x 和 6.x 的设备上,局部内存访问始终以与全局内存访问相同的方式缓存在 L2 中(参见计算能力 5.x 和计算能力 6.x)。

 

共享内存

 

由于共享内存是片上存储器,因此与局部或全局内存相比,带宽要大得多,延迟也低得多。

为了实现高带宽,共享内存被划分为大小相等的内存模块,称为"库(bank)",可同时访问。因此,对于 n 地址在 n 个不同的内存库中提出的任何内存读写请求都可以同时进行响应,从而产生整体带宽,其带宽是单个模块带宽的 n 倍。

 

然而,如果存储器请求的两个地址位于同一内存库中,则存在库冲突,访问必须序列化。硬件根据需要将带有库冲突的内存请求拆分为尽可能多的独立无冲突请求,将吞吐量减少到等于独立内存请求数。如果单独的内存请求数为n,则初始内存请求被定义为 n 路库冲突。

 

因此,要获得最大的性能,了解内存地址如何映射到内存库非常重要,以便安排内存请求,从而最大限度地减少行内存库冲突。这些在计算能力 3.x计算能力 5.x计算能力 6.x计算能力 7.x 和计算能力 8.x 分别被详述。

 

常量内存

 

常量内存空间位于设备内存中,并缓存在常量缓存中。

 

一个请求被分割成与初始请求中不同的内存地址一样多的单独的请求,从而将吞吐量减少到等于单独请求数。

 

产生的请求将在常量缓存命中(cache hit)发生时的数据吞吐时响应,否则在设备内存吞吐时相应。

 

纹理和表面内存

 

纹理和表面内存空间位于设备内存中,并缓存在纹理缓存中,因此,一个纹理/表面读取仅仅在缓存未击中(cache miss)时消耗一次设备内存读取,否则只消耗一次纹理缓存读取。纹理缓存是针对二维区域优化的,因此读取二维空间中相邻的纹理或表面地址的相同线程束中的线程将实现最佳性能。此外,它专为恒定延迟的流获取而设计;缓存命中可降低 DRAM 带宽需求,但无法降低获取延迟。

 

通过纹理或表面获取读取设备内存相较于从全局或常量内存中读取设备内存这些好处:

  1. 如果内存读取不遵循全局或常量内存读取必须遵循才能获得良好的性能的访问模式,只要纹理/表面读取中有区域性,也可以实现更高的带宽:
  2. 计算部分由专用单元在内核之外执行:
  3. 打包的数据可以在单个操作中广播到不同的变量:
  4. 8 位和 16 位整数输入数据可在 [0.0、1.0] 或 [-1.0、 1.0] (参见纹理内存)范围内可选转换为 32 位浮点值。

 

 

你可能感兴趣的:(并行计算,cuda)