CUDA核函数与线程配置

CUDA核函数

在GPU上调用的函数成为CUDA核函数(Kernel function),核函数会被GPU上的多个线程执行。每个线程都会执行核函数里的代码,当然由于线程编号的不同,执行的代码路径可能会有所不同。下面的几行代码是向量加计算的CUDA核函数:

__global__ void vectoradd (int *a, int *b, int *c, int n){
    int i = blockDim.x *  blockIdx.x + threadIdx.x;
    if (i < n){
        c[i] = a[i] + b[i];
    }
}
vectoradd<<<grid, block>>>(d_a, d_b, d_c, N);

从这段代码可以看出CUDA核函数的几个特征:
-函数的最前面是声明标识符__global__,该标识符表示这个函数可以在GPU上执行。需要指出的是尽管是在GPU上执行,但是仍然是由CPU端发起调用的
-核函数调用时需要用<<<...>>>符号来指定线程配置
-在核函数内部可以调用CUDA内置变量,比如threadIdx,blockDim等
-核函数相对于CPU代码是异步的,也就是控制会在核函数执行完成之前就返回,这样CPU就可以不用等待核函数的完成而继续执行后面的CPU代码
关于线程的配置以及内置变量将会在后面详细介绍。CUDA核函数除了上面提到的几个特征之外,还有一些限制:
-核函数内部只能访问device内存。因为核函数是执行在设备端,所以只能访问设备端内存。
-必须返回void类型。我们知道核函数是由CPU端发起的并执行在GPU上的函数。在核函数内部的数据均是位于GPU上的,假设核函数有返回值,那么返回值是位于GPU上的数据,CPU去直接接收这个数据是不被允许的。所以,核函数没有返回值。
-核函数不支持可变参数
-核函数不支持静态变量
-核函数不支持函数指针
在CUDA编程中,除了__global__外,常用的标识符还有:
__device__
-有标识符__device__的函数只能在GPU段执行
-只能在GPU段调用,比如可以在__global__以及__device__函数中调用
-__global__与__device__不能同时使用
另外一个常用的标识符是__host__
-只能在host端执行
-只能在host端调用
单独使用__host__的情况时,该函数与普通的CPU函数的性质及使用方法没有任何差别。那既然这样为什么还要引入这个标识符呢?我们可以想象有这样一种情况,一个函数我们希望它既可以在CPU上调用也可以在GPU上调用,那么我们这样声明这个函数:__host__ __device__ funForCPUandGPU(args), 则这个函数既可以在CPU上执行也可以在GPU上执行。

线程配置

前面提到,在调用核函数时需要通过<<<...>>>指定线程配置,在具体介绍之前,我们先来了解CUDA编程中几个基本的概念。
线程(Thread)是CUDA程序的基本执行单元,每个线程内的执行都会顺序执行。所有的线程都会执行相同的代码,当然有可能会执行相同代码的不同分支。所有的线程之间是并行执行的,没有先后之分。
线程块(Thread Block)是由一组线程组成。每个线程块内部的线程之间可以进行协作,有可以共同访问的内存-共享内存。每个线程块会在GPU上的某一个流处理器(Streaming Multiprocessor, SM)中执行。
线程网格(Thread Grid)是一组线程块的集合。线程网格里的线程块会被调度到GPU的多个SM上去执行。线程块之间并没有同步机制,线程块被执行的先后顺序是不确定的。线程块之间的通讯比较昂贵,需要通过全局内存(global memory)来实现。

CUDA线程层次。每个核函数对应一个线程网格,每一个线程网格包含多个线程块,每个线程块包含多个线程。线程网格以及线程块可以是1维、2维或者3维的。

在调用核函数时需要指定的线程配置就是需要给定每个线程网格中有多少线程块,每个线程块有多少线程,并且他们的排列方式是怎样的。一个线程配置的例子如下:

dim3 grid(3,2,1), block(5,3,1)
kernel_name<<<grid, block>>>(…)

线程网格以及线程块的数据类型是dim3,实质上是一个结构体,有三个变量分别用来描述x、y、z三个方向的长度。<<<...>>>中的第一个参数用来指定线程网格的结构,也就是每个线程网格中有多少线程块,上面的例子中每个线程网格中有321=6个线程块,排布方式是三个方向上分别是3、2、1。第二个参数是用来指定线程块的结构,也就是每个线程块中有多少个线程,上面的例子中每个线程块中有531=15个线程,排布方式是三个方向上分别是5、3、1。<<<...>>>也可以接受整型变量,比如<<<6, 32>>>代表一个线程网格中有6个线程块,一维排布,一个线程网格块内有32个线程,同样一维排布。这样整个核函数内的总的线程数就是6*32=192。
另外核函数内部可以使用CUDA的内置变量来获取线程号以及线程块号:
threadIdx.[x y z]指的是线程块内线程的编号
blockIdx.[x y z]指的是线程网格内线程块的编号
blockDim.[x y z]指的是线程块的维度,也就是线程块中每个方向上线程的数目
gridDim.[x y z]指的是线程网格的维度,也就是线程网格中每个方向上线程块的数目
下面我们来看一个简单的例子,线程网格有4个线程块,每个线程块内有8个线程,并且都是一维排布:

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

具体的线程配置以及相应的内置变量的值如下图所示


内置变量均从0开始编号

从上图可以看出,我们可以很轻易的获取一个线程在线程块的位置。在核函数中,我们经常需要得知一个线程在一个线程网格中的位置,那么该怎么计算呢?同样来看一个简单的例子:


dim3 grid(4,1,1), block(4,1,1)

上面的例子中有4个线程块,每个线程块中4个线程,假设我们需要计算红色标记的线程在线程网格中的位置。观察上图,我们可以分成两个部分进程计算,首先计算该线程所在线程块前面总共有多少线程,然后在加上该线程在当前线程块的位置就可以获取在整个线程网格中的位置。该线程所在的线程块编号是blockIdx.x,每个线程块内的线程数是blockDim.x,那总的线程数是blockIdx.x * blockDim.x. 再加上该线程在当前线程块中的位置threadIdx.x,则有:
int idx = blockIdx.x * blockDim.x + threadIdx.x;

二维与三维的情况会稍微复杂些,但计算方法是一样的。下面是一段打印二维线程编号的核函数的例子,自己可以尝试编译运行,相信会有助于对线程位置的计算加深理解。

#include <stdio.h>                                                                                                                                                                
#include <cuda.h>

__global__ void printThreadIndex() {
    int ix = threadIdx.x + blockIdx.x * blockDim.x;
    int iy = threadIdx.y + blockIdx.y * blockDim.y;
    unsigned int idx = iy*blockDim.x * gridDim.x + ix; 
    printf("thread_id (%d,%d) block_id (%d,%d) coordinate (%d, %d), global index %2d \n", 
            threadIdx.x, threadIdx.y, blockIdx.x, blockIdx.y, ix, iy, idx);
}

int main(void) {
    dim3 grid(2, 3, 1), block(4, 8, 1); 
    printThreadIndex<<<grid, block>>>();
    cudaResetDevice();
    return 0;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容

  • 开篇一张图,后面听我编 1. 知识准备 1.1 中央处理器(CPU) 中央处理器(CPU,Central Proc...
    He_Yu阅读 47,113评论 7 115
  • CUDA从入门到精通(零):写在前面 本文原版链接: 在老板的要求下,本博主从2012年上高性能计算课程开始接触C...
    Pitfalls阅读 3,613评论 1 3
  • 1. CPU vs. GPU 1.1 四种计算机模型 GPU设计的初衷就是为了减轻CPU计算的负载,将一部分图形计...
    王侦阅读 20,891评论 3 20
  • CUDA是一种新的操作GPU计算的硬件和软件架构,它将GPU视作一个数据并行计算设备,而且无需把这些计算映射到图形...
    ai领域阅读 9,080评论 0 8
  • 本篇文章作为学习CUDA官方文档的学习笔记。CUDA C Programming Guide 1. Program...
    凉凉zz阅读 3,140评论 0 1