CUDA 编程概念简介,NVIDIA GPU显卡

1.Cuda线程模型

                                   来自

 

下面的机构图说明了GPU的不同层次的结构。

CUDA 编程概念简介,NVIDIA GPU显卡_第1张图片

 

CUDA的线程模型从小往大来说就是:

  1. Thread:线程,并行的基本单位
  2. Thread Block:线程块,互相合作的线程组,线程块有如下几个特点:
    • 允许彼此同步
    • 可以通过共享内存快速交换数据
    • 以1维、2维或3维组织
  3. Grid:一组线程块
    • 以1维、2维或3唯组织
    • 共享全局内存
  4. Kernel:在GPU上执行的核心程序,这个kernel函数是运行在某个Grid上的。
    • One kernel <-> One Grid   (不可以one kernel <-> multi grid ??)

每一个block和每个thread都有自己的ID,我们通过相应的索引找到相应的线程和线程块。

  • threadIdx,blockIdx
  • Block ID: 1D or 2D
  • Thread ID: 1D, 2D or 3D

 

        CUDA 编程概念简介,NVIDIA GPU显卡_第2张图片

         kernel在device上执行时实际上是启动很多线程,一个kernel所启动的所有线程称为一个网格(grid),同一个网格上的线程共享相同的全局内存空间,grid是线程结构的第一层次,而网格又可以分为很多线程块(block),一个线程块里面包含很多线程,这是第二个层次。线程两层组织结构如上图所示,这是一个gird和block均为2-dim的线程组织。grid和block都是定义为dim3类型的变量,dim3可以看成是包含三个无符号整数(x,y,z)成员的结构体变量,在定义时,缺省值初始化为1。因此grid和block可以灵活地定义为1-dim,2-dim以及3-dim结构,kernel调用时也必须通过执行配置<<>>来指定kernel所使用的网格维度和线程块维度。

      所以,一个线程需要两个内置的坐标变量(blockIdx,threadIdx)来唯一标识,它们都是dim3类型变量,其中blockIdx指明线程所在grid中的位置,而threaIdx指明线程所在block中的位置。

举个例子,我们以上图为例,分析怎么通过<<>>>这种标记方式索引到我们想要的那个线程。CUDA的这种<<>>其实就是一个多级索引的方法,第一级索引是(blockIdx.x, blockIdx.y),有时还有blockIdx.z,对应上图例子就是(1, 1),通过它我们就能找到了这个线程块的位置,然后我们启动二级索引(threadIdx.x, threadIdx.y, threadIdx.z来定位到指定的线程。这就是我们CUDA的线程组织结构。

来自

2.CUDA编程模型     

                                                         来自

 

CUDA术语

CUDA 编程概念简介,NVIDIA GPU显卡_第3张图片

 

在CUDA中,hostdevice是两个重要的概念,我们用host指代CPU及其内存,而用device指代GPU及其内存。CUDA程序中既包含host程序,又包含device程序,它们分别在CPU和GPU上运行。同时,host与device之间可以进行通信,这样它们之间可以进行数据拷贝。典型的CUDA程序的执行流程如下:

  1. 分配host内存,并进行数据初始化;
  2. 分配device内存,并从host将数据拷贝到device上;
  3. 调用CUDA的核函数在device上完成指定的运算;
  4. 将device上的运算结果拷贝到host上;
  5. 释放device和host上分配的内存。

      上面流程中最重要的一个过程是调用CUDA的核函数来执行并行计算,kernel是CUDA中一个重要的概念,kernel是在device上线程中并行执行的函数,核函数用__global__符号声明,在调用时需要用<<>>来指定kernel要执行的线程数量,在CUDA中,每一个线程都要执行核函数,并且每个线程会分配一个唯一的线程号thread ID,这个ID值可以通过核函数的内置变量threadIdx来获得。

 

来自

编程要点1:如何指定某函数运行的设备

       

由于GPU实际上是异构模型,所以需要区分host和device上的代码,在CUDA中是通过函数类型限定词开区别host和device上的函数,通过函数前方的关键字就可以表示某个程序在CPU上跑还是在GPU上跑!如下表所示,比如我们用__global__定义一个kernel函数,就是CPU上调用,GPU上执行,注意__global__函数的返回值必须设置为void。主要的三个函数类型限定词如下:

CUDA 编程概念简介,NVIDIA GPU显卡_第4张图片

 

编程要点2:CPU和GPU间的数据传输怎么写?

首先介绍在GPU内存分配回收内存的函数接口:

  • cudaMalloc(): 在设备端分配global memory
  • cudaFree(): 释放存储空间

CPU的数据和GPU端数据做数据传输的函数接口是一样的,他们通过传递的函数实参(枚举类型)来表示传输方向:

cudaMemcpy(void dst, void src, size_t nbytes,

enum cudaMemcpyKind direction)

enum cudaMemcpyKind:

  • cudaMemcpyHostToDevice(CPU到GPU)
  • cudaMemcpyDeviceToHost(GPU到CPU)
  • cudaMemcpyDeviceToDevice(GPU到GPU)

编程要点3怎么用代码构建线程的组织模型?

   我们可以用dim3类来表示网格和线程块的组织方式,网格grid可以表示为一维和二维格式,线程块block可以表示为一维、二维和三维的数据格式。

dim3 DimGrid(100, 50); //5000个线程块,维度是100*50

dim3 DimBlock(4, 8, 8); //每个线层块内包含256个线程,线程块内的维度是4*8*8

         kernel调用时也必须通过执行配置<<>>来指定kernel所使用的网格维度和线程块维度。

比如:

dim3 threadPerBlock(16, 16);

dim3 blockNumber((Col+threadPerBlock.x-1)/ threadPerBlock.x, (Row+threadPerBlock.y-1)/ threadPerBlock.y );

matrix_mul_gpu << > > (d_dataA, d_dataB, d_dataC, Col);

 

__global__ void matrix_mul_gpu(int *M, int* N, int* P, int width)

{         int i = threadIdx.x + blockDim.x * blockIdx.x;

int j = threadIdx.y + blockDim.y * blockIdx.y;

 int sum = 0;

for( int k=0; k

{ int a = M[j*width+k];

 int b = N[k*width+i];

 sum += a*b;

}

P[j*width+i] = sum;

}

 

来自

 

    Threads的唯一标识ThreadIdx的表达方式随着grid,block的划分方式(或者说是维度)而不同

 

// 情况6:grid划分成2维,block划分为3维。  

__device__ int getGlobalIdx_2D_3D() {  

    int blockId = blockIdx.x + blockIdx.y * gridDim.x;  

    int threadId = blockId * (blockDim.x * blockDim.y * blockDim.z)  

        + (threadIdx.z * (blockDim.x * blockDim.y))  

        + (threadIdx.y * blockDim.x) + threadIdx.x;  

    return threadId;  

}  

         实质是通过线程的索引对GPU的内存进行调用和处理,不同的线程并行处理不同的内存中的数据,由每个线程的索引来确定这个线程需要处理的数据的地址。

来自

 

3.CUDA内存模型

来自

CUDA中的内存模型分为以下几个层次:

  • 每个线程都用自己的registers(寄存器)
  • 每个线程都有自己的local memory(局部内存)
  • 每个线程块内都有自己的shared memory(共享内存),所有线程块内的所有线程共享这段内存资源
  • 每个grid都有自己的global memory(全局内存),不同线程块的线程都可使用
  • 每个grid都有自己的constant memory(常量内存)和texture memory(纹理内存),),不同线程块的线程都可使用

线程访问这几类存储器的速度是register > local memory >shared memory > global memory

下面这幅图表示就是这些内存在计算机架构中的所在层次。

CUDA 编程概念简介,NVIDIA GPU显卡_第5张图片

 

来自

这里想谈谈SP和SM(流处理器),很多人会被这两个专业名词搞得晕头转向。

  • SP:最基本的处理单元,streaming processor,也称为CUDA core。最后具体的指令和任务都是在SP上处理的。GPU进行并行计算,也就是很多个SP同时做处理。
  • SM:多个SP加上其他的一些资源组成一个streaming multiprocessor。也叫GPU大核,其他资源如:warp scheduler,register,shared memory等。SM可以看做GPU的心脏(对比CPU核心),register和shared memory是SM的稀缺资源。CUDA将这些资源分配给所有驻留在SM中的threads。因此,这些有限的资源就使每个SM中active warps有非常严格的限制,也就限制了并行能力。 

        需要指出,每个SM包含的SP数量依据GPU架构而不同,Fermi架构GF100是32个,GF10X是48个,Kepler架构都是192个,Maxwell都是128个。

        简而言之,SP是线程执行的硬件单位,SM中包含多个SP,一个GPU可以有多个SM(比如16个),最终一个GPU可能包含有上千个SP。这么多核心“同时运行”,速度可想而知,这个引号只是想表明实际上,软件逻辑上是所有SP是并行的,但是物理上并不是所有SP都能同时执行计算(比如我们只有8个SM却有1024个线程块需要调度处理),因为有些会处于挂起,就绪等其他状态,这有关GPU的线程调度。

       当线程块被划分到某个SM上时,它将进一步划分为多个线程束,因为这才是SM的基本执行单元,但是一个SM同时并发的线程束数是有限的。这是因为资源限制,SM要为每个线程块分配共享内存,而也要为每个线程束中的线程分配独立的寄存器。所以SM的配置会影响其所支持的线程块和线程束并发数量。总之,就是网格和线程块只是逻辑划分,一个kernel的所有线程其实在物理层是不一定同时并发的。所以kernel的grid和block的配置不同,性能会出现差异,这点是要特别注意的。还有,由于SM的基本执行单元是包含32个线程的线程束,如果线程数小于32,则也会同时占用一个warp内的所有线程,某些线程会处于空闲状态,所以block大小一般要设置为32的倍数

      SM采用的是SIMT (Single-Instruction, Multiple-Thread,单指令多线程)架构,基本的执行单元是线程束(wraps),线程束包含32个线程,这些线程同时执行相同的指令,但是每个线程都包含自己的指令地址计数器和寄存器状态,也有自己独立的执行路径。所以尽管线程束中的线程同时从同一程序地址执行,但是可能具有不同的行为,比如遇到了分支结构,一些线程可能进入这个分支,但是另外一些有可能不执行,它们只能死等,因为GPU规定线程束中所有线程在同一周期执行相同的指令,线程束分化会导致性能下降。

来自  

来自

 

下面这个图将从硬件角度和软件角度解释CUDA的线程模型,指出了硬件概念和软件概念的对应关系。

CUDA 编程概念简介,NVIDIA GPU显卡_第6张图片

 

  • 每个线程由每个线程处理器(SP)执行
  • 线程块由多核处理器(SM)执行
  • 一个kernel其实由一个grid来执行,一个kernel一次只能在一个GPU上执行

    

     block是软件概念,一个block只会由一个sm调度,程序员在开发时,通过设定block的属性,告诉GPU硬件,我有多少个线程,线程怎么组织。而具体怎么调度由sm的warps scheduler负责,block一旦被分配好SM,该block就会一直驻留在该SM中,直到执行结束。一个SM可以同时拥有多个blocks,但需要序列执行。下图显示了GPU内部的硬件架构:

CUDA 编程概念简介,NVIDIA GPU显卡_第7张图片

 

来自

你可能感兴趣的:(CUDA,编程指南,中文版)