CUDA Libraries简介
上图是CUDA 库的位置,本文简要介绍cuSPARSE、cuBLAS、cuFFT和cuRAND,之后会介绍OpenACC。
- cuSPARSE线性代数库,主要针对稀疏矩阵之类的。
- cuBLAS是CUDA标准的线代库,不过没有专门针对稀疏矩阵的操作。
- cuFFT傅里叶变换
- cuRAND随机数
CUDA库和CPU编程所用到的库没有什么区别,都是一系列接口的集合,主要好处是,只需要编写host代码,调用相应API即可,可以节约很多开发时间。而且我们完全可以信任这些库能够达到很好的性能,写这些库的人都是在CUDA上的大能,一般人比不了。当然,完全依赖于这些库而对CUDA性能优化一无所知也是不行的,我们依然需要手动做一些改进来挖掘出更好的性能。
下图是《CUDA C编程》中提到的一些支持的库,具体细节可以在NVIDIA开发者论坛查看:
如果大家的APP属于上面库的应用范围,非常建议大家使用。
A Common Library Workflow
下面是一个使用CUDA库的具体步骤,当然,各个库的使用可能不尽相同,但是不会逃脱下面的几个步骤,差异基本上就是少了哪几步而已。
- 创建一个库的句柄来管理上下文信息。
- 分配device存储空间给输入输出。
- 如果输入的格式并不是库中API支持的需要做一下转换。
- 填充device Memory数据。
- 配置library computation以便执行。
- 调用库函数来让GPU工作。
- 取回device Memory中的结果。
- 如果取回的结果不是APP的原始格式,就做一次转换。
- 释放CUDA资源。
- 继续其它的工作。
下面是这几个步骤的一些细节解释:
Stage1:Creating a Library Handle
CUDA库好多都有一个handle的概念,其包含了该库的一些上下文信息,比如数据格式、device的使用等。对于使用handle的库,我们第一步就是初始化这么一个东西。一般的,我们可以认为这是一个存放在host对程序员透明的object,这个object包含了跟这个库相关联的一些信息。例如,我们可定希望所有的库的操作运行在一个特别的CUDA stream,尽管不同的库使用不同函数名字,但是大多数都会规定所有的库操作以一定的stream发生(比如cuSPARSE使用cusparseSetSStream、cuBLAS使用cublasSetStream、cuFFT使用cufftSetStream)。stream的信息就会保存在这个handle中。
Stage2:Allocating Device Memory
本文所讲的库,其device存储空间的分配依然是cudaMalloc或者库自己调用cudaMalloc。只有在使用多GPU编程的库时,才会使用一些定制的API来实现内存分配。
Stage3:Converting Inputs to a Library-Supported Format
如果APP的数据格式和库要求的输入格式不同的话,就需要做一次转化。比如,我们APP存储一个row-major的2D数组,但是库却要求一个column-major,这就需要做一次转换了。为了最优性能,我们应该尽量避免这种转化,也就是尽量和库的格式保持一致。
Stage4:Populating Device Memory with Inputs
完成上述三步后,就是将host的数据传送到device了,也就是类似cudaMemcpy的作用,之所说类似,是引文大部分库都有自己的API来实现这个功能,而不是直接调用cudaMemcpy。例如,当使用cuBLAS的时候,我们要将一个vector传送到device,使用的就是cubalsSetVector,当然其内部还是调用了cudaMemcpy或者其他等价函数来实现传输。
Stage5:Configuring the Library
有步骤3知道,数据格式是个明显的问题,库函数需要知道自己应该使用什么数据格式。某些情况下,类似数据维度之类的数据格式信息会直接当做函数参数配置,其它的情形下,就需要手动来配置下之前说的库的handle了。还有个别情况是,我们需要管理一些分离的元数据对象。
Stage6:Executing
执行就简单多了,做好之前的步骤,配置好参数,直接调用库API。
Stage7:Retrieving Results from Device Memory
这一步将计算结果从device送回host,当然还是需要注意数据格式,这一步就是步骤4的反过程。
Stage8:Converting Back to Native Format
如果计算结果和APP的原始数据格式不同,就需要做一次转化,这一步是步骤3的反过程。
Stage9:Releasing CUDA Resources
如果上面步骤使用的内存资源不再使用就需要释放掉,正如我们以前介绍的那样,内存的分配和释放是非常大的负担,所以希望尽可能的资源重用。比如device Memory、handles和CUDA stream这些资源。
Stage10:Continuing with the Application
继续干别的。
再次重申,上面的步骤可能会给大家使用库是非常麻烦低效的事儿,但其实这些步骤一般是冗余的,很多情况下,其中的很多步骤是不必要的,在下面的章节我们会介绍几个主要的库以及其简要使用,相信看过后,你就不会认为使用库得不偿失了。
THE CUSPARSE LIBRARY
cuSPARSE就是一个线性代数库,对稀疏矩阵之类的操作尤其独到的用法,使用很宽泛。他当对稠密和稀疏的数据格式都支持。
下图是该库的一些函数调用,从中可以对其功能有一个大致的了解。cuSPARSE将函数以level区分,所有level 1的function仅操作稠密和稀疏的vector。所有level2函数操作稀疏矩阵和稠密vector。所有level3函数操作稀疏和稠密矩阵。
cuSPARSE Data Storage Formats
稠密矩阵就是其中的值大部分非零。稠密矩阵所有值都是存储在一个多维的数组中的。相对而言,稀疏矩阵和vector中元素主要是零,所以其存储就可以做一些文章。比如我们可以仅仅保存非零值和其坐标。cuSPARSE支持很多种稀疏矩阵的存储方式,本文只介绍其中三种。
先看一下稠密(dens)矩阵的存储方式,图示很明显,不多说了:
Coordinate(COO)
对于稀疏矩阵中的每个非零值,COO方式都保存其行和列坐标,因