cuda c和c++的简单介绍

原文地址,纯翻译

https://developer.nvidia.com/blog/easy-introduction-cuda-c-and-c/

  这是cuda并行计算平台 c和c++接口系列的第一篇文章。学习前要求熟练掌握c,针对cuda fortran编程的帖子也会同步更新。这两个系列将涵盖cuda平台上并行计算基本概念。从这里开始,除非有特别说明,否则我将使用属于"cuda c"作为cuda c和c++的简写。cuda c本质上是带有一些拓展的c/c++,允许使用多个并行线程在gpu上执行的函数。

 

CUDA编程模型基础

  在接触cuda c代码之前,那些刚接触cuda的人最好先了解cuda编程模型基本描述和其中的一些术语。

  cuda编程模型是一种同时使用cpu和gpu的异构模型。在cuda中,host指cpu及其内存,device指gpu及其内存,host上运行的代码可以管理host和device上的内存、启动kernel(核函数),这些kernel是device上运行的函数,他们由gpu上的许多线程并发运行。

  鉴于cuda编程模型的异构性质,cuda c程序的普遍操作顺序是:

  1.声明和分配host端,device端内存

  2.初始化host端数据

  3.将数据从host端传送到device端

  4.执行一个或多个核函数

  5.将结果从device端传回host端

  记住这个操作流程,让我们来看一个cuda c的例子

 

第一个cuda c程序

  上一篇文章中,我介绍了六种SAXPY(Scalar Alpha X Plus Y)的方法,其中就包括了cuda c版本,SAXPY表示单精度A*X+Y,对于并行计算来说是一个很好的hello world程序。在这篇文章中,我将展示一个cuda SAXPY的更完整版本,详细说明做了什么以及为什么这样做,完整的SAXPY代码如下:

#include <stdio.h>

__global__ void saxpy(int n,float a,float *x,float *y)
{
    int i=blockIdx.x*blockDim.x+threadIdx.x;
    if(i<n)
    {
        y[i]=a*x[i]+y[i];
    }
}

int main(void)
{
    int N=1<<20;    // 1左移20
    float *x,*y,*d_x,*d_y;
    x=(float*)malloc(N*sizeof(float));
    y=(float*)malloc(N*sizeof(float));

    cudaMalloc(&d_x,N*sizeof(float));
    cudaMalloc(&d_y,N*sizeof(float));

    for(int i=0;i<N;++i)
    {
        x[i]=1.0f;
        y[i]=2.0f;
    }

    cudaMemcpy(d_x,x,N*sizeof(float),cudaMemcpyHostToDevice);
    cudaMemcpy(d_y,y,N*sizeof(float),cudaMemcpyHostToDevice);

    // perform SAXPY in 1M elements
    int threads=256;
    int blocks=(N+threads-1)/threads;   //  上取整
    saxpy<<<blocks,threads>>>(N,2.0f,d_x,d_y);
    
    cudaMemcpy(y,d_y,N*sizeof(float),cudaMemcpyDeviceToHost);

    float maxError=0.0f;
    for(int i=0;i<N;++i)
    {
        maxError=max(maxError,abs(y[i]-4.0f));
    }
    printf("Max error: %f\n",maxError);

    cudaFree(d_x);
    cudaFree(d_y);
    free(x);
    free(y);

    return 0;
}

  函数saxpy就是在gpu上运行的核函数,main函数是host端代码,让我们先从host端代码开始讨论这个程序

Host端代码

  主函数声明了两对数组

float *x,*y,*d_x,*d_y;
x=(float*)malloc(N*sizeof(float));
y=(float*)malloc(N*sizeof(float));

cudaMalloc(&d_x,N*sizeof(float));
cudaMalloc(&d_y,N*sizeof(float));

  指针x和y指向host端数组,使用malloc函数申请内存空间,d_x,d_y数组指向device端数组,使用cuda runtime api中的cudaMalloc函数申请空间。cuda编程中host端和device端具有独立的内存空间,两者都可以通过host端代码进行管理(cuda c内核还可以在支持他的设备上分配设备内存)。

  host端代码随后初始化host端数组。在此,我们将x设为元素全部为1的数组,y设为元素全为2的数组

for(int i=0;i<N;++i)
{
    x[i]=1.0f;
    y[i]=2.0f;
}

  要初始化device端数组,我们只需使用cudaMemcpy函数将数据从x,y拷贝到device端对应数组d_x,d_y上,这个过程就像c语言中的memcpy函数,唯一的区别就是cudaMemcpy需要地四个参数来指定数据拷贝到方向(host端到device端还是device端到host端),在此例中,我们使用cudaMemcpyHostToDevice表示数据从host拷贝到device端。

cudaMemcpy(d_x,x,N*sizeof(float),cudaMemcpyHostToDevice);
cudaMemcpy(d_y,y,N*sizeof(float),cudaMemcpyHostToDevice);

  在运行完核函数后,为了将数据d_y从device端拷贝回host端的y,我们使用cudaMemcpy函数,第四个参数指定为cudaMemcpyDeviceToHost

cudaMemcpy(y,d_y,N*sizeof(float),cudaMemcpyDeviceToHost);

启动核函数

  saxpy核函数由以下代码启动

saxpy<<<blocks,threads>>>(N,2.0f,d_x,d_y);

  <<<>>>中间的信息是核函数的执行配置,具体指有多少设备线程并行执行内核。在cuda中,软件有一个线程层次结构,它模仿线程处理器在gpu上的分组方式。在cuda编程模型中,我们通常说通过grid来启动线程block。执行配置中的第一个参数指定grid中的block数目,第二个参数指定一个block中的线程数。

  block和grid可以通过给dim3传值构造成1维,2维或3维度数据结构(由cuda定义的具有x,y,z策划那个圆的简单结构),但是对于这个简单的例子,我们只需要一个维度,所以我们传递一个整数。在这个例子中,我们设置一个block中包含256个线程,使用取整算法来计算得到处理数组N个元素所需要的block数目((N+256-1)/256)。

  对于数组中元素的数量不能被线程块大小整除的情况,内核代码必须检查越界内存访问。(我认为就是检测取整多出来的那部分)

释放内存

  程序结束时,我们需要释放掉所有申请的内存空间。对于用cudaMalloc()申请的device端内存,使用cudaFree()来释放。对于host端申请单内存,使用free()来释放。

cudaFree(d_x);
cudaFree(d_y);
free(x);
free(y);

device端代码

  现在我们来看核函数代码

__global__ void saxpy(int n,float a,float *x,float *y)
{
    int i=blockIdx.x*blockDim.x+threadIdx.x;
    if(i<n)
    {
        y[i]=a*x[i]+y[i];
    }
}

  在cuda中,我们使用__global__声明符来声明核函数。device代码中定义的变量不需要指定为device变量,因为他们被指定驻留在device上。(我的理解是核函数里声明的变量,他的生命周期就是该线程上执行的核函数生命周期)。在本例中,n,a和i变量将由每个线程存储在一个寄存器中,并且指针x和y必须是指向device内存地址空间的指针。因为当我们从host端启动核函数时,我们要将d_x,d_y传递给kernel。前两个变量n和a,我们没有明确的代码将它们从host端传到device端,由于函数参数在c/c++中默认按值传递,因此cuda运行时可以自动处理这些值到device的传输。cuda runtime api这个特性使得gpu上启动核函数非常自然和容易,几乎与调用c函数一样。

  核函数saxpy的代码只有两行,前面我们提到过,核函数是由多线程并发执行的。如果我们希望每个线程处理数组的一个元素,那么我们需要一种区分和识别每个线程的方法。cuda定义了变量blockDim,blockIdx和threadIdx。这些预定义变量的类型为dim3,类似于主机代码中的执行配置参数。预定义变量blockDim包含在核函数启动的第二个执行配置参数中,指定block中的线程个数(维度)。预定义变量threadIdx和blockIdx分别表示block内的线程索引和grid内地block索引。表达方式:

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

  生成用于访问数组元素的全局索引。我们在这个例子中没有使用gridDim,gridDim表示核函数启动的第一个执行配置参数中指定的grid尺寸(一个grid里面有多少个block)

  在使用索引访问数组元素之前,会根据元素个数n检查索引是否合法,以确保没有内存越界访问。如果数组中的元素不能被block中的线程数整除,(由于向上取整)核函数启动的线程数会大于数组大小,此时需要进行检查。核函数的第二行逐个元素执行SAXPY操作,除了边界检查外,他的结果等于host端通过循环实现的SAXPY

if(i<n) y[i]=a*x[i]+y[i];

 

编译和运行代码

  cuda c编译器,nvcc是NVIDIA CUDA Toolkit(http://www.nvidia.com/content/cuda/cuda-toolkit.html)的一部分。为了编译我们的SAXPY例子,我们将代码保存为.cu格式,文件名是saxpy.cu。我们可以使用nvcc编译它

nvcc -o saxpy saxpy.cu

  我们可以运行代码

./saxpy

 

 

总结与结论

  通过使用cuda c对saxpy简单实现,我们现在了解了cuda编程的基础知识。将c代码移植到cuda c上只需要几个c扩展:核函数的__global__声明符;核函数启动时的执行配置(<<<blocks,threads>>>);用于识别和区分并行执行核函数的gpu线程的内置设备变量blockDim,blockIdx和threadIdx。

  异构cuda编程模型的一个优点是,可以增量的将现有代码从c移植到cuda c,一次一个内核。

  在本系列的下一篇文章中,我么将了解一些性能测量和指标。

 
posted @ 2021-12-17 17:24  Wangtn  阅读(1480)  评论(0编辑  收藏  举报