CUDA编程——GPU加速库和OpenACC
很多人是参考《Professional CUDA C Programming》一书来入门CUDA的,这本书本身是很好的入门材料,但由于CUDA版本迭代非常快,导致书中的一些内容已经是过时的了。这也是笔者撰写本系列博客的初衷之一,这个系列参考了本书以及CUDA 12.x的官方文档,并在每个章节都附有详细的代码参考,并且代码是基于CUDA 12.x的,可以解决一些由于版本迭代带来的问题。本系列的博客由《Professional CUDA C Programming》一书、CUDA官方文档、互联网上的一些资料以及笔者自己的理解构成,希望能对你有一些帮助,若有错误也请大胆指出。
CUDA库概述
CUDA支持的库及其作用域如下表所示。
库名 | 作用域 | 官方文档 |
---|---|---|
NVIDIA CUDA Math Library | 数学运算 | https://docs.nvidia.com/cuda/cuda-math-api/index.html |
NVIDIA cuBLAS | 线性代数 | https://docs.nvidia.com/cuda/cublas/index.html |
NVIDIA cuSPARSE | 稀疏线性代数 | https://docs.nvidia.com/cuda/cusparse/index.html |
NVIDIA CUSP | 稀疏线性代数和图形计算 | https://cusplibrary.github.io/index.html |
NVIDIA cuFFT | 快速傅里叶变换 | https://docs.nvidia.com/cuda/cufft/index.html |
NVIDIA cuRAND | 随机数生成 | https://docs.nvidia.com/cuda/curand/index.html |
NVIDIA NPP | 图像和信号处理 | https://docs.nvidia.com/cuda/npp/index.html |
MAGMA | 新一代线性代数 | https://icl.utk.edu/magma/ |
IMSL Fortran Numerical Library | 数学与统计学 | https://www.imsl.com/products/imsl-fortran-libraries |
AccelerEyes ArrayFire | 数学,信号和图像处理,统计学 | https://arrayfire.com/ |
Thrust | 并行算法和数据结构 | https://docs.nvidia.com/cuda/thrust/index.html |
Geometry Performance Primitives | 计算几何 | https://developer.nvidia.com/geometric-performance-primitives-gpp |
Paralution | 稀疏迭代方法 | https://www.paralution.com/ |
AmgX | 核心求解 | https://github.com/NVIDIA/AMGX |
CUDA的库有一些通用的工作流:
- 在库操作中创建一个特定的库句柄来管理上下文信息;
- 为库函数的输入输出分配设备内存;
- 如果输入格式不是函数库支持的格式则需要进行转换;
- 将输入以支持的格式填入预先分配的设备内存中;
- 配置要执行的库运算;
- 执行一个将计算部分交付给GPU的库函数调用;
- 取回设备内存中的计算结果(结果可能是库设定的格式);
- 如有必要,将取回的数据转换成应用程序的原始格式;
- 释放CUDA资源;
- 继续完成应用程序的其他工作。
cuSPARSE库
较新版本的cuSPARSE将API分为了两大类:
- Legacy:这部分接口是为了兼容旧版本所保留的,在未来的版本也不会改进;
- Generic:这部分是cuSPARSE的标准接口。
下面的讨论都基于Generic系列接口。
Generic系列接口大体分为几类:
- 稀疏向量与稠密向量之间的操作(
Axpby
、Gather
、Scatter
、求和、点积); - 稀疏向量与稠密矩阵之间的操作(乘积);
- 稀疏矩阵与稠密向量之间的操作(乘积、三角线性方程求解、三对角、五对角线性方程求解);
- 稀疏矩阵与稠密矩阵之间的操作(乘积、三角线性方程求解、三对角、五对角线性方程求解);
- 稀疏矩阵与稀疏矩阵之间的操作(求和、乘积);
- 稠密矩阵与稠密矩阵之间的操作,输出一个稀疏矩阵(乘积);
- 稀疏矩阵预处理(不完全Cholesky分解、不完全LU分解);
- 不同稀疏矩阵存储格式的相互转换。
cuSPARSE数据存储格式
cuSPARSE的索引有两种,从零开始和从一开始的,这是为了兼容C/C++
和Fortran
。
向量存储格式
稠密向量不过多介绍,与C/C++
的数组存储方式是一致的。
稀疏向量是借助两个数组表示的:
- 值数组
values
:存储向量中的非零值; - 索引数组
indices
:存储向量中非零值在等价稠密向量中的索引。
官方文档中的图片很好的解释了这种存储方式。
矩阵存储格式
稠密矩阵
稠密矩阵有行优先和列优先两种组织方式,通过几个参数来表示:
- 矩阵行数
rows
; - 矩阵列数
columns
; - 主维度
leading_dimension
:主维度在行优先模式下必须大于等于列数,在列优先模式下必须大于等于行数; - 值数组指针:该数组的长度在行优先模式下为
rows * leading_dimension
,在列优先模式下为columns * leading_dimension
。
下图是一个
这里比较特殊的一个参数就是主维度leading_dimension
,这个参数的存在是为了更好的表示子矩阵。下图是官方文档中的示例。
我们推广这个示例,将一个rows * columns
的矩阵以行优先存储,并令其leading_dimension
为columns
。此时取它的一个m * n
的子矩阵,起始元素指针为sub
,想要得到其(i, j)
位置的元素,只需要利用如下计算公式:
1 |
|
列优先存储同理。
稀疏矩阵
坐标存储Coordinate (COO)
COO是一种利用非零元素及其坐标来存储稀疏矩阵的方式,主要有如下参数表示:
- 矩阵行数
rows
; - 矩阵列数
columns
; - 非零元素个数
nnz
; - 行索引数组指针
row_indices
:其长度为nnz
,存放了非零元素在等价稠密矩阵中的行索引; - 列索引数组指针
column_indices
:其长度为nnz
,存放了非零元素在等价稠密矩阵中的列索引; - 值数组指针
values
:其长度为nnz
,存放了矩阵的非零元素,按照等价稠密矩阵行优先的顺序排列。
COO的每一项由一个<row, column>
的二元组表示,COO默认是按照行的顺序排序的。
若想计算COO格式下的元素在等价稠密矩阵中的位置,可以通过如下公式:
1 |
|
压缩稀疏行Compressed Sparse Row (CSR)
CSR和COO非常类似,只是将行索引数组进行了压缩,用一个行偏移数组来代替了。
- 行偏移数组
row_offsets
:其长度为rows + 1
,存储了每一行起始元素在列索引数组和值索引数组中的位置; - 其余参数与COO一致。
若想计算CSR格式下的元素在等价稠密矩阵中的位置,可以通过如下公式:
1 |
|
其中,
row
表示稠密矩阵的第几行,k
的范围是k = 0; k < row_offsets[row + 1] - row_offsets[row]
。
此外还有CSC、SELL、BSR、BLOCKED-ELL等稀疏矩阵的存储方式,详细参考官方文档的介绍。
具体示例
我们来实现一个比较常见的操作SpMV
函数。接下来的内容重点在于cuSPARSE库一些通用操作。
首先需要创建一个句柄。
1 |
|
由于我们的数据大部分是在主机端准备的,所以生成的数据自然而然地是以稠密的形式存储的。所以需要进行稠密矩阵到稀疏矩阵的转换。这一部分的工作可能有一些复杂,总体步骤包含:
- 创建cuSPARSE的稠密和稀疏矩阵;
- 判断是否需要额外的buffer;
- 分析非零元素个数;
- 准备特定稀疏矩阵格式所需要的空间;
- 执行转换。
1 |
|
准备好矩阵后,接着准备参与运算的两个稠密向量。
1 |
|
最后就是执行计算,但在执行真正的计算之前,依然需要判断是否需要额外的buffer。
1 |
|
注意,无论是计算之前,还是前面提到的稠密转稀疏之前,它们判断是否需要额外buffer的操作,全部交给库来自行判断,不需要人为干预。程序员需要做的只是写一个像上面一样较为通用的代码,使其能够在需要buffer的时候申请得到即可。
示例完整代码参考cusparse.cu。关于其他API就不过多阐述了,用到的时候查官方文档即可。
cuBLAS库
cuBLAS库与cuSPARSE库最大的不同在于,cuBLAS库并不支持多种稀疏矩阵类型,它更擅长处理稠密矩阵和稠密向量的运算。
当前的cuBLAS库将接口分为了四类:
- cuBLAS API(CUDA 6.0开始);
- cuBLASXt API(CUDA 6.0开始);
- cuBLASLt API(CUDA 10.1开始);
- cuBLASDx API(未包含在CUDA Toolkit中)。
上面四套API的主要区别在于:
- cuBLAS API需要将数据搬移到设备上进行运算;
- cuBLASXt API可以将数据放在主机或参与运算的任何设备上,库会承担运算和数据分发的责任;
- cuBLASLt API是一套专注于通用矩阵乘(GEMM)的灵活的轻量级API,该API可以通过参数来灵活指定矩阵数据布局、输入类型、计算类型以及算法的实现。用户指定了一组预期的GEMM操作后,这组操作可以根据不同的输入来复用;
- cuBLASDx API则是一个设备端API扩展,可以在核函数中执行BLAS计算。通过融合数值运算,可以减少延迟并进一步提高性能。目前该组API还在preview阶段。
同时,cuBLAS
API存在新旧两套API,后面的所有讨论都基于定义在cublas_v2.h
头文件中的新版API,旧版API定义在cublas.h
中。具体的区别这里不过多讨论,有兴趣可以查看官方文档New
and Legacy cuBLAS API。
cuBLAS数据存储格式
cuBLAS有两套数据排布方式,一套为了兼容Fortran从1开始的索引,另一套是兼容C从0开始的索引。可以通过以下两个宏来计算全局索引。
1 |
|
要记住最核心的一点,cuBLAS是以列主序的形式存储矩阵的。
具体示例
在熟悉了cuSPARSE的使用之后,你会发现cuBLAS要简洁很多,因为少了很多配置稀疏矩阵的过程,下面以通用矩阵乘法为例说明。
同样地,首先创建句柄。
1 |
|
然后就可以直接进行计算了,如果需要的话,可以使用cublasSetStream()
来绑定一个流。
1 |
|
详细代码参考cublas.cu。
cuFFT库
cuFFT库由两部分组成:
- cuFFT:提供GPU上的高性能快速傅里叶变换操作,它需要提前将数据搬移到设备端;
- cuFFTW:为FFTW的用户提供快速移植到GPU的能力,为了这种快速移植能力,cuFFTW支持主机端的数据传入,它将自动为用户处理如
cudaMalloc
、cudaMemcpy
等操作。
下面的内容重点介绍cuFFT。cuFFT提供一种简单易用的配置机制称为plan
,它使用内部构建的block来优化给定配置和特定GPU硬件之间的转化。一旦创建了一个plan
,库将自动保存多次执行该plan
所需的所有状态,无需重新配置。不同类型的FFT需要不同的线程配置和GPU资源,plan
接口提供了这样一种简单的配置重用方式。
cuFFT支持多种类型的变换,如复数-复数变换(C2C)、复数-实数变换(C2R)、实数-复数变换(R2C)。由于变换的不同,需要的输入输出数据布局也就不同。
cuFFT数据存储格式
在cuFFT中,数据布局严格取决于配置和变换的类型。一般地,C2C变换情况下,输入输出数据应为cufftComplex
或cufftDoubleComplex
,具体取决于计算精度。而C2R变换,只需要非冗余的复数元素组成的向量作为输入,输出则是由cufftReal
或cufftDouble
元素组成的向量。对于R2C变换,需要一个实数向量作为输入,输出一个非冗余复数元素组成的向量。
在C2R和R2C变换中,输入输出的大小是不同的。对于非就地变换,创建一个大小合适的输出数组即可。但对于就地变换,程序员应当使用填充的数据布局,这种布局与FFTW兼容。无论是就地C2R还是R2C变换,输出的起始地址都与输入的起始地址一致,所有应当填充R2C中的输入或C2R中的输出数据。
以一维变换为例,期望的输入输出大小以及类型如下表所示。
FFT类型 | 输入数据大小(类型) | 输出数据大小(类型) |
---|---|---|
C2C | cufftComplex ) |
cufftComplex ) |
C2R | cufftComplex ) |
cufftReal ) |
R2C | cufftReal ) |
cufftComplex ) |
对于多维的情况,参考官方文档multidimensional-transforms。
具体示例
关于傅里叶变换算法本身这里不过多展开,下面用一个比较基本的一维复数-复数FFT来说明。
首先依旧是创建句柄,这里之所以将句柄命名为plan
是因为,后续创建cuFFT的plan
时,是基于这个句柄的。
1 |
|
创建plan
,这个plan
可以复用。如果需要的话,可以使用cufftSetStream()
来绑定一个流。
1 |
|
执行变换操作,这里进行了一次正向变换,归一化后又进行了逆向变换。
1 |
|
详细代码参考cufft.cu。
cuRAND库
介绍cuRAND库之前要引入两个与随机数生成相关的概念:
- PRNG:伪随机数生成器;
- QRNG:拟随机数生成器。
PRNG和QRNG的最大区别就在于,生成每一个随机数的事件是否为独立事件。PRNG每次采样均为独立统计事件,这意味着每次采样,所得到某个数的概率是相同的。而QRNG每次采样并不是独立事件,它会尽可能的均匀填充输出类型的范围。一个更具体的例子是,假设第一个生成的随机数是2的概率为
cuRAND库与其他库最大的不同就是,它提供了主机端和设备端两套API。
主机端API定义在头文件curand.h
中。但要注意的是,主机端API允许两种生成方式:主机生成和设备生成。若生成时传入的数据指针是主机内存指针,则生成过程由CPU在主机端完成,结果也存储在主机内存中。若生成时传入的数据指针是设备内存指针,则生成过程由设备端完成,结果存储在设备的全局内存中。
设备端API定义在头文件curand_kernel.h
中,可以在核函数中直接生成随机数并使用,而不需要将生成结果存入全局内存后再读取。
cuRAND库的RNG有9种,分别有5种PRNG和4种QRNG:
- PRNG:
- CURAND_RNG_PSEUDO_XORWOW:使用XORWOW算法实现的,XORWOW算法是伪随机数生成器xor-shift系列的成员;
- CURAND_RNG_PSEUDO_MRG32K3A:组合多重递归伪随机数生成器系列的成员;
- CURAND_RNG_PSEUDO_MTGP32:Mersenne Twister伪随机数生成器系列的成员,具有为GPU定制的参数;
- CURAND_RNG_PSEUDO_MT19937:Mersenne Twister伪随机数生成器系列的成员,参数与CPU版本相同,但顺序不同,仅支持主机API,并且只能在架构sm_35或更高版本上使用;
- CURAND_RNG_PSEUDO_PHILOX4_32_10:Philox系列的成员,三大基于非加密计数器的随机数生成器之一。
- QRNG:
- CURAND_RNG_QUASI_SOBOL32:32位序列的Sobol生成器;
- CURAND_RNG_QUASI_SCRAMBLED_SOBOL32:添加扰乱的32位序列的Sobol生成器;
- CURAND_RNG_QUASI_SOBOL64:64位序列的Sobol生成器;
- CURAND_RNG_QUASI_SCRAMBLED_SOBOL64:添加扰乱的64位序列的Sobol生成器。
cuRAND中的QRNG都是基于Sobol算法的,它以方向向量作为种子,上述的四种变体每种都能产生高达20000维的序列。
具体示例
主机端API
主机端API调用流程如下:
- 创建生成器并指定RNG类型;
- 设置偏移量(
offset
)、排序方式(ordering
)、种子(seed
); - 从指定分布中执行生成任务;
- 若在设备端生成,根据需要确定是否将生成结果拷贝回主机端;
具体示例如下,首先创建生成器。
1 |
|
设置offset
、ordering
、seed
。
1 |
|
从指定分布中执行生成任务。
1 |
|
设备端API
设备端API调用主要有以下几个步骤:
- 根据RNG算法创建状态;
- 初始化状态;
- 生成随机值;
每个支持的RNG算法都有对应的状态。
1 |
|
根据不同的RNG算法调用不同的curand_init
重载。
1 |
|
值得注意的是,由于线程的高度并发,所以应当避免在不同线程中使用相同的种子,也应当避免使用当前时间戳作为种子。
这里的
subsequence
会使得curand_init()
返回的序列是调用了(2^67 * subsequence + offset)
次curand()
的结果。
最后生成随机值。
1 |
|
详细代码参考curand.cu。
OpenACC
基本概念
OpenACC是一个基于编译器指令的API,它的工作方式和OpenMP非常类似,都是使用#pragma
开头的编译器指令作为指导。OpenACC的并行粒度分为gang
、worker
、vector
,这些概念可以和CUDA一一对应。
OpenACC | CUDA |
---|---|
gang |
block |
worker |
warp (未显式指出) |
vector |
thread |
一个gang
可以包含一个或多个执行线程,每个gang
内部都包含一个或多个worker
。每个worker
都有一个向量宽度,由一个或多个同时执行相同指令的向量元素构成,简单理解就是一个worker
可以包含一个或多个vector
。每个vector
都是一个单一的执行流,类似于CUDA线程。
OpenACC的目标是建立一个单线程的主机程序,该主机程序将核函数下放至多处理单元(PU),每个PU一次只运行一个gang
,但可以同时执行多个独立的worker
。在OpenACC中,gang
并行使用多个PU,每个gang
中的多线程并行即为worker
并行。每个worker
中的并行以及一个跨向量操作的并行称为vector
并行。这里的PU有点类似于NVIDIA
GPU中的SM。
并行模式
根据任务是否通过gang
、worker
、vector
并行执行,OpenACC将执行分为几种模式。
假设一个OpenACC程序的并行计算区域创建了gang
,每个gang
包含worker
,每个worker
的向量宽度为V
。此时总共有
gang
冗余模式
开始执行并行区域时,gang
以冗余模式执行,可以在并行执行前对gang
的状态进行初始化。在该模式下,每个gang
中只有一个活跃的worker
和一个活跃的vector
元素,其他worker
和vector
元素是闲置的,因此只有
1 |
|
gang
分裂模式
在OpenACC并行区域的某些地方,程序可能通过gang
转换为并行执行,这种情况下程序以gang
分裂模式执行。该模式下每个gang
中仍然只有一个活跃的worker
和一个活跃的vector
元素,因此同样只有vector
执行不同的并行区域,故计算任务被分散到各个gang
中。以向量加法为例,CUDA伪核函数表达如下。
1 |
|
每个
gang
只有一个活跃worker
时,程序处于单一worker
模式,当worker
中只有一个活跃vector
时,程序处于单一vector
模式。所以gang
冗余模式和gang
分裂模式也可以被称作单一worker
模式和单一vector
模式。
worker
分裂模式
在该模式下,并行区域的工作被划分到多个gang
的多个worker
中,可以提供
1 |
|
vector
分裂模式
该模式将工作在gang
、worker
、vector
通道上进行划分,提供
1 |
|
基本用法
前面提到了OpenACC的工作方式与OpenMP的极为相似,在源代码中加入#pragma acc
即可指导编译器对源代码进行翻译,使其能够在GPU上并行执行。
计算指令
核函数指令
#pragma acc kernels
会自动分析代码块中的可并行循环。
1 |
|
核函数指令可以有条件子句来修饰,当条件为false
时,代码块不会在设备上执行。
1 |
|
默认情况下,核函数指令结束时会有一个隐式同步,但可以通过添加async
子句来使执行不被阻塞。
async
子句接受一个可选的整型参数,若传入ID则可以使用指令来等待。
1 |
|
也可以将async
子句和wait
子句结合起来,实现链式异步工作。
1 |
|
而检查异步任务在没有阻塞的情况下是否完成只能通过运行时API来完成。
1 |
|
目前想要编译OpenACC的代码,推荐使用PGI编译器。PGI被英伟达收购之后,编译器就纳入了NVIDIA HPC SDK中了。需要安装HPC SDK后使用
pgcc
命令来编译代码。详细代码参考openacc_kernels.c。
并行指令
上面提到的核函数指令是一个强大的工具,编译器会自动分析代码并选择一个合适的并行策略,在这个过程中,程序员对程序的控制是较少的。但并行指令#pragma acc parallel
则可以提供更多的控制选项。
并行指令同样支持核函数指令的一些子句,如if
、async
、wait
。此外可以使用num_gangs(int)
来设置gang
数量,num_workers(int)
设置worker
数量,vector_length(int)
设置每个worker
的向量宽度。
1 |
|
并行指令还支持reduction
子句,格式为#pragma acc parallel reduction(op:var1, var2, ...)
,支持的op
有+
、*
、max
、min
、&
、|
、^
、&&
、||
。
1 |
|
并行指令还支持private
和firstprivate
子句。private
会为每个gang
创建一个private
型复制变量,只有该gang
可以使用该变量的拷贝,因此该值的改变对其他gang
或主机程序是不可见的。firstprivate
功能和private
相同,只是会将每个gang
中的private
型变量的值初始化为主机上该变量当前的值。
1 |
|
详细代码参考openacc_parallel.c。
循环指令
并行指令需要程序员为编译器明确标注并行性,并行区域总是以gang
冗余模式开始的,执行并行模式之间的转换需要对编译器有明确的指示,这种指示可以通过循环指令#pragma acc loop
来完成。
1 |
|
上面的代码并没有为循环指令添加子句,所以编译器可以自由使用它认为的最优循环调度。也可以通过gang
、worker
或vector
子句来显式控制每一级的并行性。
1 |
|
上述代码中,并行区域以gang
冗余模式开始,遇到带有gang
子句的循环指令后,转换为了gang
分裂模式。
考虑下列代码。
1 |
|
可以简写为下列形式。
1 |
|
详细代码参考openacc_parallel.c。
循环指令loop
不仅可以和并行指令paralle
结合,还可以与核函数指令kernels
结合,但某些循环指令的子句在并行指令和核函数指令下会有所不同。
子句 | 并行指令下的行为 | 核函数指令下的行为 |
---|---|---|
collapse(int) |
指明循环指令适用于多重嵌套循环 | 与并行指令下相同 |
gang(int) |
指明循环应通过gang 划分到并行区域,gang 数量由并行指令决定 |
说明循环应通过gang 进行划分,gang 有选择的使用整型参数 |
worker(int) |
指明循环应通过每个gang 中的worker 划分到并行区域,将每个gang 由单一worker 模式转换到worker 分裂模式 |
说明循环应通过每个gang 中的worker 划分到并行区域,worker 有选择的使用整型参数 |
vector(int) |
指明循环应通过vector 通道进行分配,是一个worker 由单一vector 模式转换到vector 分裂模式 |
说明循环应通过vector 通道进行分配,vector 有选择的使用整型参数 |
seq |
为了按序执行,使用seq 对循环进行标记 |
与并行指令下相同 |
auto |
指明编译器应为相关的循环选择gang 、worker 或vector 并行 |
与并行指令下相同 |
tile(int, ...) |
指明编译器应将嵌套循环中的每个循环拆分为两个循环:外层的tile 循环和内层的element 循环。内层循环次数为tile_size ,外层循环次数取决于串行代码。若附加到多个紧密的嵌套循环中,tile 可以使用多个tile_size ,并自动将所有外部循环放在内部循环之外 |
与并行指令下相同 |
device_type(type) |
type 是一个逗号分隔的列表,分隔不同设备类型的子句。所有子句都遵循device_type 的设定,只有当循环在指定设备类型上执行时,才可能有指令结束,或在下一个type 的设备上执行的情况。 |
与并行指令下相同 |
independent |
该子句声称被标记的循环为并行的且编译器分析高于一切 | 与并行指令下相同 |
数据指令
#pragma acc data
可以在主机和设备之间进行显式的数据传输。
1 |
|
上面的代码告知编译器,只有A
和B
应该被拷贝到设备,只有C
和D
应该被拷贝回主机。同时指明了数组的范围,某些情况下,编译器能够推断要复制的数组大小,可以将代码简化为下面的样子。
1 |
|
除了上述指定代码块的方法,还可以使用enter data
指令和exit data
指令来标记在任意节点传入和传出设备的数组。enter data
指明的数据会持续保留在设备端,直到遇到将其传回的exit data
指令。这两个指令可以与async
和wait
子句结合发挥最大作用。
注意,单纯的
data
指令不支持async
和wait
子句。
1 |
|
考虑如下代码。
1 |
|
可以简写为下列形式。
1 |
|
详细代码参考openacc_data.c。
数据指令支持的子句见下表。
子句 | 行为 | data 支持 |
enter data 支持 |
exit data 支持 |
---|---|---|---|---|
if(cond) |
若cond 为true 则执行数据搬移 |
Y | Y | Y |
copy(var1, ...) |
在进入数据区域时将变量拷贝至设备端,离开数据区域时拷贝回主机端 | Y | N | N |
copyin(var1, ...) |
指明变量只能被拷贝至设备端 | Y | Y | N |
copyout(var1, ...) |
指明变量只能被拷贝回主机端 | Y | N | Y |
create(var1, ...) |
指明列出的变量需要在设备端分配内存,但变量值不必传入或传出设备 | Y | Y | N |
present(var1, ...) |
指明列出的变量已经在设备端了,不必再次传入。运行时,编译器会发现并使用这些已经存在于设备端的数据 | Y | N | N |
present_or_copy(var1,...) |
若列出的变量已经在设备端了,则功能与present 一致;若不在设备端,则功能与copy 一致 |
Y | N | N |
present_or_copyin(var1, ...) |
若列出的变量已经在设备端了,则功能与present 一致;若不在设备端,则功能与copyin 一致 |
Y | Y | N |
present_or_copyout(var1, ...) |
若列出的变量已经在设备端了,则功能与present 一致;若不在设备端,则功能与copyout 一致 |
Y | N | N |
present_or_create(var1, ...) |
若列出的变量已经在设备端了,则功能与present 一致;若不在设备端,则功能与create 一致 |
Y | Y | N |
deviceptr(var1, ...) |
指明列出的变量是设备内存指针,不必再为该指针指向的数据分配空间,也不必在主机和设备之间传输。 | Y | N | N |
delete(var1, ...) |
可以与exit data 结合使用,显式释放设备内存 |
N | N | Y |
更多指令和运行时API可以参考官方文档Specification | OpenACC。
与CUDA结合
要结合CUDA与OpenACC,需要通过deviceptr
子句来实现CUDA和OpenACC之间的数据共享。核心代码如下。
1 |
|
详细代码参考cuda_openacc.cu。
CUDA编程——GPU加速库和OpenACC
1.CUDA编程——调整指令集原语
2.CUDA编程——流和并发
3.CUDA编程——性能分析工具
4.CUDA编程——共享内存和常量内存
5.CUDA编程——全局内存
6.CUDA编程——执行模型
7.CUDA编程——NVCC编译器
8.CUDA编程模型概述