CUDA编程入门(一)

1.认识CUDA编程

2006年,NVIDIA公司发布了CUDA(Compute Unified Device Architecture, 统一计算设备架构),是建立在NVIDIA的CPUs上的一个通用并行计算平台和编程模型。 基于CUDA编程可以利用GPUs的并行计算引擎来更加高效地解决比较复杂的计算难题,广泛应用于深度学习领域,基于GPU的并行计算已经成为训练深度学习模型的标配。

GPU并不是一个独立运行的计算平台,而需要与CPU协同工作,可以看成是CPU的协处理器,因此当我们在说GPU并行计算时,其实是指的基于CPU+GPU的异构计算架构。在异构计算架构中,GPU与CPU通过PCIe总线连接在一起来协同工作,CPU所在的位置称为主机端(host),而GPU所在的位置称为设备端(device),如下图所示:

可以看到,GPU包含更多的运算核心,其特别适合数据并行的计算密集型任务,如大型矩阵运算,而CPU的运算核心较少,但是其可以实现复杂的逻辑运算,因此其适合控制密集型任务。另外,CPU的线程是重量级的,上下切换开销大,但是GPU由于存在很多核心,其线程是轻量级的。因此,基于CPU+GPU的异构计算平台可以优势互补,CPU负责处理逻辑复杂的串行程序,GPU重点处理数据密集型的并行计算程序,从而发挥最大功效。

CUDA是NVIDIA公司开发的GPU编程模型,它提供了GPU编程的简易接口,基于CUDA编程可以构建基于GPU计算的应用程序。CUDA提供了对其它编程语言的支持,如C/C++,Python,Fortran等语言。

2.CUDA编程基础

2.1 host和device

CUDA异构计算架构中,CPU和GPU是协同工作的。hostdevice是两个重要的概念,我们用host指代CPU及其内存,而用device指代GPU及其内存。host程序在CPU上运行,device程序在GPU上运行,host和device之间可以相互通讯(进行数据拷贝)。

CUDA编程中,通过3个函数类型限定词(__global____device____host__)来区分host函数和device函数。具体如下:

  • __global__:在device上执行,从host中调用(一些特定的GPU也可以从device上调用),返回类型必须是void,不支持可变参数,不能成为类成员函数。注意:下文会提到CUDA中的核函数,它就是用__global__声明的,并且是异步的,host不会等待kernel执行完就执行下一步。
  • __device__:在device上执行,从device中调用。
  • __host__:在host上执行,从host中调用,一般省略不写(默认)。

典型的CUDA程序执行流程如下:
1.分配host内存,并进行数据初始化;
2.分配device内存,并从host将数据拷贝到device上;
3.调用CUDA的核函数在device上完成指定的运算;
4.将device上的运算结果拷贝到host上;
5.释放device和host上分配的内存。

2.2 kernel

在CUDA的执行流程中,最重要的一个过程是调用CUDA的核函数来执行并行计算。kernel是CUDA中一个十分重要的概念,kernel是在device上实现并行计算的函数,核函数用__global__符号声明,调用时用<<<grid, block>>>来指定kernel要执行的线程数量。在CUDA中,每个线程都要执行核函数,并且会为每个线程分配一个唯一的线程号thread ID,这个ID值可以通过核函数的内置变量threadIdx来获得。

让我们进一步理解kernel的线程层次结构。GPU上有很多并行化的轻量级线程,kernel在device上执行时,实际上是启动很多线程,一个kernel所启动的所有线程称为一个网格(grid),同一个网格上的线程共享相同的全局内存空间,grid是线程的第一个层次结构,而网格又可以分为很多线程块(block),一个线程块中包含多个线程,是线程的第二个层次结构。线程的两层层次结构,如下图所示(这是一个grid和block均为2-dim的线程结构示例):

其中,grid和block都定义为dim3类型的变量,dim3是包含3个无符号整数 (x, y, z) 成员的结构体,在定义时,缺省值为1。grid和block可以灵活的定义为1-dim、2-dim以及3-dim结构,kernel在调用时通过执行配置<<<grid, block>>>来指定kernel所使用的线程数及结构。以上图为例,定义grid和block、核函数调用的代码如下:

dim3 grid(3, 2);
dim3 block(5, 3);
kernel_fun<<<grid, block>>>(params...);

对于每个线程,通过两个内置变量 (blockIdx, threadIdx) 来唯一标识,它们都是uint3类型。其中,blockIdx指明线程所在grid中的位置,而threadIdx指明线程所在block中的位置。以上图中的Thread(3, 1)为例,坐标满足:

blockIdx.x = 1
blockIdx.y = 1
threadIdx.x = 3
threadIdx.y = 1

值得注意的是,一个线程块上的线程是放在同一个SM(Streaming Multiprocessor, 流式多处理器)上的,但是单个SM的资源有限,这导致线程块中的线程数是有限制的,现代GPUs的线程块中的线程数上限为1024个。

通过内置变量gridDim、blockIdx、blockDim、threadIdx,可以计算每一个线程的唯一标识ID——threadId。其中,gridDim用于获取grid各个维度的大小,blockDim用于获取block各个维度的大小。对于任意一个线程,它的blockId、threadId的计算公式:
blockId = blockIdx.z * gridDim.x * gridDim.y + blockIdx.y * gridDim.x + blockIdx.x
threadId = blockId * blockDim.x * blockDim.y * blockDim.z \\ \qquad \;\;\,+ (threadIdx.z * blockDim.x * blockDim.y + threadIdx.y * blockDim.x + threadIdx.x)
上面的计算公式,适用于任意维度(1维、2维、3维),以grid为2维、block为2维为例,此时gridDim.z=1、blockDim.z=1,blockIdx.z=0、threadIdx.z=0,带入化简得:
blockId = blockIdx.y * gridDim.x + blockIdx.x
threadId = blockId * blockDim.x * blockDim.y + (threadIdx.y * blockDim.x + threadIdx.x)

kernel的这种线程层次结构,天然适合vector、matrix等运算,以2维grid+2维block核结构为例,实现一个二维矩阵(N * N)的加法运算,每个线程负责处理每个位置的两个元素相加,代码见下。

// kernel函数定义
__global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    int j = blockIdx.y * blockDim.y + threadIdx.y;
    if (i < N && j < N)
        C[i][j] = A[i][j] + B[i][j];
}

int main() {
    ...
    // kernel的线程配置
    // 在这里,总线程的数量与矩阵大小一致,为N * N
    dim3 blockSize(16, 16);  // block的大小
    dim3 gridSize(N / blockSize.x, N / blockSize.y);  // grid的大小
    // kernel函数调用-->多线程并行执行矩阵加法操作
    MatAdd<<<gridSize, blockSize>>>(A, B, C);
    ...
}
2.3 CUDA的内存模型(Memory Model)

CUDA的内存模型分为6类:

  • 1.Global Memory,全局内存:速度普通,读写。
  • 2.Local Memory,本地内存(其实是全局内存):速度普通,读写。
  • 3.Shared Memory,共享内存:速度快,读写。
  • 4.Register,寄存器():速度“最快”,读写。
  • 5.Constant Memory,常量内存:速度快,只读。
  • 6.Texture Memory,纹理内存:速度快,只读。
2.4 线程数(Warp)

3.实例演示

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

推荐阅读更多精彩内容