OpenMP和OpenACC

OpenMP

OpenMP是CPU的并行编程模型,它使用编译器指令来识别并行区域。

omp_set_num_threads(n_streams);//用来指定要用到的CPU线程数,类似于设置环境变量
#pragma omp parrallel //标记代码为并行部分
{
    int i= omp_get_thread_num();//为每个主机线程返回唯一的线程ID,将该ID作为streams数组的索引,实现OpenMP线程与CUDA流的一一对应
    kernel_1<<<gria,block,0,streams[i]>>>(); 		 
    kernel_2<<<gria,block,0,streams[i]>>>();
    kernel_3<<<gria,block,0,streams[i]>>>();
    kernel_4<<<gria,block,0,streams[i]>>>();
}
//下面是上述代码更简单可靠的OpenMP指令形式
#pragma omp parrallel num_threads(n)//指定OpenMP并行区域里要用到的CPU线程数
{
    int i= omp_get_thread_num();//为每个主机线程返回唯一的线程ID
}

在OpenMP并行区域之前或之后进行分配/释放资源能够在该区域达到最佳的CPU利用率。

编译命令

$ nvcc -O3 -Xcomplier -fopenmp  src.cu -o dst -lgomp

OpenACC

OpenACC是OpenMP模型在GPU上的扩展,两者极其相似。

OpenAcc是CUDA的一个补充编程模型,使用基于编译器指令的API,具有高性能、可编程性和跨平台可移植性。OpenACC由Nvidia主攻编译器的子公司PGI提出并开源,它们的编译器为pgcc。

OpenACC 规范要求支持它的编译器预定义一个宏_OPENACC ,宏的值为 yyyymm ,其中 yyyy 是编译器所支持 OpenACC 版本的发布年份, mm 是月份。

编程模型

OpenACC CUDA
gangs Blocks
workers Warps
vectors Threads

OpenACC 的线程模型与 CUDA 的线程模型类似,但添加了一个并行的维度。 OpenACC 可以分为 gang 、worker 和 vector 三个并行层次。在上层, gang 类似于 CUDA 线程块。一个 gang 可包含一个或多个执行的线程,在每个 gang 内部都包含一个或多个 worker 。在 CUDA 中,一个 worker类似于线程中的一个线程束。每个 worker 都有一个向量宽度,由一个或多个同时执行相同指令的向量元素组成。毎个向量元素都类似于一个CUDA 线程,因为它是一个单一的执行流。 OpenACC 和 CUDA 线程模型之间的主要区别在于, OpenACC 在编程模型中直接指出了 worker 的概念(即线程束),而在 CUDA 中并没有明确建立线程束。
OpenACC 平台模型与 CUDA 类似,但它使用不同的术语和略有不同的抽象概念。 OpenACC 的目标是建立一个具有单线程的主机程序平台,在该主机程序中,将内核交付给多处理单元( PU ),在此平台上,每个 PU 一次只运行一个gang 。每个PU可以同时执行多个独立的并发执行线程(worker)。每个执行线程可以执行具有一定向量宽度的向量运算。在 OpenACC 中, gang 并行使用多个PU。每个 gang 里的多线程并行即为 worker 并行。每个 worker 里的并行以及一个跨向量操作的并行被称为向量并行。当在 GPU 上使用 OpenACC 时,一个PU就类似于一个 SM。

编程方法

OpenACC将串行程序并行化的手段是添加一些C/C++预处理语句,预处理语句分为directive(导语)和clause(子语)两类。

  • 导语(directive) 表示主要功能,每句有且只能有一个(除了parallel loopkernels loop),作用是给编译器一些指导,指出哪
    些代码需要并行化、需要怎么并行化,编译器根据程序员的指导信息生成最佳的并行代码。
  • 子语(clause) 表示对 导语(directive) 的修饰,更详细地表明导语的意图,每句可以有零个或多个。

一个directive(导语)和若干个clause(子语)构成一个功能模块construct(构件)。

#pragma acc direcive [clause ...]

当使用 OpenACC 时,由程序员用编译器指令指定并行代码区域,或是并行运行。编译器指令还可以指定使用何种类型的并行处理。编译器指令是一行源代码,用 C/C++编写,开头为#pragma 。 OpenACC 指令使用 acc关键字作为唯一标识,这意味着所有 OpenACC 指令都是以#pragma acc 开头的。

#pragma acc kernels loop gang(64),vector(128)//指定该循环使用64个线程块,且每个线程块有128个线程

如果不指定任何东西,编译器会选择他认为最佳的值。

默认情况下OpenACC模型使用同步内核调用。也就是说,主机处理器会等待GPU完成,然后在GPU内核调用返回后才继续执行,这类似于在C中进行函数调用,而不是生成一个从线程,随后进行汇合。

为此OpenACC为内核和数据提供了async子句以允许它们在主机端异步地运行并使传输与主机端操作异步执行

#pragma acc kernels loop async
for(i=0,i<num;i++){
    ...
}

异步传输需要锁页内存,也就是说不会交换到感盘上的内存。在 OpenACC 中,不需要像在 cUDA 中那样特别关心这些。指定 async 子句将会导致 OpenACC 编译器在传输的底层使用锁页内存。当然,使用异步操作时要记住的一点是,在异步操作完成之前,不能交换传输到内核或内核操作的数据。

计算构件

OpenACC 有两个计算构件( Compute Construct ): parallel 和 kernels ,用来将循环并。两个构件的目标是一样的,但行为有较大的区别。初学者应优先使用简单的 kernels 构件,熟练以后可以使用 parallel 构件,众多的子语还能够精细控制并行化方案。

编译器将把 kernels 构件区域编译成一系列在加速器设备上执行的 CUDA kernel ,这也是 kernels 构件名字是复数形式的原因。

编译器将 kernels 区域内的代码分割为一系列的加速器内核( kernel )。通常,每个非嵌套循环成为一个单独的内核。当程序遇到一个 kernels 构件时,它在设备上按顺序启动这一系列内核。对不同的内核, gang 的数量、每个 gang 包含多少个worker 、vector 的长度以及三者的组织方式都可能不同。

如果没有使用 async子语, kernels 区域结東时将有一个隐式障碍,本地线程不再向前执行,直至所有的内核都执行完毕。

parallel构件
#pragma acc parallel [子语列表]
{
    //结构块
}

parallel的子语:

  • async[(id)] :接收一个可选的整型参数,给内核块指定唯一ID
  • wait [(id列表)]:等待ID内核块执行完成
  • num_gangs (整数表达式)
  • num_workers (整数表达式)
  • vector_length(整数表达式)
  • device_type(设备类型列表)
  • if (条件)
  • reduction (操作符:变量列表)
  • copy (变量列表)
  • copyin (变量列表)
  • copyout (变量列表)
  • create (变量列表)
  • present (变量列表)
  • deviceptr (变量列表)
  • private(变量列表)
  • firstprivate (变量列表)
  • default(none|present)
kernels构件
#pragma acc kernels [子语列表]
{
    //结构块
}

kernels的子语:

  • async(id)
  • wait[(整数表达式列表)]
  • num_gangs (整数表达式)
  • num_workers (整数表达式)
  • vector_length (整数表达式)
  • device_type(设备类型列表)
  • if(条件)
  • copy(变量列表)
  • copyin(变量列表)
  • copyout (变量列表)
  • create (变量列表)
  • present (变量列表)
  • deviceptr (变量列表)
  • default(none|present)
data构件
#pragma acc data [子语列表]
{
    //结构块
}

data的子语:

  • if (条件)
  • copy (变量列表)
  • copyin (变量列表)
  • copyout (变量列表)
  • create (变量列表)
  • present (变量列表)
  • deviceptr (变量列表)
enter data导语
#pragma acc enter data [子语列表]

enter data的子语:

  • if (条件)

  • async(id)

  • wait [(整数表达式列表)]

  • copyin (变量列表)

  • create (变量列表)

exit data导语
#pragma acc exit data [子语列表]

exit data的子语:

  • if (条件)

  • async[(整数表达式)]

  • wait [(整数表达式列表)]

  • copyout (变量列表)

  • delete (变量列表)

  • finalize

host_data导语
#pragma acc host_data [子语列表]
 结构化块

host_data的子语:

  • use_device(变量列表)
loop导语
#pragma acc loop [子语表表]
	for循环 

loop 的子语:

  • collapse(n)
  • gang[( gang 参数列表)]
  • worker[([num:] 整数表达式)
  • vector[([ length :]整数表达式)]
  • seq
  • auto
  • tile(尺寸表达式列表)
  • device_type (设备类型列表)
  • independent
  • private (列表)
  • reduction (操作符:列表)
组合导语
#pragma acc parallel loop [子语列表]
for循环
    
#pragma acc kernels loop [子语列表]
for循环
declare导语
#pragma acc declare [子语表表]

declare的子语:

  • copy (变量列表)
  • copyin (变量列表)
  • copyout (变量列表)
  • create (变量列表)
  • present (变量列表)
  • deviceptr (变量列表)
  • device_resident(变量列表)
  • link(变量列表)
init导语
#pragma acc init [子语表表]

init的子语:

  • device_type(设备类型列表)
  • device_num(整数表达式)
shutdown导语
#pragma acc shutdown [子语表表]

shutdown的子语:

  • device_type(设备类型列表)
  • device_num(整数表达式)
set 导语
#pragma acc set[子语列表]

set 的子语:

  • default_async(整数表达式)
  • device_num(整数表达式)
  • device_type (设备类型列表)
update 导语
#pragma acc update [子语列表]

update 的子语:

  • async(id)

  • wait [(整数表达式列表)]

  • device_type (设备类型列表)

  • if (条件)

  • if_present

  • self (变量列表)

  • host (变量列表)

  • device (变量列表)

routine 导语
#pragma acc routine [子语列表]

#pragma acc routine(名字) [子语列表]

routine的子语:

  • gang

  • worker

  • vector

  • seq

  • bind(名字)

  • bind(字符串)

  • device_type(设备类型列表)

  • nohost

wait 导语
#pragma acc wait[(整数表达式列表)] [子语列表]

wait 的子语:

  • async[(整数表达式)]

子语含义

independent子语

independent 子语告诉编译器该循环的迭代步是彼此数据独立的。因此允许生成并行执行这些迭代步的代码,且不需要同步。在一个 parallel 构件内,所有不带 seg 子语的 loop 导语都暗含一个 independent 子语。

下而的子语常用于探度调优,在本书中应用不是特别多,只列出含义,不再举例,(1) privae 子语
loop 构件上的 private 子语指明,要为变量列表中的每一项创建一个刷本。如循环体以向量分製模式执行,那么与每一个向量通道相关联的每一个线程都会创建…个本。如果循环体以 worker 分裂 vector 单独的模式执行,那么将只创建一个变量副本,井在们一个 worker 内部的所有向量通道的关联线程间其享。其他情形中,创建一个副本,并在你一个 gang 中的所有 worker 中的所有向量通道的关联线程之间共享。

colapse 子语

collapae子语用来指定与1oop构件相关联的紧密嵌套循环有多少层。 collapse 子语的参数必须是一个常量正整数表达式。如果没有 collapse 子语,只有紧接着的循环才与 loop 构件相关联,
如果有一个以上的循环与 loop 构件关联,那么关联循环中的所有循环体都会按照剩余的子语来调度。与 collapse 子语相关联的所有循环的迭代步数必须是可计算的,并在所有循环中保持不变。
编译器自行决定是否将导语的 gang 、 worker 或 vector 子语应用到每一个循环、线性化迭代空间。

gang 子语

当上一层计算构件是一个 kernela 构件时, gang 子语指明,关联循环的迭代步需要在为内核创建的所有 gang 上并行执行。如果指定一个不带关键字的参数,或者指定一个放在 num 关键字后而的参数,那么该參数指明使用多少个 gang 来执行本循环的迭代步。除非处在一个嵌套的 parallel 区域内或嵌套的 kernels 区域内,否则不允许一个带 gang 子语的1oop区城包含另外一个带 gang 子语的loop区域。
除非出现 static 参数,否则循环的迭代步对各个 gang 的调度方式不确定。如果出现一个带星号的 atatic 参数,那么编译器将自行决定块的尺寸。

数据指令

在OpenACC中,#pragma acc data被显式地用于主机 与加速器之间的数据传输,类似于CUDA中的cudaMemcpy。与 kernels 和 parallel 指令类似,数据被应用到代码的某个区域。它定义了在该区域边界处必须进行的数据传输工作。例如,可以把一个变量标记为copyin ,也就是说,可以将这个变量在该区域的起始位置传送给加速器,但最后不能传出。相反地, copyout 是在数据区的末端将该变量传回主机端,但不能在该数据区域的起始位置将其传给加速器。

#pragma acc data copyin(A[0:N],B[0:N]) copyout(C[0:N],D[0:N) 
{
	#pragma acc parallel 
    {
         #pragma acc loop
         for(i=0;i<M;i++){
         	C[i]= A[i]+B[i];
         }
         #pragma acc loop 
         for(i=0;i<M;i++){
             D[i]=C[i]*A[i] ;
         }   
    }
}

所添加的#pragma acc data指令通知编译器:只有A和 B 应该拷贝到设备端,只有 C 和 D 应该拷贝回主机端。该段代码还指明了传输数组的范围,在这种情况下,传输的应该是整个数组。在某些情况下,编译器能够推断出要复制的数组大小,这能略微简化代码:

#pragma acc data copyin(A,B) copyout(C,D)

除了数据指令,在执行过程中也可以用#pragma acc enter data#pragma acc exit data 来标记任意节点传人和传出加速器的数组。当编译器遇到 enter data指令时,它会指明哪些数据应该复制到设备端。这些数据将继续留在设备端,直到编译器遇到将其传回的exit data 指令或者程序终止执行。当与async 子句和 wait子句相结合时, enter data指令和exit data指令能够发挥最大的作用。注意。 data 指令不支持 async子句和wait子句。
当把 async子句应用到 enter dataexit data 指令中时,它会创建将数据传人或传出加速器的异步传输任务,类似于 cudaMemcpyAsync 。正如在 CUDA 中异步拷贝是很有用的一样,作为一种重叠计算和通信的方法,在 OpenACC 中它也是很有用的。当把 wait 子句应用到 enter dataexit data 指令中时,它的作用与在 kernels 指令或 parallel 指令中一样:通信指令要等待其他异步任务结束后再执行。需要注意的是通信指令(即 enter data 和 e data 指令)可以便用 asyne 和 wait 子句来交互异步计算任务(即 kernels 指令和 parallel 作今),反之亦然。参考下面的代码段:

#pragma acc data <directives>

其中可以是以下之一:

  • copy(data1 data2 …)
  • copyin(data1 data2 …)
  • copyout(data1 data2 …)
  • create(data1 data2 …)
  • present(data1 data2 …)

多GPU

OpenACC 标准仅支持“毎个 GPU 一个 CPU 线程”形式的单个节点上的多GPU 。这可以充分利用多核CPU的潜力。

只需简单地使用OpenMP指令启动一些线程就行了

acc_set_device_num API调用是每个主机线程执行一次的事件

编译方法

$ pgcc -acc -Minfo=accel src.c -o app

为PGI编译器添加-acc标志使其支持OpenACC,允许其在提供的代码中识别带有#pragma acc的指令。添加

你可能感兴趣的:(并行编程:CUDA,OpenMP,OpenACC,c++,算法)