并行编程——Lesson 1:GPU 编程模型

前言

《并行编程》系列是学习《Intro to Parallel Programming》过程中所做的笔记记录以及个人一些所思所想。

GPU 与 CPU

衡量一个高性能处理器的时候,采用两个指标。

  • 执行时间(Latency):执行一项任务所花时间,采用时间单位。
  • 吞吐量(Throughput):单位时间完成的任务量。

而非常遗憾的是,这两项指标并不总是一致的,它们通常是矛盾的。比如说:
A地和B地相距4500 KM,从A到B可以有两种选择。一种方法是开跑车,车上乘坐2个人,以200 KM/H的速度开到B地;另一种方法是乘坐客车,车上乘坐着40个人,以50 KM/H的速度开到B地。

方案 Latency(hour) Throughput(people/hour)
开跑车 4500 / 200 = 22.5 2 / 22.5 = 0.0889
客车 4500 / 50 = 90 40 / 90 = 0.444

虽然这个例子并不是很合理,但是它展示了 Latency 和 Throughput 的计算方式。

传统的 CPU 设计就是尝试去最优化执行时间,使其在每一项任务上的处理时间都能够达到最优。而 GPU 的设计与 CPU 不同,它的目标最大化吞吐量。因为在计算机图形学中,我们更加关心每秒能处理的像素数量,而不是每个像素需要花多少时间处理,甚至只要每秒能处理的像素数量只要能增加,即便单个像素处理的时间需要增加两倍也是可以接受的。

GPU 设计原则

  • GPU 有许多简单的计算单元,它们组合在一块可以执行大量的计算,GPU 通常会牺牲更多的控制力来换取更强大的计算能力。
  • GPU 采用显式并行编程模型(课程一大重点),即编程的时候就是按多处理器的思路进行,而不是假设只有单个处理器,然后将程序交给编译器映射到多个处理上。
  • GPU 设计的优化目标是 Throughput 而不是 Latency ,它可以接受单个任务执行时间延长,只要每秒能处理的任务总数能增加,也因此 GPU 适用于以 Throughput 为最重要衡量指标的应用程序中。

CUDA 编程模型

异构型计算机拥有两种不同的处理器,它们是 CPU 和 GPU。如果只是简单地写一个 C 程序,那么它只使用到了 CPU,而如果想要使用 GPU 就要借助 CUDA。CUDA 编程模型允许我们通过一个程序同时对两个处理器进行编程,另外虽然 CUDA 支持多门编程语言,但是本课程中主要使用 C 语言。

CUDA 中普通 C 语言部分的程序会运行在 CPU (也称为"HOST")中,而另外一部分将在 GPU (相对于"HOST"被称为"DEVICE")中运行。然后 CUDA 编译器会将 CPU 部分的代码和 GPU 部分的代码分开编译,为每个处理器生成各自的编译结果。

CUDA 将 GPU 当做 CPU 的协处理器(co-processor)来对待,并且假设 HOST 和 DEVICE 各自拥有独立的内存用于存储数据,GPU 通常采用高性能的内存块来作为内存。当谈到 GPU 和 CPU 的关系时,CPU 则处于主导地位。CPU 负责运行主程序,并向 GPU 发送控操作指令。

操作内容包含有:

  1. 将数据从 CPU 内存中移动到 GPU 内存中。
  2. 将数据从 GPU 内存中移动到 CPU 内存中。
  3. 向 GPU 中的内存申请空间。
  4. 调用 GPU 中的程序,以并行的方式进行运算,这些程序也称为内核,所以 HOST 能够启动 DEIVCE 中的内核。

操作1和2涉及的命令是 cudaMemcpy ,操作3涉及的指令是 cudaMalloc

CUDA 程序流程

一个典型的 CUDA 程序流程是:

  1. CPU 为 GPU 申请存储内存空间(cudaMalloc)。
  2. CPU 将输入数据复制到 GPU 内存中(cudaMemcpy)。
  3. CPU 启动 GPU 内核处理数据(Kernel launch)。
  4. CPU 将结果从 GPU 内存中复制回来(cudaMemcpy)。

容易发现,步骤2与4属于数据传输的过程。在程序中我们通常都希望能尽量减少数据传输所消耗的时间,而使更多时间花在计算上。所以对于 I/O 密集型的程序,便不适用于 CUDA 或者 GPU 编程。事实上,成功的 GPU 程序在计算时间与传输通信时间的比率上通常具有较高的值。

GPU 的优点

GPU 擅长处理以下两个事项:

  1. 高效地启动大量的线程
  2. 并行地运行大量的线程

举个例子,比如说要对一个长64的数组进行求平方运算。

CPU 的做法

首先是只运行于 CPU 中的做法。

程序中对数组进行遍历,然后依次对每一个元素都执行相同的乘积操作。这些操作是在一个线程中串行执行的,所以该线程将会执行循环64次。

注:此处的线程指的是执行完整代码的一条独立路径。

GPU 的做法

理论知识

之前介绍过,CUDA 的代码需要分成两部分,一部分运行于 CPU,一部分运行于 GPU。GPU 部分所要实现的逻辑很简单,这里是使得输出等于输入的平方,但是这部分并没有说明并行运算的程度(或者是线程数量)。事实上,指明并行运算程度的任务将交给 CPU 进行。所以 CPU 需要为 GPU 分配内存空间,再将数据复制到 GPU 内存中,然后再启动 GPU 计算平方数的内核(此处声明了64个线程)。

同时创建64个线程用于执行平方运算的好处是,每个线程都有一个唯一的线程索引,所有就可以将数组的第 n 个元素分配给第 n 个线程进行处理。

代码实践

定义 GPU 内核代码。

__global__ void square(float *d_out, float *d_in){
  // 获取线程索引,将线程索引也作为数组的元素索引
  int idx = threadIdx.x;
  float f = d_in[idx];
  d_out[idx] = f * f;
}

然后通过内核启动语句配置并启动内核。

...
const int ARRAY_SIZE = 64;
...
square<<<1, ARRAY_SIZE>>>(d_out, d_in);
...

所以,这里启动了一个含有64个线程的块,每个线程各自负责计算数组中的一个元素。

配置启动内核的参数

启动内核的语句形式如:

kernel_function<<<blocks_number, thread_per_block>>>(d_out, d_in)

kernel_function 是自定义的内核函数名称。<<<....>>> 则是 CUDA 定义的特殊符号,其中接受两个参数,分别用于启动的块的数量以及每个块的线程数
例如,SQUARE<<<1, 64>>>(d_out, d_in) 语句启动了一个内核,并指定了内核具有一个块,每个块存在64个线程。
如果需要使用到更多的计算资源,那么便可以通过 <<<...>>> 的参数进行配置。其中关于内核的配置具有两个特点需要知道:

  1. 一个内核可以同时运行多个块。
  2. 每个块可以运行多个线程,不过线程具有上限,通常而言在较旧的 GPU 中这个上限为 512,在较新的 GPU 中上限是 1024。

所以当我们想要启动128个线程计算128个数的平方时,代码可以改为 SQUARE<<<1, 128>>>(...) 。那么如果想要想要启动1280个线程呢?这时便有多种策略。比如 SQUARE<<<5, 256>>> 或者 SQUARE<<<10, 128>>> ,但是需要注意不能写成 SQUARE<<<1, 1280>>> ,因为这样超出了最大线程限制。

但是当前的块和线程都是一维的,而如果我们需要处理二维或者是三维结构的数据,这样显然就不方便了,所以 CUDA 也支持二维和三维的块与线程布局方式。

借助 dim3(x, y, z) 函数可以创建指定维度的布局方式。默认情况下,每个维度的值都为1,所以 dim3(w, 1, 1) == dim3(w) == w

多维内核

启动内核的最一般形式是:

kernel<<<dim3(bx, by, dz), dim3(tx, ty, tz), shmem>>>(...)

dim3(bx, by, bz) 指定了块的维度。dim3(tx, ty, tz) 指定了每个块的线程维度。shmem 这个参数不太常用,默认为 0,它以字节为单位指定了每个线程块分配的共享内存量,关于该参数的具体使用方法,后续会介绍到。

上图展示了一个二维的布局,其中 thread 是最基本的单位,若干个 thread 组成了一个 block,而若干个 block 组合成一个 grid。CUDA 还提供了多个属性用于实现获取线程索引、块索引和块大小等。

  • threadIdx:获取块内的线程索引,最多具有三个维度 x, y, z
  • blockDim:获取块大小,最多具有三个维度 x, y, z
  • blockIdx:获取块索引,与线程索引一样,最多具有三个维度。
  • gridDim:获取 grid 大小,也是最多具有三个维度 x, y, z

注意事项:这边有一个在编程处理图像的时候经常容易犯错的坑。在处理图像数据的时候,通常会将 grid of blocks 定义为与图像大小一致,然后每个 block 中的 thread 数定义为 1。 这样对于一个分辨率为 n*m 的图像,则使用了 n*m 个块,每个块中只有一个线程负责处理当前像素值。但是需要注意的是,在定义 gridSize 的时候需要定义成 dim3(m, n) 。但是在编程时出于习惯,我们很可能会写成 dim3(n, m),然而这样是错误的 。这是因为 dim3() 接受的三个参数依次对应了 x, y, z 的取值范围,所以在使用图像的宽高参数指定 dim3 参数的时候应该是先制定宽度再指定高度。否则处理之后的图像很可能出现有一半是黑色的情况。

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

推荐阅读更多精彩内容