CUDA --- Fundamental Optimization Strategies

背景简介

2006年11月,Nvidia提出了CUDA;在几年5月份的GPU技术大会上,CUDA 9正式发布,新增加了许多功能,也变得更加强大。CUDA本身支持C、 C++、 Fortran等多种高级语言,因为我个人学习的是CUDA C,并且各种高级语言在优化方法思想上一致的,所以下面介绍的都是基于CUDA C。

CUDA C 与 C

CUDA C与C语言是很相似的,但二者也有区别。区别之一即是CUDA C的代码会分为host和device两部分,host部分与一般的c语言区别不大,device部分即是要编写的内核函数部分,显著标志便是相比C语言的函数有前缀global(见图1),当然如图2所示,还有另外两个,一个是device,一个是host。这三者的区别在于由host和devic哪一方调用,在哪一方上执行,具体的见图2。

图1

图2

图1所示的代码片段中,可以分为这样几个步骤:将数据从CPU拷贝到GPU;GPU上执行kernel;将结果返回给CPU;释放占用的内存显存等收尾处理措施,也就是图3概括的这样。从图1所示的代码片段中,可以注意到kernel调用时,与C语言的不同之处在于多了三个尖括号,括号内部的内容是grid与block,这二者是一个三维的变量,具体指示每个grid有多少个block,每个block有多少个thread。如图4所示,因为是三维的,所以需要坐标ID来区别不同的block与thread,当然这个图所示的grid与block均是二维的,另一维被设置成了1。

图3
图4

上面讲到的grid,block,thread是软件层面的概念,与之相对应的硬件层面(见图5)还有CUDA core(或者叫SP,即streaming processor),SM(streaming multiprocessor)和整个device。可以简单的认为这三者是一一对应的,当然细究起来还是有差别的。一个thread会交给一个CUDA Core处理,一个block会占用一个SM直到整个程序执行完成;32个thread会组成一个warp,warp是调度的基本单位,warp内的不同thread会基于各自不同的数据执行相同的指令,也就是SIMT(single instruction multiple threads)。在编写CUDA程序时,每个grid多少Block,每个block多少thread可以人为指定,只要不超出受硬件限制的上限即可。每个thread有各自的寄存器和local Memory,整个block有shared memory,所有的block均可以访问global memory, 因为硬件限制,寄存器数量和shared memory大小均是有上限的,每个线程占用的这些资源会变化,当占用的多时,thread的数量就少,warp数量也就会少,但warp中的thread在执行时可能会stall,此时warp scheduler可以调用其他warp,并且这种调度是没有开销的,因为硬件资源shared memory已经分配给每个block了。

图5

在CUDA优化过程中,有如下几个阻碍:

  1. Compute resource
  2. memory bandwidth
  3. Instruction and memory latency

针对上述这些阻碍,会有如下措施进行优化:

优化措施

减少host和device之间的数据交换

从图6中可以看出,CPU和GPU之间通过PCIe进行数据交换,速度为8GB/s;GPU和GPU Memory通过GDDR5进行数据交换,速度为144GB/s,这二者之间速度的不匹配会造成整体速度受限,因此要尽量减少GPU和CPU之间的数据交换。比如中间结果可以直接在GPU上进行缓存再计算等;许多小的数据交换由一次大的数据交换代替以此来减少数据交换次数。

扫描二维码关注公众号,回复: 2631664 查看本文章

图6

在初学CUDA时遇到的最基础的几个命令之一cudaMemcpy,在进行从CPU拷贝到GPU的过程中,因为CPU端是分页管理存储,虚拟内存技术来实现下层的硬盘来扩展内存的容量,因此可能存在的情况就是要拷到GPU的数据因为虚拟内存技术,此时物理上并没有存在内存中,会带来数据上的一些不太安全的问题(具体是啥暂时还不清楚),所以CPU会先将数据从pageble Memory拷贝到pinned Memory中去,pinned memory顾名思义就是固定在内存中,不会被分配到下层硬盘中去,然后再与GPU进行数据传送。

CUDA对此可以做出的优化就是直接将数据固定在pinned Memory中,节省从pageable memory拷到pinned memory的时间。在此基础上进一步提出的技术zero-copy Memory,通过cudaGetDevicePointer命令直接获得指向CPU端数据的指针,而不需通过cudaMemcpy进行拷贝。当然这一措施也存在缺陷,因为内存的容量总归是有限的,所以pinned Memory的数量和大小都会受到限制,并且建议使用完毕后立即释放。

针对Global Memory进行的优化

避免Global memory带宽浪费需要注意以下两点:

  1. Aligned
    memory accesses occur when the first address of a device memory
    transaction is an even multiple of the cache granularity being used to
    service the transaction
  2. Coalesced memory accesses occur when all 32 threads in a warp access a contiguous chunk of memory.

针对Shared Memory进行的优化

在矩阵相乘的例子中,我们通过A矩阵的行乘以B矩阵的列得到一个结果,然后计算另一个结果时,相同的一行会被再次读取,这会造成带宽的浪费;通过将行存到shared memory中,相对global memory可以节省一部分时间。

在使用shared
memory时要注意bank conflict的问题:shared
memory通常会被分成32部分,每一部分称为bank,如果同一warp中的不同thread请求的数据位于同一bank不同的地址时,会产生bank
conflict。在正常情况下,不同的thread读取不同的bank是并行执行的,如果出现了bank
conflict会造成串行执行的情况,这会造成带宽的浪费。

猜你喜欢

转载自blog.csdn.net/konghaocc/article/details/78515478
今日推荐