【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字节的自然对齐操作分解。