CUDA_主机内存
主机内存
系统中被CPU访问的内存,分为两种类型:可分页内存(pageable memory,一般应用中默认使用)和页锁定内存(page-locked或者pinned)。
可分页内存即为通过操作系统api(malloc(), new())分配的存储器空间;而页锁定内存始终不会被分配到低速的虚拟内存中,能够保证存在于物理内存中,并且能够通过DMA加速与设备端的通信。
为了让硬件使用DMA,操作系统允许主机内存进行页锁定,并且因为性能原因,CUDA包含了开发者使用这些操作系统工具的API。页锁定后的且映射为cuda直接访问的锁定的内存允许以下几点:
- 更快的传输性能;
- 异步的复制操作(在必要的内存复制结束之前内存复制返回控制给调用者;GPU复制操作与cpu并行的执行);
- 映射锁页内存可以被cuda内核直接访问
主机端锁页内存
使用pinned memory有很多好处:可以达到更高的主机-设备端的数据传送带宽,如果页锁定内存以write-commbined方式分配,带宽更高;某些设备支持DMA功能,在执行内核函数的同时利用pinned memory进行主机端与设备端之间的通信;在某些设备上,pinned memory还可以通过zero-copy功能映射到设备地址空间,从GPU直接访问,这样就不用在主存和显存之间进行数据传输。
分配锁页内存
通过cudaHostAlloc
()和cudaFreeHost
()来分配和释放pinned memory。
portable memory/可共享锁页内存
在使用cudaHostAlloc
分配页锁定内存时,加上cudaHostAllocPortable
标志,可以使多个CPU线程通过共享一块页锁定内存,从而实现cpu线程间的通信。在默认情况下,pinned memory由哪个cpu线程分配,就只有该CPU线程才能访问这块空间。而通过portable memory则可以让控制不同GPU的几个CPU线程共享同一块pinned memory,减少CPU线程间的数据传输和通信。
write-combined memory/写结合锁页内存
当CPU对一块内存中的数据进行处理时,会将这块内存上的数据缓存到CPU的L1、L2 Cache中,并且需要监视这块内存中数据的更改,以保证缓存的一致性。
一般情况下,这种机制可以减少CPU对内存的访问,但在“CPU生产数据,GPU消费数据”模型中,CPU只需要写这块内存数据即可。此时不需要维护缓存一致性,对内存的监视反而会降低性能。
通过write-combined memory,就可以不使用CPU的L1、L2 Cache对一块pinned memory中的数据进行缓冲,而将Cache资源留给其他程序使用。
write-combined memory在PCI-e总线传输期间不会被来自CPU的监视打断,可以将主机端-设备端传输带宽提高多达40%。
在调用cudaHostAlloc
()时加上cudaHostAllocWriteCombined
标志,就可以将一块pinned memory声明为write-combined memory.
由于对write-combined memory的访问没有缓存,CPU从write-combined memory上读数据时速度会有所下降。因最好只将CPU端只写的数据存放在write-combined memory中。
mapped memory/映射锁页内存
mapped memory拥有两个地址:主机端地址(内存地址)和设备地址(显存地址),可以在kernel中直接访问mapped memory中的数据,而不必再在内存和显存间进行数据拷贝,即zero-copy功能。如果内核程序只需要对mapped memory进行少量读写,这样做可以减少分配显存和数据拷贝的时间。
mapped memory在主机端的指针可以由cudaHostAlloc
()函数获得;它的设备端指针可以通过cudaHostGetDevicePointer
()获得,从kernel中访问页锁定内存,需要将设备端指针作为参数传入。
并不是所有的设备都支持内存映射,通过cudaGetDeviceProperties
()函数返回的cudaMapHostMemory
属性,可以知道当前设备是否支持mapped memory。如果设备提供支持,可以在调用cudaHostAlloc
()时加上cudaHostAllocMapped
标志,将pinned memory映射到设备地址空间。
由于mapped memory可以在CPU端和GPU端访问,所以必须通过同步来保证CPU和GPU对同一块存储器操作的顺序一致性。可以使用流和事件来防止读后写,写后读,以及写后写等错误。
对mapped memory的访问应该满足与global memory相同的合并访问要求,以获得最佳性能。
注意:
- 在执行cuda操作之前,先调用
cudaSetDeviceFlags()
(加cudaDeviceMapHost
标志)进行锁页内存映射。否则,调用cudaHostGetDevicePointer()
函数会返回一个错误。 - 多个主机端线程操作的一块portable memory同时也是mapped memory时,每一个主机线程都必须调用cudaHostGetDevicePointer()获得这一块pinned memory的设备端指针。此时,这一块内存在每个主机端线程中都有一个设备端指针。
注册锁页内存
锁页内存注册将内存分配与页锁定和主机内存映射分离。可以实现操作一个已分配的虚拟地址范围,并页锁定它。然后,将其映射给GPU,正如cudaHostAlloc()
可以根据需要让内存映射到cuda地址空间或变成可共享的(所有GPU可访问)。
函数cuMemHostRegister()/cudaHostRegister()
和cuMemHostUnregister()/cudaHostUnregister()
分别实现吧主机内存注册为锁页内存和移除注册的功能。
注册的内存范围必须是页对齐的;无论是基地址还是大小,都必须是可被操作系统页面大小整除。
注意:
当UVA(同一虚拟寻址)有效,所有的锁页内存分配均是映射的和可共享的。这一规则的例外是写结合内存和注册的内存。对于这二者,设备指针可能不同于主机指针,应用程序需要使用cudaHostGetDevicePointer()/cuMemHostGetDevicePointer()查询设备指针。