第6章 CUDA内存处理 摘录

6.1 简介

抽象已经在现代程序语言中成为一种趋势。它使程序员离底层硬件越来越远,导致程序员不必过多了解底层硬件就可以编写程序。编译器将上层的抽象转换成底层硬件能够理解的语言。

两个比较重要的概念,一个是存储带宽(memeory bandwidth),即在一定时间内从DRAM中读出或写入的数据量。一个是延迟(latency),即响应一个获取内存的请求所花费的时间,通常这个时间会是上百个处理器周期。

从内存总线带宽与内存设备时钟频率的角度来看,处理器的设计也出现了相同的权衡点。设备上硅片占用的空间是有限的,外部内存总线的宽度通常会受到处理器上物理引脚数目的限制。

另一个需要理解的概念为事务开销(transaction overload)。有些内存事务,相较于处理它们的开销来说,是属于轻量级的。为了获得最高的存储效率,GPU需要大量的庞大事务和尽可能少的轻量级事务。

6.2 高速缓存

高速缓存是硬件上非常接近处理器核的高速存储器组。高速缓存的最大速度与缓存的大小成反比关系。一级缓存是最快的,但它的大小一般限制在16kb,32kb或64kb。

程序员主要托管GPU的缓存是共享内存区。

每个SM有一个一级缓存。在所有的SM有一个共享的二级缓存。但目前架构取消了二级缓存。而是让所有的SM直接去全局内存相连接。

缓存在SM直接共享有什么意义呢?主要是为了让设备之间能够通过相同的共享缓存进行通信。共享缓存允许处理器之间不需要每次通过全局内存进行通信。这在进行原子操作的时候特别有用。

数据存储类型:

存储类型寄存器共享内存纹理内存常量内存全局内存
带宽~8TB/s~1.5TB/s~200MB/s~200MB/s~200MB/s
延迟1个周期1~32个周期400~600个周期400~600个周期400~600个周期

大多数CUDA程序都是逐渐成熟的。一开始使用全局内存初始化,初始化完毕之后再考虑使用其他类型的内存。例如,零复制内存、共享内存、常量内存、最终寄存器也考虑进来。

为优化一个程序,在程序之初考虑使用速度较快的存储器,并且准确知道在何处以及如何提高程序性能,而不是在程序写完之后才想到用那些存储器对程序进行优化。另外,不仅要考虑高效的访问全局内存,也要时刻想办法减少对全局内存的访问次数,尤其是在数据会被重复利用的时候。

6.3 寄存器的用法

CPU与GPU架构的一个主要区别就是CPU与GPU映射寄存器的方式。CPU是通过寄存器重命名和栈来实现多线程。GPU是利用多线程隐藏了内存获取与指令带来的延迟。因此,在GPU上开启过少的线程反而会因为等待内存事务使GPU处于闲置状态。此外,GPU也不使用寄存器重命名的机制,而是致力为每一个线程都分配真实的寄存器。当需要上下文切换时,所需要的的操作就是将指向当前的寄存器组的选择器更新,以指向下一个执行的线程束的寄存器组,因此几乎是零开销。

在SM层,线程块即若干个独立线程束的逻辑组。编译时会计算出每个内核需要的寄存器数目。所有的线程块都具有相同的大小,并拥有已知数目的线程,每个线程块需要的寄存器数目是已知和固定的。因此,GPU就能为在硬件上调度的线程块分配固定数目的寄存器组。

然而在线程层,这些细节对于程序员是完全透明的。如果一个内核函数中的每个线程需要的寄存器过多,在每个SM中GPU能够调用的线程块数目就会受到限制,因此总的执行的线程数目也会受到限制。开启的线程数量过少会造成硬件无法被充分利用,性能急剧下降,但开启过多又意味着资源可能短缺,调度到SM上的线程块数量会减少。

若每个SM最多运行1536个线程,一般而言,占用率越高,程序运行越快。当线程级的并行(Thread-Level Parallelism)足以隐藏存储延迟的程度达到一个临界点时,如果想继续提高程序的性能,要么考虑更大块的存储事务,要么引进ILP,即单个线程处理数据集的多个元素。

循环一般非常低效,因为它们会产生分支,造成流水线停滞。更重要的是,它们消耗指令但并不是为了得出最终的结果。

6.4 共享内存

共享内存实际上是可受用户控制的一级缓存。然而,GPU执行的是一种内存的加载/存储模型,即所有的操作都要在指令装入寄存器之后才能执行。因此,加载数据到共享内存与加载数据到寄存器中不同,只有当数据重复利用、全局内存合并、或线程之间有共享数据时使用共享内存才更合适。否则,直接将数据从全局内存加载到寄存器性能会更好。

共享内存是基于存储体切换的架构(bank-switched architecture)。无论有多少线程发起操作,每个存储体每个周期至执行一次操作。因此,如果线程束中每个线程访问一个存储体,那么所有线程的操作都可以在一个周期内同时执行。此时无需顺序地访问,因为每个线程访问的存储体在共享内存中都是独立的,互不影响的。实际上,在每个存储体与线程之间有交叉开关将它们连接。

同常量内存一样,当所有线程访问同一地址的存储单元时,会触发一个广播机制到线程束的每个线程中。

6.4.1 使用共享内存排序

快速排序是串行编程中比较受到青睐的排序算法。作为一个分治算法,快速排序看起来也可以用并行实现。然而,快速排序默认涉及到递归调用。当每层进行一连串的内核调用。数据会导致分支,对于GPU是不利的。

归并排序是并行世界里常见的算法。它递归地将数据划分成为越来越小的数据包,直到剩下两个数值需要排序。然后再将排好序的列表合并起来,已产生一个整体的,排好序的列表。

基数排序是一个固定迭代次数和一致执行流的算法。它通过从最低有效位到最高有效位一一进行比较,对数值进行排序。对于一个32位的整数,使用一个基数位,无论数据集多大,整个排序需要迭代32次。

6.4.8 共享内存小结

        当数据有时间局部性时候,全局内存访问不连续时,线程块内有通信时,必须采用共享内存;尽量保证线程束读写的共享内存在不同的bank内,不必强求行连续,因为bank有自己的地址解码器。

        当数据的时间局部性仅在线程自身,可不用共享内存;当数据只有空间局部性时,线程束访问可确保利用空间局部性,全部给L1缓存来加载即可。

6.5 常量内存

常量内存其实只是全局内存的一种虚拟地址形式,并没有特殊保留的常量内存块。常量有两个特征:一个是高速缓存,另一个是它支持将单个值广播到线程束中的每个线程。

常量内存,是只读内存。这种类型的内存要么是在编译时声明为只读内存,要么是在运行时通过主机端定义为只读内存。在编译时,需要加上关键字__constant__。如果要在运行时改变常量内存区的内容,只需在调用GPU内核之前简单地调用cudaCopyToSymbol()函数。

对于非均匀的常量内存访问,如果缓存没有命中所需的数据,将导致N次对全局内存的访问,而不单是从常量缓存上获取数据。因此,对于那些数据不太集中或数据重用率不高的内存访问,尽量不要使用常量内存。

只要有足够多的线程束供SM调度,通过线程束的任务交换,对于全局内存的几百个周期的访问延迟能很好的被隐藏。因此使用常量内存的高速缓存这一特性带来的好处取决于从全局内存获取数据的时间以及算法中数据重复利用的程度。

目前大多数算法都会将它们的数据从一个很大的问题分解成一个很小的块。此时需要进行数据划分。数据划分时候需要注意一下几点:
        1. 数据块的大小;
        2. 数据块之间的通信;
        3. 负载平衡;

针对常量内存进行分块,意味着划分的每个块的大小不能大于64KB。块的大小最好为8KB或者更小。有时分块需要对边界出现的重影区(halo region)或者假象单元(ghost cell)进行处理,使值能够在块之间共享。如果出现重影区,大块通常比小块效果更好,因为大块需要通信的区域变小了。其中针对重影区的解决方案通常包含将重影单元复制到常量内存或共享内存的操作。

6.5.3 运行时进行常量的更新

注意cudaMemcpy函数的工作原理。该函数可以将数据复制到GPU上任何以全局符号命名的内存区域,无论该符号是全局内存还是常量内存。

内存的分配,事件的创立,销毁,以及主循环之外的设备内存释放都非常消耗cpu的运行时间。记住总是在最初进行分配操作,在最后进行销毁或释放操作。绝不可以在循环体内部进行这些工作。

常量内存的缓存是针对线性访问优化的,也就是说,每次访问都是基于缓存行读取的。因此,靠近初始访问数据将被缓存。不基于缓存行的访问将导致缓存未命中,造成的损失比直接访问全局内存而未发生缓存命中的损失还要大。

6.6 全局内存

GPU的全局内存之所以是全局的,主要是因为GPU和CPU都可以对它进行写操作。任何设备都可以通过PCI-E总线对其进行访问。GPU之间不通过CPU,直接将数据从一块GPU传输到另一块GPU卡中。这种点对点的特性是在CUDA4.0引入的。

CPU主机端处理器可以通过三种方式对GPU的内存进行访问:

        1.显式地阻塞传输;
        2.显式地非阻塞传输;
        3.隐式地使用零内存复制;

对齐访问含义就是如果”内存事务“(32和128字节两种)的访问首地址是缓存粒度(L1的128字节或L2缓存的32字节)的偶数倍,即实现了对齐访问。

合并访问就是所有线程访问连续的对齐的内存块。

6.6.1 记分牌

GPU遵循了一种惰性计算模型,只有在真正需要变量内容进行计算时才会到内存中取值。

只要将内存获取延迟与其他GPU计算指令时间重叠,从而降低了内核中存储延迟带来的影响。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值