cuda程序的基本框架
头文件
常量定义(或者宏定义)
C++自定义函数和cuda核函数的声明
int main()
{
a分配主机与设备内存
初始化主机中的数据
将某些数据从主机复制到设备
调用核函数在设备中进行计算
将某些数据从设备复制到主机
释放主机与设备内存
}
c++自定义函数和cuda核函数的实现
下面通过列子来体现这个框架
这里直接使用c++编写程序的习惯写的数组相加的程序:
#include
#include
#include
const double EP = 1.0e-15;
const double a = 1.23;
const double b = 2.34;
const double c = 3.57;
void add(const double *x,const double *y,double *z,const int N);
void check(const double *z,const int N);
int main()
{
const int N = 100000000;
const int M = sizeof(double)*N;
// 分配内存
double *x = (double *)malloc(M);
double *y = (double *)malloc(M);
double *z = (double *)malloc(M);
// double *x = new double[M];
// double *y = new double[M];
// double *z = new double[M];
// 初始化
for(int n=0;n<N;++n)
{
x[n] = a;
y[n] = b;
}
// 数组求和
add(x,y,z,N);
check(z,N);
// 释放内存
free(x);
free(y);
free(z);
// delete []x;
// delete []y;
// delete []z;
return 0;
}
void add(const double *x,const double *y,double *z,const int N)
{
for(int n=0;n<N;++n)
{
z[n] = x[n] + y[n];
}
}
void check(const double *z, const int N)
{
bool has_error = false;
for(int n=0;n<N;++n)
{
if(fabs(z[n]-c)>EP)
{
has_error = true;
}
}
printf("%s\n",has_error?"HAS_ERROR":"NO_ERROR");
}
// 头文件
#include
#include
#include
// 常量定义
const double EP = 1.0e-15;
const double a = 1.23;
const double b = 2.34;
const double c = 3.57;
// c++自定义函数和cuda核函数的声明
__global__ void add(const double *x,const double *y,double *z);
void check(const double *z,const int N);
int main()
{
const int N = 100000000;
const int M = sizeof(double)*N;
// 分配主机内存
double *h_x = (double *)malloc(M);
double *h_y = (double *)malloc(M);
double *h_z = (double *)malloc(M);
// double *h_x = new double[M];
// double *h_y = new double[M];
// double *h_z = new double[M];
// 分配设备内存
double *d_x,*d_y,*d_z;
// printf("%p",d_x);
cudaMalloc((void **)&d_x,M);
cudaMalloc((void **)&d_y,M);
cudaMalloc((void **)&d_z,M);
// 初始化主机上的数据
for(int n=0;n<N;++n)
{
h_x[n] = a;
h_y[n] = b;
}
// 将某些数据从主机复制到设备上
cudaMemcpy(d_x,h_x,M,cudaMemcpyHostToDevice);
cudaMemcpy(d_y,h_y,M,cudaMemcpyHostToDevice);
// 调用核函数在设备中进行计算,数组求和
const int block_size = 128; // 不同型号的GPU有线程限制,开普勒到图灵最大为1024
const int gride_size = N/block_size;
add<<<gride_size,block_size>>>(d_x,d_y,d_z);
// 将某些数据从设备复制到主机上,这个数据传输函数隐式的起到了同步主机与设备的作用,所以后面用不用cudaDeviceSynchronize都可以
cudaMemcpy(h_z,d_z,M,cudaMemcpyDeviceToHost);
check(h_z,N);
// 释放内存
free(h_x);
free(h_y);
free(h_z);
// delete []h_x;
// delete []h_y;
// delete []h_z;
cudaFree(d_x);
cudaFree(d_y);
cudaFree(d_z);
return 0;
}
__global__ void add(const double *x,const double *y,double *z)
{
// 单指令-多线程,注意核函数中数据与线程的对应关系
const int n = blockDim.x * blockIdx.x + threadIdx.x;
z[n] = x[n] + y[n];
}
void check(const double *z, const int N)
{
bool has_error = false;
for(int n=0;n<N;++n)
{
if(fabs(z[n]-c)>EP)
{
has_error = true;
}
}
printf("%s\n",has_error?"HAS_ERROR":"NO_ERROR");
}
// 单指令-多线程,注意核函数中数据与线程的对应关系
const int n = blockDim.x * blockIdx.x + threadIdx.x;
blockDim指的是每个线程块的线程总固定大小;
blockIdx指的是线程块的索引;
threadIdx指的是线程的索引;
所以具体的一维线程索引会等于上面代码式子所计算的索引
核函数中数据与线程的对应关系时,注意到保存相应数据类型的内存大小N是自己定义的,所以就存在一个问题,如果gride_size = N/block_size不是刚好整数倍时,就有可能引发错误(越界)。所以,尽可能让定义的线程个数多于元素个数,然后通过条件语句来规避不需要的线程操作。可以写成
int gride_size = (N%block_size==0?(N/block_size):(N/block_size + 1));
// 简化成
int gride_size = (N-1) / block_size + 1; //or
int gride_size = (N+block_size-1) / block_size;
同时,在核函数里使用if条件语句规避不需要的线程操作:
// 参数传入了 N
void __global__ add(const double *x, const double *y, double *z, const int N)
{
const int n = blockDim.x * blockIdx.x + threadIdx.x;
if (n < N)
{
z[n] = x[n] + y[n];
}
}
编译的时候,用nvcc编译会将设备代码编译为PTX(parallel thread execution)伪汇编代码,再将伪汇编代码编译成二进制的cubin代码。在编译为PTX代码时,需要指定GPU架构的计算能力。默认是2.0,可以自己看看自己GPU的算力,用nvcc编译时,加上-arch=sm_XY语句。如:
nvcc -g -arch=sm_75 xxx.cu -o xxx // GeForce RTX2080是7.5的
通过结合上面的基本框架流程和未使用cuda核函数的c++程序进行比较:
上面的cuda运行的api可以在官方文档查看使用说明:
https://docs.nvidia.com/cuda/cuda-runtime-api
如cudaMalloc,直接在搜索框里搜索,然后点进去查看说明:
这里关注下返回值和输入参数:返回值是一个cudaError_t类型的信息,表示是否成功运行此api,对于后面的程序检查有用,输入参数void **devPtr,因为返回值已经使用了,所以输入参数使用一个双重指针,指向指针的指针来指向数据原地址。
在cuda程序中,可以使用标识符确定一个函数在哪里被调用和执行。
(1)用__global__修饰的函数称为核函数。一般由主机调用,在设备中执行。动态并行时,也可以在核函数中互相调用。
(2)用__device__修饰的函数称为设备函数,只能被核函数或其他设备函数调用,在设备中执行。
(3)用__host__修饰的函数就是主机端的普通C++函数,在主机中调用,在主机中执行。也可以省略,之所以提供这个修饰符,主要是有时可以用__host__和__device__同时修饰一个函数,这样可以避免代码的冗余。
(4)可以使用修饰符__noinline__或者__forceinline__建议一个设备函数为非内联函数或内联函数。
注意:
1.不能同时使用__global__和__device__修饰同一个函数。
2.不能同时使用__global__和__host__修饰同一个函数。
熟悉cuda程序的基本框架。
如博客内容有侵权行为,可及时联系删除!
CUDA 编程:基础与实践
https://docs.nvidia.com/cuda/
https://docs.nvidia.com/cuda/cuda-runtime-api
https://github.com/brucefan1983/CUDA-Programming