下面是一个使用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的反过程。