【CUDA编程】【9】【3.Programming Interface】【3.2.CUDA Runtime】【3.2.6.Page-Locked Host Memory】

Page-Locked Host Memory,Portable Memory,Write-Combining Memory,Mapped Memory

Posted by x-jeff on November 16, 2024

【CUDA编程】系列博客参考NVIDIA官方文档“CUDA C++ Programming Guide(v12.6)”
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.Page-Locked Host Memory

运行时提供了几种函数,允许使用页锁定(page-locked,也称pinned)host内存(和通过malloc()分配的常规的可分页host内存不同):

  • cudaHostAlloc()cudaFreeHost()用于分配和释放页锁定host内存。
  • cudaHostRegister()页锁定一段由malloc()分配的内存。

使用页锁定host内存有以下几个好处:

  • 在某些device上,页锁定host内存和device内存之间的复制操作可以和kernel执行并发进行。
  • 在某些device上,页锁定的host内存可以映射到device的地址空间,从而消除将数据在host内存和device内存之间拷贝的需求。
  • 在使用前端总线的系统上,如果host内存被分配为页锁定,则host内存和device内存之间的带宽会更高。如果采用了写合并(write-combining),带宽甚至更高。

前端总线(front-side bus)是计算机系统中的一种总线架构,它用于连接CPU和内存控制器。前端总线主要负责在CPU与内存、以及其他设备(如外部总线、显卡等)之间传输数据。

注意:

页锁定的host内存不会缓存在非I/O一致的Tegra设备上。此外,非I/O一致的Tegra设备不支持cudaHostRegister()

2.Portable Memory

默认情况下,页锁定内存只适用于分配它的device(以及共享相同统一地址空间的所有device,如果有的话)。如果要让所有device共享这块页锁定内存,必须将标志cudaHostAllocPortable传给cudaHostAlloc()或将标志cudaHostRegisterPortable传给cudaHostRegister()

3.Write-Combining Memory

默认情况下,页锁定host内存是可缓存的。可以通过将标志cudaHostAllocWriteCombined传给cudaHostAlloc()将页锁定host内存分配为写合并(write-combining)内存(不可缓存)。写合并内存不占用host的L1和L2缓存,使应用程序的其他部分可以使用更多的缓存。此外,在通过PCI Express总线的传输过程中,写合并内存不会被窥探(snooped,即内存一致性不需要频繁检查),这可以提高传输性能多达40%。

从host读取写合并内存的速度非常慢,因此通常将写合并内存用于host只进行写操作的场景。

应避免在写合并内存(WC memory,即Write-Combining Memory)上使用CPU的原子指令,因为并非所有CPU都保证支持此功能。

4.Mapped Memory

可以通过将标志cudaHostAllocMapped传给cudaHostAlloc()或将标志cudaHostRegisterMapped传给cudaHostRegister(),将一块页锁定host内存映射到device的地址空间。因此,这块内存通常有两个地址:一个在host内存中,由cudaHostAlloc()malloc()返回;另一个在device内存中,可以通过cudaHostGetDevicePointer()获取,并在kernel中使用。唯一的例外是,如果使用cudaHostAlloc()分配内存并且系统支持统一虚拟地址空间(Unified Virtual Address Space)时,host和device之间的地址是统一的,也就是说,host和device访问同一个指针,不需要额外的地址转换,这个时候,host和device使用相同的虚拟地址访问内存。

直接从kernel中访问host内存虽然带宽不如device内存,但有以下一些优势:

  • 无需在device内存中分配block,并在host内存和device内存之间复制数据,数据传输会根据需要自动执行。
  • 不需要使用CUDA stream来重叠kernel执行和数据传输(“重叠”指的是不同操作在同一时间并行进行),因为kernel发起的数据传输会自动与kernel执行重叠进行。

由于映射的页锁定内存在host和device之间共享,应用程序必须使用stream和event来同步内存访问,以避免潜在的读后写(read-after-write)、写后读(write-after-read)或写后写(write-after-write)的风险。

为了能够检索任何映射的页锁定内存的device指针,在执行其他CUDA调用之前,必须调用cudaSetDeviceFlags()并传入cudaDeviceMapHost标志来启动页锁定内存映射。否则,cudaHostGetDevicePointer()将返回错误。

cudaHostGetDevicePointer()还会在设备不支持映射的页锁定host内存时返回错误。应用程序可以通过canMapHostMemory来检查device是否支持该特性,如果device支持映射的页锁定host内存,则该值为1。

需要注意的是,在映射的页锁定内存上运行的原子操作从host或其他device的角度来看不是原子的。

此外,CUDA运行时要求所有从device发起的对host内存的加载和存储操作,其数据大小为1字节、2字节、4字节、8字节或16字节时,必须遵守自然对齐规则。自然对齐规则是指数据在内存中的地址必须是该数据大小的倍数,比如,1字节的数据可以存储在任何地址,2字节的数据必须存储在偶数地址(即地址能够被2整除),4字节的数据必须存储在能够被4整除的地址,依此类推。无论是从host的角度还是从其他device的角度来看,对于自然对齐的1字节、2字节、4字节、8字节或16字节的加载和存储操作,CUDA都将其视为单次内存访问。也就是说,这些内存操作不会被拆分或分解为多个更小的内存访问操作。这样可以确保访问的效率和一致性。在某些硬件平台上,原子操作可能会被分解为单独的加载和存储操作。这意味着,一个看似是原子性的内存操作(如“读取-修改-写入”),可能会被硬件分解为多个步骤:例如,先从内存加载数据,然后进行修改,再将修改后的数据写回内存。即使这些操作被分解,每个分解的加载和存储操作仍然必须满足自然对齐的要求,以确保数据的一致性和正确性。CUDA运行时不支持某些特定的PCI Express总线拓扑结构,例如,某些PCI Express拓扑可能会将8字节的自然对齐操作拆分为两个4字节的操作,或者将16字节的操作拆分为多个更小的操作,这样会导致性能问题或操作失败。NVIDIA明确指出,它并不支持这种情况,并且目前也没有发现任何硬件拓扑会将16字节的自然对齐操作分解。