cuda核函数

核函数的要求

  • 核函数的返回类型必须是 void。所以,在核函数中可以用 return 关键字,但不可返回任何值。
  • 必须使用限定符 __ global __。也可以加上一些其他 C++ 中的限定符,如 static。限定符的次序可任意。
  • 函数名无特殊要求,而且支持 C++ 中的重载(overload),即可以用同一个函数名表示具有不同参数列表的函数。
  • 不支持可变数量的参数列表,即参数的个数必须确定。
  • 可以向核函数传递非指针变量,其内容对每个线程可见。
  • 除非使用统一内存编程机制,否则传给核函数的数组(指针)必
    须指向设备内存。
  • 核函数不可成为一个类的成员。通常的做法是用一个包装函数调用核函数,而将包装函数定义为类的成员。
  • 在计算能力 3.5 之前,核函数之间不能相互调用。从计算能力 3.5 开始,引入了动态并行(dynamic parallelism)机制,在核函数内部可以调用其他核函数,甚至可以调用自己。
  • 无论是从主机调用,还是从设备调用,核函数都是在设备中执行。调用核函数时必须指定执行配置,即三括号和它里面的参数。

核函数的注意事项

c++ 复制代码
__global__:在device上执行,从host中调用(一些特定的GPU可以从device上调用),返回值必须是void,不支持可变参数,不能是类成员函数。 
c++ 复制代码
__global__定义的核函数是异步的,主机不会主动等待核函数执行完在执行后续操作。
c++ 复制代码
__device__:在device上执行,但仅可从device中调用,不能与__global__共用。
c++ 复制代码
__host__:在主机上执行,仅可以从host上调用,一般忽略不写,不可以与__global__同时使用,但可和__device__同时使用,此时函数会在device和host都编译。

计算线程索引

c++ 复制代码
//! 获取线程ID(1维块和2维网格)
#define getThreadId() (blockDim.x * (blockIdx.x + blockIdx.y * gridDim.x) + threadIdx.x)
//! 获取块ID(2维网格)
#define getBlockId() (blockIdx.x + blockIdx.y * gridDim.x)
//! 计算需要用到的数组元素索引(1维)
const int n = blockDim.x * blockIdx.x + threadIdx.x;
z[n] = x[n] + y[n];

核函数的基本使用

c++ 复制代码
//! 网格大小(线程块的大小),线程块大小(线程块中的线程数)
hello_from_gpu << <2, 4 >> > ();
  • blockDim表示一个线程块里有多少个线程。

    c++ 复制代码
    blockDim.x //! X方向上的线程数 
  • blockIdx表示当前线程块在整个网格中是第几块。

    c++ 复制代码
    blockIdx.x //! 当前线程块在 X 维度的索引(从 0 开始计数)。
  • threadIdx表示当前线程在线程块的索引(在哪个索引的线程)

    c++ 复制代码
    threadIdx.x	//! X方向的线程索引

threadIdx与blockIdx的区别:

  • blockIdx全局唯一的,指明线程所在grid中的位置。(整个网格里,第 0 块就只有一个)
  • threadIdx局部唯一 的,指明线程所在block中的位置。(每一个块里,都有一个 threadIdx.x = 0 的线程)。

自定义设备函数

函数执行空间标识符

标识符确定一个函数在哪里被调用以及在哪里执行:

  • __ global __ 修饰的函数称为核函数,一般由主机调用,在设备中执行。如果使用动态并行,则也可以在核函数中调用自己或其他核函数。
  • 用 __ device__ 修饰的函数叫称为设备函数,只能被核函数或其他设备函数调用,在设备中执行。
  • 用 __ host__ 修饰的函数就是主机端的普通 C++ 函数,在主机中被调用,在主机中执行。
  • 不能同时用 __ device__ 和 __ global__ 修饰一个函数,即不能将一个函数同时定义为设备函数和核函数。
  • 不能同时用 __ host__ 和 __ global__ 修饰一个函数,即不能将一个函数同时定义为主机函数和核函数。
  • 编译器决定把设备函数当作内联函数(inline function)或非内联函数,但可以用修饰符 __ noinline__ 建议一个设备函数为非内联函数(编译器不一定接受),也可以用修饰符 __ forceinline__ 建议一个设备函数为内联函数。
c++ 复制代码
//! 有返回值的设备函数
double __device__ add1_device(const double x, const double y)
{
    return (x + y);
}

void __global__ add1(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] = add1_device(x[n], y[n]);
    }
}


//! 有指针的设备函数
void __device__ add2_device(const double x, const double y, double* z)
{
    *z = x + y;
}

void __global__ add2(const double *x, const double *y, double *z, const int N)
{
    const int n = blockDim.x * blockIdx.x + threadIdx.x;
    if(n < N)
    {
        add2_device(x[n], y[n], &z[n]);
    }
}

//! 引用的设备函数
void __device__ add3_device(const double x, const double y, double &z)
{
    z = x + y;
}

void __global__ add3(const double *x, const double *y, double *z, const int N)
{
    const int n = blockDim.x * blockIdx.x + threadIdx.x;
    if(n < N)
    {
        add3_device(x[n], y[n], z[n]);
    }
}

CUDA程序的错误检测

cuda接口都有一个类型为cudaError_t的返回值,代表了一种错误信息,只有返回值为cudaSuccess时才代表成功调用了接口。

c++ 复制代码
#pragma once
#include <stdio.h>
#define CHECK(call) \
do  \
{   \
    const cudaError_t error_code = call;    \
    if(error_code != cudaSuccess)           \
    {                                       \
        printf("CUDA Error:\n");            \   
        printf("File:   %s\n", __FILE__);   \
        printf("Line:   %d\n", __LINE__);   \
        printf("Error code: %d\n", error_code); \
        printf("Error text: %s\n", cudaGetErrorString(error_code)); \
        exit(1);                            \
    }                                       \
}while(0)                                   \
检查核函数

由于核函数不返回任何值,采用以下方式可以捕捉调用核函数可能发生的错误。

c++ 复制代码
hello_from_gpu << <2, 4 >> > ();
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());

第一条语句是捕捉第二个语句之前的最后一个错误,第二个语句的作用是同步主机与设备。之所以要同步主机与设备,是因为核函数的调用是异步的,即主机发出调用核函数的命令后会立即执行后面的语句,不会等待核函数执行完毕。

同步函数是比较耗时的,只建议在调试程序阶段使用。

或者通过将环境变量CUDA_LAUNCH_BLOCKING的值设置为1:

shell 复制代码
export CUDA_LAUNCH_BLOCKING=1

该环境变量的作用是将核函数的调用从异步改为同步(主机调用核函数之后,必须等待核函数执行完毕,才能继续执行,仅在调试阶段使用)。

检查显存错误(CUDA-MEMCHECK)

CUDA-MEMCHECK工具集具体包括memcheck,racecheck,initcheck,synccheck四个工具可由可执行可执行文件cuda-memcheck调用。

c++ 复制代码
cuda-memcheck --tool memcheck [options] app_name [options]
cuda-memcheck --tool racecheck [options] app_name [options]
cuda-memcheck --tool initcheck [options] app_name [options]
cuda-memcheck --tool synccheck [options] app_name [options]

例如:

c++ 复制代码
cuda-memcheck ./LearnCuda.exe

GPU加速

CUDA事件计时
c++ 复制代码
cudaEvent_t start, stop;
CHECK(cudaEventCreate(&start));
CHECK(cudaEventCreate(&stop));
CHECK(cudaEventRecord(start));
cudaEventQuery(start);
//! 运算核函数
CHECK(cudaEventRecord(stop));
CHECK(cudaEventSynchronize(stop));
float elapsed_time;
CHECK(cudaEventElapsedTime(&elapsed_time, start, stop));
printf("Time = %gms.\n", elapsed_time);
CHECK(cudaEventDestroy(start));
CHECK(cudaEventDestroy(stop));
nvprof工具

可用于对CUDA程序进行更多的性能剖析。

c++ 复制代码
nvprof ./a.out
nvprof --unified-memory-profiling off ./a.out
  • 第一列是列出的每类操作所用时间的百分比。

  • 第二列是每类操作用的总时间。

  • 第三列是每类操作被调用的次数。

  • 第四列是每类操作单次调用所用时间的平均值。

  • 第五列是每类操作单次调用所用时间的最小值。

  • 第六列是每类操作单次调用所用时间的最大值。

  • 第七列是每类操作的名称,从这里的输出可以看出核函数的执行时间及数据传输所用时间

核函数加速的要点

CUDA 程序能够获得高性能的必要(但不充分)条件有如下几点:

  • 数据传输比例较小。
  • 核函数的算术强度较高。
  • 核函数中定义的线程数目较多。

优化 CUDA 程序时,(主要是指仔细设计算法)做到以下几点:

  • 减少主机与设备之间的数据传输。
  • 提高核函数的算术强度。
  • 增大核函数的并行规模。

CUDA的内存组织

一般来说,延迟低(速度高)的内存容量小,延迟高(速度低)的内存容量大。

当前被处理的数据一般存放于低延迟、低容量的内存中;当前没有被处理但之后将要被处理的大量数据一般存放于高延迟、高容量的内存中。相对于不用分级的内存,用这种分级的内存可以降低延迟,提高计算效率。

内存类型 物理位置 访问权限 可见范围 生命周期
全局内存 在芯片外 可读可写 所有线程和主机端 由主机分配与释放
常量内存 在芯片外 仅可读 所有线程和主机端 由主机分配与释放
纹理和表面内存 在芯片外 一般仅可读 所有线程和主机端 由主机分配与释放
寄存器内存 在芯片内 可读可写 单个线程 所在线程
局部内存 在芯片外 可读可写 单个线程 所在线程
共享内存 在芯片内 可读可写 单个线程 所在线程
全局内存

全局内存的主要角色是为核函数提供数据,并在主机与设备及设备与设备之间传递数

据。首先,我们用 cudaMalloc 函数为全局内存变量分配设备内存。然后,可以直接在核函数中访问分配的内存,改变其中的数据值。

全局内存对整个网格的所有线程可见。也就是说,一个网格的所有线程都可以访问(读或写)传入核函数的设备指针所指向的全局内存中的全部数据。全局内存的生命周期(lifetime)不是由核函数决定的,而是由主机端决定的。

静态全局内存变量操作

静态全局内存变量,其所占内存数量是在编译期间就确定的。而且,这样的静态全局内存变量必须在所有主机与设备函数外部定义,所以是一种"全局的静态全局内存变量"。

在核函数中,可直接对静态全局内存变量进行访问,并不需要将它们以参数的形式传给核函数。不可在主机函数中直接访问静态全局内存变量,但可以用 cudaMemcpyToSymbol 函数 和 cudaMemcpyFromSymbol 函 数 在 静 态 全 局 内 存 与 主 机 内 存 之 间 传 输 数 据。

c++ 复制代码
__device__ int d_x = 1;
__device__ int d_y[2];

void __global__ my_kernel(void)
{
    d_y[0] += d_x;
    d_y[1] += d_x;
    printf("d_x = %d, d_y[0] = %d, d_y[1] = %d.\n", d_x, d_y[0], d_y[1]);
}

void main()
{
    int h_y[2] = {10, 20};
    //! 主机内存拷贝到设备静态内存
    cudaMemcpyToSymbol(d_y, h_y, sizeof(int) * 2);
    
    my_kernel<<<1, 1>>>();
    cudaDeviceSynchronize();

    cudaMemcpyFromSymbol(h_y, d_y, sizeof(int) * 2);
    printf("h_y[0] = %d, h_y[1] = %d.\n", h_y[0], h_y[1]);
}
常量内存

常量内存是有常量缓存的全局内存,数量有限,一共仅有 64 KB。它的可见范围和生命周期与全局内存一样。不同的是,常量内存仅可读、不可写。由于有缓存,常量内存的访问速度比全局内存高,但得到高访问速度的前提是一个线程束中的线程(一个线程块中相邻的 32 个线程)要读取相同的常量内存数据。

一个使用常量内存的方法是在核函数外面用 constant 定义变量,并用前面介绍

的 CUDA 运行时 API 函数 cudaMemcpyToSymbol 将数据从主机端复制到设备的常量内存后

供核函数使用。当计算能力不小于 2.0 时,给核函数传递的参数(传值,不是像全局变量

那样传递指针)就存放在常量内存中,但给核函数传递参数最多只能在一个核函数中使用

4 KB 常量内存。

纹理内存和表面内存

纹理内存(texture memory)和表面内存(surface memory)类似于常量内存,也是一

种具有缓存的全局内存,有相同的可见范围和生命周期,而且一般仅可读(表面内存也可

写)。不同的是,纹理内存和表面内存容量更大,使用方式和常量内存也不一样。

寄存器

在核函数中定义的不加任何限定符的变量一般来说就存放于寄存器(register)中。核函数中定义的不加任何限定符的数组有可能存放于寄存器中,但也有可能存放于局部内存中。另外,以前提到过的各种内建变量,如 gridDim、blockDim、blockIdx、threadIdx 及 warpSize 都保存在特殊的寄存器中。

c++ 复制代码
//! n就是一个寄存器变量
const int n = blockDim.x * blockIdx.x + threadIdx.x;

寄存器变量仅仅被一个线程可见。也就是说,每一个线程都有一个变量 n 的副本。虽然在核函数的代码中用了这同一个变量名,但是不同的线程中该寄存器变量的值是可以不同的。每个线程都只能对它的副本进行读写。寄存器的生命周期也与所属线程的生命周期一致,从定义它开始,到线程消失时结束。

局部内存

局部内存和寄存器几乎一样。核函数中定义的不加任何限定符的变量有可能在寄存器中,也有可能在局部内存中。寄存器中放不下的变量,以及索引值不能在编译时就确定的数组,都有可能放在局部内存中。这种判断是由编译器自动做的。

局部内存在用法上类似于寄存器,但从硬件来看,局部内存只是全局内存的一部分。所以,局部内存的延迟也很高。每个线程最多能使用高达 512 KB 的局部内存,但使用过多会降低程序的性能。

共享内存

共享内存和寄存器类似,存在于芯片上,具有仅次于寄存器的读写速度,数量也有限。

不同于寄存器的是,共享内存对整个线程块可见,其生命周期也与整个线程块一致。也就是说,每个线程块拥有一个共享内存变量的副本。共享内存变量的值在不同的线程块中可以不同。一个线程块中的所有线程都可以访问该线程块的共享内存变量副本,但是不能访问其他线程块的共享内存变量副本。共享内存的主要作用是减少对全局内存的访问,或者改善对全局内存的访问模式。

SM(流多处理器)及其占有率

一个线程块上的线程是放在同一个流式多处理器上的,单个SM的资源有限,导致线程块中的线程数量是限制的,现代GPU的线程块可支持的线程数可达1024个。

一个GPU 是由多个 SM 构成的。一个 SM 包含如下资源:

  • 一定数量的寄存器
  • 一定数量的共享内存
  • 常量内存的缓存
  • 纹理和表面内存的缓存
  • 两个(计算能力 6.0)或 4 个(其他计算能力)线程束调度器(warp scheduler),用于
    在不同线程的上下文之间迅速地切换,以及为准备就绪的线程束发出执行指令。
  • 执行核心包括:
    • 若干整型数运算的核心。
    • 若干单精度浮点数运算的核心。
    • 若干双精度浮点数运算的核心。
    • 若干单精度浮点数超越函数的特殊函数单元。
    • 若干混合精度的张量核心。

一个 SM 中的各种计算资源是有限的,那么有些情况下一个 SM 中驻留的线程数目就有可能达不到理想的最大值。此时,我们说该 SM 的占有率小于 100%。获得 100% 的占有率并不是获得高性能的必要或充分条件,但一般来说,要尽量让 SM 的占有率不小于某个值,比如 25%,才有可能获得较高的性能。

计算能力 3.5 6.0 7.0 7.5
GPU 代表 Tesla K40 Tesla P100 Tesla V100 Geforce RTX 2080
SM 寄存器数上限 64 *1024 64 * 1024 64 * 1024 64 * 1024
单个线程块寄存器数上限 64 * 1024 64 * 1024 64 * 1024 64 * 1024
单个线程寄存器数上限 255 255 255 255
SM 共享内存上限 48 KB 64 KB 96 KB 64 KB
单个线程块共享内存上限 48 KB 48 KB 96 KB 64 KB
CUDA运行时API函数查询设备

查询显卡设备信息。

c++ 复制代码
int device_id = 0;
cudaSetDevice(device_id);

cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, device_id);
printf("Device id: %d\n", device_id);
printf("Device name: %s\n", prop.name);
printf("Compute capability: %d.%d\n", prop.major, prop.minor);
printf("Amount of global memory: %gGB\n", prop.totalGlobalMem / (1024.0 * 1024 * 1024));
printf("Amount of constant memory: %g KB\n",prop.totalConstMem / 1024.0);
printf("Maximum grid size: %d %d %d\n",prop.maxGridSize[0],prop.maxGridSize[1], prop.maxGridSize[2]);
printf("Maximum block size: %d %d %d\n", prop.maxThreadsDim[0], prop.maxThreadsDim[1], prop.maxThreadsDim[2]);
printf("Number of SMs: %d\n", prop.multiProcessorCount);
printf("Maximum amount of shared memory per block: %g KB\n", prop.sharedMemPerBlock / 1024.0);
printf("Maximum amount of shared memory per SM: %g KB\n", prop.sharedMemPerMultiprocessor / 1024.0);
printf("Maximum number of registers per block: %d K\n", prop.regsPerBlock / 1024);
printf("Maximum number of registers per SM: %d K\n", prop.regsPerMultiprocessor / 1024);
printf("Maximum number of threads per block: %d\n", prop.maxThreadsPerBlock);
printf("Maximum number of threads per SM: %d\n", prop.maxThreadsPerMultiProcessor);
相关推荐
㓗冽1 小时前
矩阵问题(二维数组)-基础题70th + 发牌(二维数组)-基础题71th + 数字金字塔(二维数组)-基础题72th
c++·算法·矩阵
系统修复专家1 小时前
UG12.0官方未公开修复方法:彻底解决C++异常崩溃问题
开发语言·c++·安全·bug·dll·游戏报错
HAPPY酷2 小时前
温和 C++:构建一个线程安全的异步消息服务器
服务器·c++·安全
量子炒饭大师2 小时前
【C++入门】Cyber尖层的虚实重构—— 【类与对象】类型转换
开发语言·c++·重构·类型转换·隐式转换·explicit·类与对象
AutumnorLiuu2 小时前
C++并发编程学习(四)——死锁及其预防
开发语言·c++·学习
元让_vincent2 小时前
DailyCoding C++ CMake | CMake 踩坑记:解决 ROS 项目中的“循环引用”与库链接依赖问题
c++·机器人·ros·动态库·静态库·cmake·循环引用
燃于AC之乐2 小时前
深入解剖STL set/multiset:接口使用与核心特性详解
开发语言·c++·stl·面试题·set·multiset
HAPPY酷2 小时前
C++ 高性能消息服务器实战:融合线程、异步与回调的三大核心设计
java·服务器·c++
HAPPY酷2 小时前
现代 C++ 并发服务器的核心模式
服务器·开发语言·c++