DAY25: 阅读硬件的多线程

Posted 吉浦迅科技

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DAY25: 阅读硬件的多线程相关的知识,希望对你有一定的参考价值。

 


我们正带领大家开始阅读英文的《CUDA C Programming Guide》,今天是第25天,我们讲解的是硬件架构,希望在接下来的75天里,您可以学习到原汁原味的CUDA,同时能养成英文阅读的习惯。


本文共计304字,阅读时间5分钟

前情回顾:





注意:最近涉及到的基础概念很多,所以我们备注的内容也非常详细,希望各位学员认真阅读

4.2. Hardware Multithreading

The execution context (program counters, registers, etc.) for each warp processed by a multiprocessor is maintained on-chip during the entire lifetime of the warp. Therefore, switching from one execution context to another has no cost, and at every instruction issue time, a warp scheduler selects a warp that has threads ready to execute its next instruction (the active threads of the warp) and issues the instruction to those threads.

In particular, each multiprocessor has a set of 32-bit registers that are partitioned among the warps, and a parallel data cache or shared memory that is partitioned among the thread blocks.

The number of blocks and warps that can reside and be processed together on the multiprocessor for a given kernel depends on the amount of registers and shared memory used by the kernel and the amount of registers and shared memory available on the multiprocessor. There are also a maximum number of resident blocks and a maximum number of resident warps per multiprocessor. These limits as well the amount of registers and shared memory available on the multiprocessor are a function of the compute capability of the device and are given in Appendix Compute Capabilities. If there are not enough registers or shared memory available per multiprocessor to process at least one block, the kernel will fail to launch.

The total number of warps in a block is as follows:

ceil ( T W s i z e , 1 )

· T is the number of threads per block,

· Wsize is the warp size, which is equal to 32,

· ceil(x, y) is equal to x rounded up to the nearest multiple of y.

The total number of registers and total amount of shared memory allocated for a block are documented in the CUDA Occupancy Calculator provided in the CUDA Toolkit.


本文备注/经验分享:

 首先你要明白,现在的N卡都最多一个SM有2048个线程的(从3.0+就这样了)。这些线程的执行环境(上下文)同时存在于1个SM上。而GPU整体,等于是很多这样的状况的SM的集合。(暂不考虑L2/显存之类的全局共享资源)这些数量非常多的线程,叫驻留线程。
所谓驻留,顾名思义,驻扎在SM上了。这些线程,它们的用到的寄存器,用到的shared memory,之类的资源,至少它们还驻留在SM上一分钟,就持续被占用着。
(一个SM往往有256KB的寄存器,64K/96K或者更多的shared memory之类的)(注意这里的说法不是很精确,不过可以这样看,例如shared memory精确的说法是被block---一些线程的集体所有的)


驻留线程是一个很重要的指标,往往和occupancy(以及, achieved occupancy)正相关。


一般情况下,如果你只有1024个线程/SM,那么occupancy往往会降低到50%(相比最大驻留状态为100%),甚至更低。为何会更低而不是正好50%,以后再说。

因为每时每刻,SM总是从它身上的这些驻留的线程中(以warp为单位)选出当前可以被发射指令的,进行发射指令,执行运算,所以当SM身上的驻留的线程太少(occupancy太低)的时候,往往会影响SM的性能发挥,而GPU是由很多SM构成的,当这些SM都影响了性能发挥的时候,整个GPU也容易性能降低。所以这是为何我们常说,一般情况下,occupancy越高越好,越高越容易发挥理论峰值性能。(请注意,这个说法不一定100%正确。稍后我给你举出一个反例)

而有了occupancy越高越好的认识,我们往往需要想方设法的去提高occupancy.
而想提高occupancy, 就需要知道都有什么原因会导致occupancy, 干掉这些原因,就可以提高了。


这个章节也列出了一些原因,其实就一句话,执行所需要的资源。SM上都有什么资源?显然我们知道的是有INT/Logical, FP32, FP64, FP16,Tensor Core,等等,这些固定的ALU。(ALU你可以理解成执行单元,虽然不太恰当)还有负责特殊功能函数的单元。

这几个单元均不会影响occupancy, 因为无论有1个warp在SM上,还是SM满载了64个Warp(2048个线程),这些计算用的单元都是分时服务所有的线程/warps的。所以occupancy无需考虑它们。而影响的,只有那些不能时刻被分时复用的资源,主要是:
寄存器---在线程/warp存活期间,静态的分配给了线程/warp;shared memory---在线程的特定集体(block)存在的期间,静态的分配给了block。这两个资源均不能这个周期给某线程,下个周期给另外一个线程,因此它们决定了occupancy。一般来说,因为寄存器往往只有256KB的32-bit寄存器,也就是64K个(一个4B么)。
如果一个线程/warp用的寄存器太多,例如说:一个线程用了256(255)个寄存器,一共64K的寄存器,显然只能最多满足256个线程的要求(64K/256(255))

此时occupancy不会超过实际256/2048 = 1/8 = 12.5%的occupancy,而如果一个线程只是用128个寄存器,还是64KB的寄存器在SM上,那么却能上512个线程了(64K/128),此时occupancy立刻变成了512/2048 = 25%了。甚至再缩小一下线程们用的资源,例如改成一个线程用64个寄存器,那么occupancy立刻变成了50%,如果用32个寄存器。哈哈,这次立刻可以100%的occupancy(理论)。所以例子来说,用的寄存器越少每线程/warp,那么occupancy就容易越高。为何我打的是线程/warp?

因为实际上线程不能独立存在,根据之前的章节(就在前几天),你知道除了volta的部分情况,所有的计算能力下, 线程们总是被打包32个在一起,当作一个warp来整体处理的。所以实际上,寄存器这种资源也是以warp来算的。

类似的, 因为shared memory总是给一大帮紧密合作在一起的线程/warps(block)来整体用的。所以shared memory这种资源会在block的级别上进行分配。
注意手册没有提到的一点是,block中的所有线程,必须同时分布在1个SM上,不能拆分的。(例如我就启动了一个block,里面有1024个线程,例如<<<1,1024>>>,这1024个线程必须只能在同一个SM上运行,而不能像很多论坛的读者想象的那样,例如我在使用1080,有20个SM,不会每个SM都并行的运行50来个线程的。
所以论坛很多人的写法,用<<<1,1024>>>这种,看上去启动了上千个线程,但因为它们只能在同一个SM上,导致性能很不好的),但是为啥要求1个block必须在同一个SM上,还是因为资源。刚才说了shared memory必须是block整体所有的,如果1个block能在2个SM上运行,而该block要求了,例如8000字节的shared memory,请问这8000字节是哪个SM出?每个人出一半?SM 0出80%,SM 1比较偷懒出20%?这些都不会。只能是1个SM全部出完。所以这样,1个block不能在2个SM上运行。类似的,block往往有内部的整体同步,也叫局部同步(不是全局同步哈),
这种局部同步,需要block中的线程能执行到一个特定的点,(精确的说,这种说法不太对---但这里不解释。后面会遇到的),而这种整体能运行到一个固定的点,需要一种叫barrier的东西,进行一种叫barrier synchronization的操作。而barrier大家必须都对着相同的barrier才能在它下面进行同步,而barrier也是属于SM的资源,因此也不能拆分,所以这样1个block就无论如何也不能拆分了)

这样会导致一个问题,本章节也说了,如果有任何1个SM不能满足至少能启动1个block的情况,那么kernel会整体启动失败。例如1080有20个SM,1280K个寄存器(64K个/SM × 20 SM),如果我有一个要求使用128K个寄存器的block(例如每个线程要求128个寄存器,block里有1024个线程,一共128K个寄存器),虽然说1080整体有1280K个,是启动1个block的要求的10倍那么多的资源,但是它实际上无法启动任何一个该kernel中的block的,因为block不能被拆分到2个SM上,必须有1个SM就能启动它。此时,此kernel会直接启动失败。 If there are not enough registers or shared memory available per multiprocessor to process at least one block, the kernel will fail to launch.这就是这句说的。这里的shared memory的情况和寄存器一样。

此外,除了block不能拆分,N卡还有一个特点,那就是warp不能合并。我们知道32个线程是一个warp,那么48个线程是多少个呢?1.5个?2个?1个?答案是2个warp,不满1个warp,那么只有1个线程多出来(33线程例如),也会形成2个warp的,虽然33只比32多了1个线程。然后论坛上曾经有人问,我有2个blocks,每个里面48个线程,这样一共是96个线程对不对。我说对。然后他说,这样一共有3个warps,对吗?这种是不对的。因为warp不能跨block组合起来。这样依然会形成,这2个blocks具有4个warps,只是这4个warps里面有2个warps是不满的(只有一半的active threads)。

所以本章节给出了一个奇葩的公式,ceil的那个。这公式看起来很奇怪。其实上意思是说,如果我不是32线程的整数倍,有任何余数,均也会构成一个额外的warp的。这公式的另外一种等效的写法是:
int warp_count_in_your_block = (线程数量 + 31) / 32
这种等效的写法是群众喜闻乐见的。通过整数除法,用(a + N - 1) / N的方式,来完成a/N的向上取整。这个和刚才那个是等价的,但好理解的多。此外,安装CUDA的时候,如果你也安装了office,则可以打开一个在CUDA安装目录里面的一个.xlsx文件,这个文件是一个电子表格,里面有个计算器,输入你的线程,block之类的资源需求,和你的目标卡(例如计算能力3.7的K80---这卡比较奇特),这个电子表格会自动告诉你occupancy的情况,受限于那些资源之类的。
(其实这个文件还有很多其他信息,点击这文件下面的其他tab,有一些手册不告诉你的,例如特定计算能力下,各种资源的分配单位(例如是线程?warp?还是block?还是其他?以及,特定的计算能力的资源情况如何?为何某些嵌入式的板子限制特别多?T*1?T*2?),可以直接从这个文件都看到。这是这个章节的最后一部分。
我需要额外的说一下,如果没有安装office也没有关系,可以直接上profiler(包括NVVP,或者nsight自带的那个小profiler)里面也会告诉你,当前某kernel受限于什么,导致occupancy上不去之类的,而且还会画出图表。很是形象。

但用profiler的坏处是你必须手头拥有计算能力为M.N的卡,才能知道该卡上的实际情况。而用那个电子表格(例如你需要针对计算能力5.3的设备,可以直接看)

此外occupancy并非越高越好。也有个限制的。光说了这些资源限制,如何程序员们手工控制资源的使用呢?这些这手册的后面都有的。到了我们再说。

此外,本章节还提到了一句:a parallel data cache or shared memory that is partitioned among the thread blocks。这句我对原文的含义保留任何看法。并且不给出任何评价或者说明。(因为目前没有任何资料或者实验来表明,和这个长句中的前后的寄存器或者shared memory那样,L1 Data Cache(或者它在不同计算能力上的等效变种)是同样需要这样,如同寄存器或者shared memory一样,切分给block的。这里和另外两个公用的partitioned一词,含有静态切分的意思。但实际情况可能并非如此。
In particular, each multiprocessor has a set of 32-bit registers that are partitioned among the warps, and a parallel data cache or shared memory that is partitioned among the thread blocks.这句的全句。所以我这里只提到了寄存器和shared memory(以及后面提到了barrier),但是却没有提到data cache的原因。

本章节还是很好理解的。

如果不能理解,做几个项目,跑跑profiler之类的,会有更深刻的印象。


有不明白的地方,请在本文后留言

或者在我们的技术论坛bbs.gpuworld.cn上发帖



以上是关于DAY25: 阅读硬件的多线程的主要内容,如果未能解决你的问题,请参考以下文章

day9-Python学习笔记(二十二)多线程,多进程

python 使用TensorFlow的多线程图像阅读器示例

Web Worker——js的多线程,实现统计博客园总阅读量

day9-01 线程

带有 libevent 的多线程 HTTP 服务器

Python中的多线程并行运行