CUDA编程模型

1. 典型的CUDA编程包括五个步骤:

  1. 分配GPU内存

  2. CPU内存中拷贝数据到GPU内存中

  3. 调用CUDA内核函数来完成指定的任务

  4. 将数据从GPU内存中拷贝回CPU内存中

  5. 释放GPU内存

*2.

数据局部性:(是指数据重用,以降低对于内存访问的延迟)

  1. 时间局部性:指在较短的时间内实现对数据或资源的重用

  2. 空间局部性:指在相对较接近的存储空间内数据元素的重用

CPU中通过缓存来增强时间局部性和空间局部性的优化

(不是很懂)3.CUDA中有内存层次和线程层次的概念

  1. 内存层次结构

  2. 线程层次结构

CUDA编程模型中存在共享内存,通过为主内存节省带宽来大幅度提高运行速度。

4.CUDA编程中,你只需要编写单个线程需要执行的操作命令,通过调用核函数,GPU通过处理这样的核函数,来启动成千上万的线程来运行这个操作。

5.CUDA开发环境

  • NVIDIA Nsight 集成开发环境

  • CUDA-GDB命令行调试器

  • 用于性能分析的可视化和命令行分析器

  • CUDA-MEMCHECK内存分析器

  • GPU设备管理工具

6.CUDA编程模型利用GPU架构的计算能力提供了以下几个特有功能:

  • 一种通过层次结构在GPU中组织线程的方法

  • 一种通过层次结构在GPU中访问内存的方法

7.在使用CUDA编程模型的时候,需要注意到

  • 主机:CPU及其内存

  • 设备:GPU及其内存(GPU显存)

8.CUDA6.0开始,NVIDIA推出统一寻址方式,可以通过指针访问主机内存和设备内存

9.内核是CUDA编程模型的重要组成部分,其代码在GPU设备上运行。

10.多数情况下,主机可以独立地对设备进行操作,内核一旦启动,管理权立刻返回给主机,一个典型的CUDA程序包括由并行代码互补的串行代码。

11.CUDA编程模型假设系统是由一个主机和一个设备组成的,而且各自拥有独立的内存,核函数是在设备上运行的,CUDA运行时负责分配和释放设备上的内存,并且在主机内存和设备内存之间进行传输。

12.用于分配设备内存的CUDA函数是

cudaError_t cudaMalloc(void ** devPtr,size_t size);

当分配好内存后,该函数会返回devPtr指针用来指向分配的内存,size是分配内存的大小。

cudaError_t cudaFree(void * devPtr)

用于释放GPU上的内存。

13.用于拷贝主机和设备之间数据的函数是

cudaError_t cudaMemcpy(void * dst,const void * src,size_t count,cudaMemcpyKind kind)

此函数从src指向的存储空间复制一定字节数的数据到由dst指定的数据存储区。count表示复制的字节数量,kind表示复制的方向,:

  • cudaMemcpyHostToDevice(主机到设备)

  • cudaMemcpyDeviceToHost(设备到主机)

  • cudaMemcpyDeviceToDevice(设备到设备)

  • cudaMemcpyHostToHost(主机到主机)

注意这个函数是以同步方式执行的,在复制还没完成的时候此时主机的程序是被阻塞的,当这个函数执行完成以后主机程序和核函数才会继续运行,除了内核启动之外的CUDA函数都会返回一个错误的枚举类型:cudaError_t,内存分配成功则返回:

cudaSuccess

否则返回:

cudaErrorMemoryAllocation

可以使用以下函数将错误代码转换为错误的可读信息:

char * cudaGetErrorString(cudaError_t error);

14.nvcc 封装了几种内部编译器,CUDA编译器允许通过命令行选项启动不同的工具完成编译操作。当存在一个完全在主机上运行的C语言文件时,也可以通过nvcc来进行编译:

nvcc -Xcompiler -std=c99 hello.c -o hello

-Xcompiler用于制定命令行选项是指向C编译器还是预处理器,-std=c99表示使用什么样的标准来对代码进行处理。

(线程管理)15.当核函数在主机端启动时,它的执行会被移动到设备端,此时设备中会产生大量的线程,并且每个线程都会完成由核函数指定的操作。CUDA明确了线程抽象的概念,

这是一个两层的线程层次结构: 由线程块和线程块网格构成,线程网格内部含有线程块,线程块内部含有线程。

16.由一个内核启动所产生的所产生的线程为同一个网格,同一个线程块中的所有线程共享同一个全局内存空间,一个网格内部包含多个线程块,一个线程块中包含着一组线程,线程块中的线程可以通过两种方式相互协作:

  • 同步

  • 共享内存

不同线程块的线程不能进行相互协作。

线程依靠两个坐标变量来进行区分索引

  • blockIdx(线程块在线程网格内的索引位置)

  • threadIdx(线程在线程块内的索引位置)

这些变量是一个核函数需要初始化的内置变量,当执行一个核函数时,CUDA运行时为每个线程分配坐标变量(blockIdx,threadIdx),你可以将部分数据分配给不同的线程。

blockIdx,threadIdx两个变量是基于uint3定义的CUDA内置向量类型,可以通过x,y,z三个字段来指定:

  • blockIdx.x

  • blockIdx.y

  • blockIdx.z

  • threadIdx.x

  • threadIdx.y

  • threadIdx.z

17.CUDA可以组织三维的线程网格和线程块,网格和块的维度由两个dim3变量决定

  • blockDim(线程块的维度,表示一个线程块中的线程的个数)

  • gridDim(线程格的维度,表示一个线程格中的线程个数)

当定义一个dim3变量时,其中的未定义的变量默认为1dim3中的每个组件都可以通过xyz获得

  • blockDim.x

  • blockDim.y

  • blockDim.z

通常情况下,一个线程格会被组织成线程块的二维数组的形式,而线程块会被组织成为线程的三维数组形式

18. 在主机端,可以通过定义dim3类型的数据自定义一个线程格和线程块的维度,当执行核函数时,会自动生成相应的内置初始化网格和块。

19.定义线程块维度和线程网格维度:

  • 首先定义一个较小的数据

int nElement = 6;

  • 其次定义线程块维度

dim3 block(3);

  • 定义线程格维度

dim3 grid((nElement + block.x – 1)/(block.x));(必须这么定义)

网格大小是块大小的倍数.

20.输出每个线程的threadIdxblockIdx的源码:

#include <cuda_runtime.h>

#include <stdlib.h>

#include <string.h>

#include <stdio.h>


__global__ void print_Id(void){

printf("threadIdx:(%d,%d,%d),blockIdx:(%d,%d,%d),blockDim:(%d,%d,%d),gridDim:(%d,%d,%d)\n",threadIdx.x,threadIdx.y,threadIdx.z,blockIdx.x,blockIdx.y,blockIdx.z,blockDim.x,blockDim.y,blockDim.z,gridDim.x,gridDim.y,gridDim.z);

}




int main(){

int nElement = 6;

cudaSetDevice(0);

dim3 block(3);

dim3 grid((nElement + block.x - 1)/ block.x);

printf("Host : block: (%d,%d,%d),grid: (%d,%d,%d)\n",block.x,block.y,block.z,grid.x,grid.y,grid.z);

print_Id<<< grid,block >>> ();

cudaDeviceReset();

return 0;

}

21.要确定块尺寸,通常需要考虑:

  • 内核的性能特性

  • GPU资源的限制

22.CUDA内核调用是对C语言函数调用语句的延伸,<<<>>>运算符内是核函数的运行配置。

Kernel_name <<<grid,block>>>(argument list)

执行配置的第一个参数是网格维度,也就是线程块的个数,第二个参数是线程块维度,也就是一个线程块中线程的个数。通过制定这两个参数,可以进行以下配置:

  • 内核中线程的数目

  • 内核中使用的线程的布局

23.核函数调用结束后控制权立刻返回主机端,它的调用与主机线程之间是异步的,要实现核函数与主机端的同步,可使用

cudaError_t cudaDeviceSynchronize(void);

cudaMemcpy函数只有当核函数计算完成后才开始进行数据的传输

24.函数类型限定符:

  • __global__ 函数执行时在设备端执行,调用时在主机端调用或者在计算能力大于3.0的设备端调用,这类函数要求返回类型要为void型。

  • __device__ 函数在设备端执行,且只能在设备端调用

  • __host__ 函数在主机端调用并执行,一般情况下可以忽略不记

25.CUDA核函数的限制

  • 只能访问设备内存(不认可)

  • 必须具有void返回类型(不认可)

  • 不支持可变数量的参数

  • 不支持静态变量

  • 显示异步行为

26.CUDA中实现对向量的相加操作源码:

__global__ void add_vec(float *A,float *B,float *C){

int count = threadIdx.x;

C[count] = A[count] + B[count];

}

27.验证核函数

除了调试工具外,还可以使用printf(argument list)函数来进行验证,

或者在配置过程<<<1,1>>>模拟串行进行编译,同时这也有助于验证数值是否按位计算

28.两个数组相加源码:

#include<stdio.h>

#include<cuda_runtime.h>


voidCHECK(const cudaError_t error){

if(error!= cudaSuccess)

{

printf("error:%s",cudaGetErrorString(error));

exit(1);

}

}


__global__voidarray_add_GPU(float *A,float *B,float *C,size_t count){

int index_count = blockIdx.x * blockDim.x + threadIdx.x;

if(index_count < count)

{

C[index_count] = A[index_count]+B[index_count];

printf("threadIdx:%d,result is %lf\n",threadIdx.x,C[index_count]);

}

}


voidinitial_data(float *A,size_t num){

for (int i = 0; i< num; i++)

{

A[i] = i;

}

}


__host__voidshow_data_host(float *show_data,size_t n){

for (int index_count = 0;index_count < n;index_count ++)

{

printf("Host Show : %lf\n",show_data[index_count]);

}

}



intmain(){

printf("Starting..\n");

cudaSetDevice(0);

float *A,*B,*C;


int nBytes = sizeof(float) * 32;

A =(float *) malloc(nBytes);

B =(float *) malloc(nBytes);

C =(float *) malloc(nBytes);


initial_data(A,32);

initial_data(B,32);


//int nBytes = sizeof(float) * 32;

float *d_A,*d_B,*d_C;

CHECK(cudaMalloc((float **) &d_A,nBytes));

printf("debug\n");

cudaMalloc((float **) &d_B,nBytes);

cudaMalloc((float **) &d_C,nBytes);


cudaMemcpy((float *)d_A,(float *)A,nBytes,cudaMemcpyHostToDevice);

cudaMemcpy((float *)d_B,(float *)B,nBytes,cudaMemcpyHostToDevice);

array_add_GPU<<<1,512>>>(d_A,d_B,d_C,32);

cudaDeviceSynchronize();

cudaMemcpy((float *)C,(float *)d_C,nBytes,cudaMemcpyDeviceToHost);

show_data_host(C,32);

cudaFree(d_C);

cudaFree(d_A);

cudaFree(d_B);

free(A);

free(B);

free(C);

cudaDeviceReset();

return 0;

}

29.如果使用了合适的块大小和网格大小来正确地组织线程,那么会对性能进行极大的优化。

*30.矩阵运算,传统的方法是对CUDA使用一个包含二维网格与二维块的布局来组织线程,还可以使用如下布局:

  • 由二维线程块构成的二维网格

  • 由一维线程块构成的一维网格

  • 由一位线程块构成的二维网格

31.通常情况下,一个矩阵的存储方式通常是行优先的方式在全局内存中进行线性存储。

*32.对于一个矩阵来说通常需要三种方式索引:

  1. 线程和块索引

  2. 矩阵中给定点的坐标

  3. 全局限性内存中的偏移量

*33.首先,通过以下公式把块索引和线程索引映射到矩阵的坐标上

ix = blockIdx.x * blockDim.x + threadIdx.x

iy = blockIdx.y * blockDim.y + threadIdx.y

其次,将矩阵坐标映射到全局内存的索引上:

idx = iy * nx + ix

35.使用二维网格和一维块进行矩阵求和操作,即一个块针对一个数据进行优化,那么此时

ix = threadIdx.x + blockDim.x * blockIdx.x

iy = blockIdx.y;

将矩阵坐标映射到全局内存的索引上:

idx = iy * nx + ix;

通过上述的几个不同的核函数配置和线程块和网格的布局不同可以认为:

  • 改变执行配置对内核性能会产生影响

  • 传统的核函数不一定能够达到最优性能

  • 对于一个核函数,可以进行多次尝试找到最优性能

36.对于核函数的调试,当发现核函数无法进入等意外出现的时候,可以使用

cudaError_t cudaGetLastError();

获得,将该函数放置在核函数运行的下方,函数将会返回CUDA中所产生的错误。

posted @ 2019-02-13 12:01  小小小二  阅读(348)  评论(0编辑  收藏  举报