主流GPU编程接口
1、CUDA
是英伟达公司推出的,专门针对 N 卡进行 GPU 编程的接口。文档资料很齐全,几乎适用于所有 N 卡。
2、OpenCL
开源的 GPU 编程接口,使用范围最广,几乎适用于所有的显卡. 但相对 CUDA,其掌握较难一些,建议先学 CUDA,在此基础上进行 Open CL 的学习则会非常简单轻松。
3、DirectCompute
微软开发出来的 GPU 编程接口。功能很强大,学习起来也最为简单,但只能用于 Windows 系统,在许多高端服务器都是 UNIX 系统无法使用。
总结,这几种接口各有优劣,需要根据实际情况选用。但它们使用起来方法非常相近,掌握了其中一种再学习其他两种会很容易。
4、CUDA与OpenCL
对OpenCL而言,它是由Apple、Intel、NVIDIA和AMD等GPU制造公司联合开发的一种异构并行计算框架,它是免费开源的,并且支持的设备类型有很多种,属于一种通用性框架,具有跨平台的特性;对于CUDA而言,它仅仅是由NVIDIA公司开发的,因此它也只支持NVIDIA公司推出的GPU产品,但由于NVIDIA公司生产的GPU在市场上的份额比较大,所以CUDA完全支持目前比较主流的系统,比如:Windows、Linux和Mac OS等。
GPU编程的高速执行速度、良好的可编程性、较高的功耗比;
并行计算中的三个重要问题
1、同步问题
2、并发度
有一些问题属于 “易并行” 问题:如矩阵乘法。在这类型问题中,各个运算单元输出的结果是相互独立的,这类问题能够得到很轻松的解决 (通常甚至调用几个类库就能搞定问题)。然而,若各个运算单元之间有依赖关系,那问题就复杂了。在CUDA中,块内的通信通过共享内存来实现,而块间的通信,则只能通过全局内存。
CUDA 并行编程架构可以用网格 (GRID) 来形容:一个网格好比一只军队。网格被分成好多个块,这些块好比军队的每个部门 (后勤部,指挥部,通信部等)。每个块又分成好多个线程束,这些线程束好比部门内部的小分队
3、局部性
在GPU编程中,局部性也是非常重要的,这体现在要计算的数据应当在计算之前尽可能的一次性的送进显存,在迭代的过程中一定要尽可能减少数据在内存和显存之间的传输,实际项目中发现这点十分重要的。对于 GPU 编程来说,需要程序猿自己去管理内存,或者换句话来说,自己实现局部性。
并行计算的两种类型
1、基于任务的并行处理
这种并行模式将计算任务拆分成若干个小的但不同的任务,如有的运算单元负责取数,有的运算单元负责计算,有的负责...... 这样一个大的任务可以组成一道流水线。需要注意的是流水线的效率瓶颈在于其中效率最低的那个计算单元。
2、基于数据的并行处理
这种并行模式将数据分解为多个部分,让多个运算单元分别去计算这些小块的数据,最后再将其汇总起来。一般来说,CPU的多线程编程偏向于第一种并行模式,GPU并行编程模式则偏向于第二种。
常见的并行优化对象
1. 循环
这也是最常见的一种模式,让每个线程处理循环中的一个或一组数据。
这种类型的优化一定要小心各个运算单元,以及每个运算单元与其自身上一次迭代结果的依赖性。
2. 派生/汇集模式
该模式下大多数是串行代码,但代码中的某一段可以并行处理。
典型的情况就是某个输入队列当串行处理到某个时刻,需要对其中不同部分进行不同处理,这样就可以划分成多个计算单元对队列进行处理 (也即派生),最后再将其汇总 (也即汇集)。
这种模式常用于并发事件事先不定的情况,具有 “动态并行性”。
3. 分条/分块模式
对于特别庞大的数据 (如气候模型),可以将数据分为过个块来进行并行计算。
4. 分而治之
绝大多数的递归算法,比如快速排序,都可以转换为迭代模型,而迭代模型又能映射到 GPU 编程模型上。
特别说明:虽然费米架构和开普勒架构的 GPU 都支持缓冲栈,能够直接实现递归模型到 GPU 并行模型的转换。但为了程序的效率,在开发时间允许的情况下,我们最好还是先将其转换为迭代模型。
GPU并行编程的存储系统架构
第一层:寄存器
每个流处理器中的寄存器数以千计,每个线程都能分配到其私有的寄存器,这样做的好处是使得线程的切换几乎是零开销 (也许说是线程束的切换会更为准确)。
应当在硬件条件允许的情况下,尽可能地使用寄存器 (注意是硬件条件的允许之下)。
在核函数中定义的变量就是寄存器变量。
第二层:共享内存
共享内存的本质是可受用户控制的一级缓存。每个 SM (特殊存储器)中的一级缓存与共享内存共享一个64 KB的内存段。在费米架构中,可以为每个块定义16 KB的共享内存。灵活地使用共享内存,能够大幅度提高显存的带宽。此外,共享内存也是实现块内线程间通信的有效工具。
使用时需要注意的一个地方是,只有在确定需要重复利用此空间的数据,或者明确要使块内线程进行通信的前提下,才考虑使用共享内存。(原因不解释)
使用时需要注意的另一个地方是应当尽可能地避免存储体冲突。这里所谓的存储体是指实现共享内存的硬件 - 一个费米架构的设备上有 32 个存储体。解决此问题的关键在于:顺序访问存储体。
实际开发中,常常将一个任务分解成多个部分(不论是任务分解还是数据分解),共享内存在其中扮演着任务块工作任务汇总或者数据块工作任务汇总的角色。
核函数中定义的变量加上__shared__声明后就会存放在共享内存中了。
第三层:常量内存
常量内存其实只是全局内存的一种虚拟地址形式,并没有特殊保留的常量内存块。
使用起来非常方便,在主机端对需要放到常量内存区的变量添加 __constant__ 关键字声明之即可。
唯独需要注意的是,如果一个常量仅仅是一个字面值,那么将它声明为宏也行,例如 PI 这样的常数就一般定义为宏。
第四层:全局内存
全局内存,也就是显存。
在主机端开辟的显存空间均属于全局内存范畴。