06.CUDA编程模型概述(二)

4. CUDA核函数

  CUDA核函数:在GPU上并行执行的函数称为CUDA核函数(Kernel Function),它属于CUDA编程中最为重要且核心的一个环节,也是我们重点要写的代码部分。我们先从启动一个核函数开始!

5. 启动一个CUDA核函数

C语言中函数调用语句:

function_name(argument list);

CUDA内核调用是对C语言函数调用语句的扩展,<<<>>>运算符内是核函数的执行配置,即需要指定网格和块的维度。

kernel_name<<<grid,block>>>(argument list);

执行配置的第一个值是网格维度,也就是启动块的数目。第二个值是块维度,也就是每个块中线程的数目。通过指定网格和块的维度,你可以进行以下配置:

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

  同一个块中的线程之间可以相互协作,不同块内的线程不能协作。对于一个给定的问题,可以使用不同的网格和块布局来组织你的线程。例如,假设你有32个数据元素用于计算,每8个元素一个块,需要启动4个块。我们可以使用dim3类型的grid维度和block维度配置内核,也可以使用int类型的变量,或者常量直接初始化,如下:

kernel_name<<<4,8>>>(argument list);

由于数据在全局内存中是线性存储的,因此可以用变量blockIdx.x和threadId.x来进行以下操作。

  • 在网格中标识一个唯一的线程
  • 建立线程和数据元素之间的映射关系

如果把所有32个元素放到一个块里,那么只会得到一个块:

kernel_name<<<1,32>>>(argument list);

如果每个块只含有一个元素,那么会有32个块:

kernel_name<<<32,1>>>(argument list);

核函数的调用与主机线程是异步的。核函数调用结束后,控制权立刻返回给主机端。你可以调用以下函数来强制主机端程序等待所有的核函数执行结束:

cudaError_t cudaDeviceSynchronize(void);

这是一个显示的方法,对应的也有隐式方法,隐式方法就是不明确说明主机要等待设备端,而是设备端不执行完,主机没办法进行,比如内存拷贝函数:

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

6. 编写核函数

  核函数用__global__符号声明,在devie(GPU)上执行,在host(CPU)上调用,返回类型必须时void,不支持可变参数,不支持静态变量,不支持函数指针,核函数相对于CPU是异步的,在核函数执行完之前就会返回,这样CPU可以不用等待核函数的完成,继续执行后续代码。在host端核函数的调用方式为:

__global__ void kernel_name(argument list);

  我们之前讲过CPU-GPU的异构模型,所以需要区分host和device上的代码,在CUDA中是通过函数类型限定词来区分host和device上的函数,主要的三个函数类型限定词如下:

限定符执行调用备注
__ global__设备端执行可以从主机调用,也可以从计算能力3以上的设备调用必须有一个void的返回类型
__ device__设备端执行设备端调用
__ host __主机端执行主机端调用一般忽略不写

  __device__和__host__限定符可以一齐使用,这样函数可以同时在主机和设备端进行
编译。

6.1 CUDA核函数的限制

核函数编写有以下限制:

  • 只能访问设备内存
  • 必须具有void返回类型
  • 不支持可变数量的参数
  • 不支持静态变量
  • 显示异步行为

考虑一个简单的例子:循环将两个大小为N的向量A和B相加,主机端代码:

void sumArraysOnHost(float *A, float *B, float *C, const int N) {
  for (int i = 0; i < N; i++)
    C[i] = A[i] + B[i];
}

设备端代码:

__global__ void sumArraysOnGPU(float *A, float *B, float *C) {
  int i = threadIdx.x;
  C[i] = A[i] + B[i];
}

可以看到设备端代码中的循环体消失了,内置的线程坐标变量替换了数组索引

7. 验证核函数

  开发过程中,验证自己的代码是必须的且高效的。这里的验证方法是为了验证我们最开始写的两个数组相加的程序,通过比较两个数组在主机端和设备端分别执行相加得出的结果,来进行验证核函数即设备端的代码是否正确。

#include <stdio.h>
#include <math.h>
void checkResult(float *hostRef, float *gpuRef, const int N){
    double epsilon = 1.0E-8 ;
    int match = 1;
    for (int i=0; i<N; i++){
        if (fabsf(hostRef[i] - gpuRef[i]) > epsilon){
            match = 0;
            printf("Arrays do not match!\n");
            printf("host %5.2f gpu %5.2f at current %d\n", hostRef[i], gpuRef[i], i);
            break;
        }
    }
    if (match){
        printf("Arrays match.\n\n");
        return;
    }
}

math.h:提供了一组用于数学计算的函数,这些函数涵盖了基本的算术运算、三角函数、指数函数、对数函数、幂函数和其他一些常用的数学操作。

abs、fabs 和 fabsf 是C/C++标准库中用于取绝对值的函数,它们的主要区别在于处理的数据类型和返回值类型。

  • abs:
  1. 用于整数数据类型(如 int、long)。
  2. 返回一个整数类型的绝对值。
  3. 不需要包含额外的头文件,通常在 或 <stdlib.h> 中定义。
  • fabs:
  1. 用于双精度浮点数数据类型(double)。
  2. 返回一个双精度浮点数的绝对值。
  3. 需要包含 或 <math.h> 头文件。
  • fabsf:
  1. 用于单精度浮点数数据类型(float)。
  2. 返回一个单精度浮点数的绝对值。
  3. 需要包含 或 <math.h> 头文件。

两个非常简单实用的方法可以验证核函数。

  • 可以在Fermi及更高版本的设备端的核函数中使用printf函数。
  • 可以将执行参数设置为<<<1,1>>>,因此强制用一个块和一个线程执行核函数,这模拟了串行执行程序。这对于调试和验证结果是否正确是非常有用的,而且,如果你遇到了运算次序的问题,这有助于你对比验证数值结果是否是按位精确的。

8. 处理错误

  由于许多CUDA调用是异步的,所以有时可能很难确定某个错误是由哪一步程序引起的。定义一个错误处理宏封装所有的CUDA API调用,这简化了错误检查过程:

#define CHECK(call)                                                             \
{                                                                               \
    const cudaError_t error = call;                                             \
    if (error != cudaSuccess)                                                   \
    {                                                                           \
        printf("Error: %s:%d, ", __FILE__, __LINE__);                           \
        printf("code: %d, reason: %s\n", error, cudaGetErrorString(error));     \
        exit(1);                                                                \
    }                                                                           \
}  

宏定义:是C提供的三种预处理功能的其中一种。其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率。

#define命令是C语言中的一个宏定义命令,它用来讲一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义(不带参数的宏定义),另一种是带参数的宏定义。
例:#define PI 3.1415926
例:#define S(a,b) a*b

注意:

  1. 宏名一般用大写
  2. 宏定义末尾不加分号 ;
  3. 可以用#undef命令终止宏定义的作用域
  4. 宏定义可以嵌套
  5. 字符串“”中永远不包含宏
  6. 宏名和参数的括号间不能有空格

你可以在以下代码中使用宏:

 CHECK(cudaMemcpy(d_c, gpuRef, nBytes, cudaMemcpyHostToDevice));

  如果内存拷贝或之前的异步操作产生了错误,这个宏会报告错误代码,并输出一个可读信息,然后停止程序。也可以用下述方法,在核函数调用后检查核函数错误:

kernel_name<<<grid,block>>>(argument list);
CHECK(cudaDeviceSynchronize());

  CHECK(cudaDeviceSynchronize())会阻塞主机端线程的运行直到设备端所有的请求任务都结束,并确保最后的核函数启动部分不会出错。当然在release版本中可以去除这部分,但是开发的时候一定要有的。

9. 编译和执行

OK,现在把所有的代码放在一个文件名为sumArraysOnGPU-small-case.cu的文件中,如下:

完整代码:https://github.com/dive-into-cuda

#include <cuda_runtime.h>
#include <stdio.h>

#define CHECK(call)                                                             \
{                                                                               \
    const cudaError_t error = call;                                             \
    if (error != cudaSuccess)                                                   \
    {                                                                           \
        printf("Error: %s:%d, ", __FILE__, __LINE__);                           \
        printf("code: %d, reason: %s\n", error, cudaGetErrorString(error));     \
        exit(1);                                                                \
    }                                                                           \
} 

void checkResult(float *hostRef, float *gpuRef, const int N){
    double epsilon = 1.0E-8 ;
    int match = 1;
    for (int i=0; i<N; i++){
        if (fabsf(hostRef[i] - gpuRef[i]) > epsilon){
            match = 0;
            printf("Arrays do not match!\n");
            printf("host %5.2f gpu %5.2f at current %d\n", hostRef[i], gpuRef[i], i);
            break;
        }
    }
    if (match){
        printf("Arrays match.\n\n");
        return;
    }
}

void initialData(float *ip, int size){
    // 为随机数生成不同的种子
    // time_t是一个数据类型,用于表示时间
    time_t t;
    // &t获取变量t的地址
    srand((unsigned int) time(&t));

    for (int i=0; i<size; i++){
        ip[i] = (float)(rand() & 0xFF) / 10.0f;
    }
}

// 当a是一个指针的时候,*a就是这个指针指向的内存的值
// const含义:只要一个变量前用const来修饰,就意味着该变量里的数据只能被访问,而不能被修改,也就是意味着“只读”(readonly)
void sumArraysOnHost(float *A, float *B, float *C, const int N){
    for (int idx=0; idx<N; idx++){
        C[idx] = A[idx] + B[idx];
    }
}

__global__ void sumArraysOnGPU(float*a,float*b,float*res)
{
  int i=threadIdx.x;
  res[i]=a[i]+b[i];
}

int main(int argc, char **argv){
    printf("%s 开始...\n", argv[0]);

    // 设置设备
    int dev = 0;
    cudaSetDevice(dev);

    // 设置向量数据
    int nElem = 32;
    printf("向量大小为 %d\n", nElem);

    // 主机申请内存
    size_t nBytes = nElem * sizeof(float);

    float *h_A, *h_B, *hostRef, *gpuRef;
    h_A     = (float *)malloc(nBytes);
    h_B     = (float *)malloc(nBytes);
    hostRef = (float *)malloc(nBytes);
    gpuRef  = (float *)malloc(nBytes);

    // 主机端初始化数据
    initialData(h_A, nElem);
    initialData(h_B, nElem);

    memset(hostRef, 0, nBytes);
    memset(gpuRef,  0, nBytes);

    // 设备端申请全局内存
    float *d_A, *d_B, *d_C;
    CHECK(cudaMalloc((float**)&d_A, nBytes));
    CHECK(cudaMalloc((float**)&d_B, nBytes));
    CHECK(cudaMalloc((float**)&d_C, nBytes));

    // 将主机数据传到设备端
    CHECK(cudaMemcpy(d_A, h_A, nBytes, cudaMemcpyHostToDevice));
    CHECK(cudaMemcpy(d_B, h_B, nBytes, cudaMemcpyHostToDevice));

    // 在主机端设置线程块,线程格
    dim3 block  (nElem);
    dim3 grid   (nElem/block.x);

    sumArraysOnGPU<<< grid, block >>>(d_A, d_B, d_C);
    printf("线程设置:<<<%d, %d>>>\n", grid.x, block.x);

    // 复制设备端结果到主机
    CHECK(cudaMemcpy(gpuRef, d_C, nBytes, cudaMemcpyDeviceToHost));

    // 主机端计算结果
    sumArraysOnHost(h_A, h_B, hostRef, nElem);

    // 对比设备端和主机端计算结果
    checkResult(hostRef, gpuRef, nElem);

    printf("打印数组A,数组B,主机结果,设备结果前5个数:\n");
    for(int i=0; i < 5; i++){
        printf("h_A[%d]=%f h_B[%d]=%f hostRef[%d]=%f gpuRef[%d]=%f\n", i, h_A[i], i, h_B[i], i, *hostRef[i], i, *gpuRef[i]);
    }

    // 释放设备端内存
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);

    // 释放主机端内存
    free(h_A);
    free(h_B);
    free(hostRef);
    free(gpuRef);

    return 0;
}

size_t:是个无符号整型,它并不是一个全新的数据类型,更不是一个关键字。size_t是由typedef定义而来的,我们在很多标准库头文件中都能发现。size_t的含义是size type,是一种计数类型。取值范围与机器架构与操作系统相关。32 位机器一般是unsigned int,占 4 字节;而 64 位机器一般是unsigned long,占 8 字节。
size_t类型常被用作计数用途,例如:sizeof运算符得到对象所占的字节数;字符串函数strlen返回字符串的长度等等,其返回值都为size_t类型。

C语言的内存必须初始化。‌在C语言中,‌不仅新定义的变量需要初始化,‌新分配的内存空间也需要初始化。‌

memset:是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作,它是直接操作内存空间。memset 一般使用“0”初始化内存单元,而且通常是给数组或结构体进行初始化。一般的变量如 char、int、float、double 等类型的变量直接初始化即可,没有必要用 memset。如果用 memset 的话反而显得麻烦。
函数原型:void *memset(void *s, int c, unsigned long n);
将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 c 替换并返回 s。

执行结果如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值