GPU 软硬件对应关系

GPU运行机制总述
市面上有很多GPU厂家,他们产品的架构各不相同,但是核心往往差不多,整明白了一个基本上就可以触类旁通了。

1.0 GPU计算流程(CPU协同GPU计算)
一个典型的计算流程是这样的:

数据从CPU的内存拷贝到GPU的内存
CPU把计算指令传送给GPU
GPU把计算任务分配到各个CUDA core并行处理
计算结果写到GPU内存里, 再拷贝到CPU内存里.
1.1 Host与Device
一个CUDA程序的可以分为两个部分(两者拥有各自的存储器):

在CPU上运行的称为Host程序
在GPU上运行的称为Device程序
1.2 Kernel
GPU上运行的函数又被叫做Kernel函数。

Host程序在调用Device程序时,可以通过参数确定执行该Kernel的CUDA threads的数量。

每个Thread在执行Kernel函数时,会被分配一个thread ID,Kernel函数可以通过内置变量threadIdx访问。

1.3 图解说明从CPU到GPU的运行机制


CUDA在执行的时候是让Host程序里面的一个一个的Kernel函数按照Grid(线程网格)的概念在GPU上执行。

一个Kernel函数对应一个Grid。

每个Grid中的任务是一定的。当要执行这些任务的时候,每一个Grid又把任务分成一部分一部分的Block(线程块),Block中间有若干Thread(线程),再分成线程来完成。

1.4 Single Instruction Multiple Threads(SIMT)
GPU中的SIMT体系结构相对于CPU的SIMD(单指令多数据,Single Instruction Multiple Data)。中文翻译:单指令多线程。SIMT对于可编程性的好处使得NVIDIA的GPU架构师为这种架构命名,而不是将其描述为 SIMD 。

为了有效地管理和执行多个单线程,流多处理器(SM)采用了SIMT架构。此架构在第一个unified computing GPU中由NVIDIA公司生产的GPU引入。

GPU使用SIMT执行 32 个并行线程的 Warp ,实现单指令、多线程,这使得每个线程能够访问自己的寄存器,从不同的地址加载和存储,并遵循不同的控制流路径。CUDA编译器和GPU一起工作,以确保Warp的线程组尽可能频繁地被分配到SM中,一起执行相同的指令序列,从而最大限度地提高性能。

每个线程可以包含控制流指令(控制流指令为标量指令)

同组Warp中的这些线程可以执行不同的控制流路径

当一个Warp中的线程分支到不同的执行路径时,产生分支发散(Branch divergence)

优势

共享控制逻辑可以有更多的空间面基去分配给计算单元
大量的并行操作,不需要进行复杂的控制编程
SIMD VS SIMT

CPU中通过SIMD来处理矢量数据;纯粹使用SIMD不能并行的执行有条件跳转的函数,很显然条件跳转会根据输入数据不同在不同的线程中有不同表现。

GPU则使用SIMT,无需开发者费力把数据凑成合适的矢量长度,并且SIMT允许每个线程有不同的分支,利用SIMT 才能做到不同分支的并行操作。

1 GPU的软件抽象
软件资源的抽象即为GPU的线程模型,可以分为Grid、Block、Thread和Warp。

Grid、Block、Thread是一种软件组织结构,是线程组织的三个层次,并不是硬件的,因此理论上我们可以以任意的维度(一维、二维、三维)去排列Grid,Block,Thread;在硬件上就是一个个的SM或者SP,并没有维度这一说,只是软件上抽象成了具有维度的概念。

thread,block,gird在不同维度的大小根据算力不同是有限制的:所以在不同CUDA版本或在编译时没有指定架构的情况下,可能CUDA版本也会对thread,block,grid在不同维度的大小产生影响。

1.1 Grid(线程网格)
一个Kernel函数对应一个Grid。

一个Grid中会分成若干个Block。同一Grid下的不同Block可能会被分发到不同的SM上执行。

Grid跑在GPU上的时候,可能是独占一个GPU,也可能是多个kernel函数并发占用一个GPU(后面这种实现需要fermi及更新的GPU架构支持)。

1.2 Block
数个threads会被群组成一个block,同一个block中的threads可以同步,也可以通过shared memory通信

1.3 Thread
一个CUDA的并行程序会被以许多个Thread来执行

每个Thread中的局域变量被映射到SM的寄存器上,而Thread的执行则由CUDA核心也就是SP来完成。

1.4 Warp
Warp是GPU执行程序时的调度单位,同一个Warp里的线程执行相同的指令,即SIMT。

一个SM的CUDA core会分成几个Warp(即CUDA core在SM中分组),由Warp scheduler负责调度。尽管Warp中的线程从同一程序地址,但可能具有不同的行为,比如分支结构。因为GPU规定同一Warp中所有线程在同一周期执行相同的指令,Warp发散分支过多会导致有效分支减少性能下降。

一个SM同时并发的Warp是有限的,因为资源限制,SM要为每个线程块分配共享内存,也要为每个线程束中的线程分配独立的寄存器,所以SM的配置会影响其所支持的线程块和Warp并发数量。

一个Warp中的线程必然在同一个block中,如果block所含线程数目不是Warp大小的整数倍,那么多出的那些thread所在的Warp中,会剩余一些inactive的thread,也就是说,即使凑不够Warp整数倍的thread,硬件也会为Warp凑足,只不过那些thread是inactive状态,需要注意的是,即使这部分thread是inactive的,也会消耗SM资源。由于warp的大小一般为32,所以block所含的thread的大小一般要设置为32的倍数。

例:如果一个块中有128个线程,那么线程0-31将在一个Warp中,32-63将在下一个Warp中

Warp非常重要,原因如下:

Warp中的线程是被绑定在一起的。如果Warp中的一个线程沿着if-else块的if侧走,而其他线沿着else侧走,那么实际上所有32条线程都会沿着两侧走。在执行功能上是没有问题的,那些不应该被执行分支的线程会被禁用,因此始终获得正确的结果,但是如果双方都很长,那么性能损失就很重要。
Warp内的线程(实际上是半纠缠的(self-warp))一起从内存中获取数据,是一起访问共享内存中的同一段数据同一段的。也就是说如果可以确保Warp中的所有线程都从同一段内获取数据,就只需要实现一次内存转换。
如果它们都从随机地址获取数据,那么就需要排队去实现32次内存转换。
2 软件抽象和硬件结构的一一对应关系
硬件结构可以参考之前的一篇文章

2.1 Block对应于SM
SM上可以同时存在多个Block被执行,这些Block不一定来自同一个kernel函数。

SM设备有Device Limit,Warp和Block的数量不能超过对应的上限。

除了受到设备定义的限制之外,还受到硬件资源的限制:

SP的寄存器数量
线程块消耗的共享内存量
每个线程会占用一定数量的寄存器和Shared Memory,因此SM上同时存活的Block数目不应当超过这些硬件资源的限制。由于SM上可以同时有来自不同kernel的Block存在,因此有时候即便SM上剩余资源不足以再容纳一个kernel A的Block,但却仍可能容纳下一个kernel B的Block。

一个线程块的thread只能在一个SM上调度

2.2 Block与Thread之间的联系Warp 对应于 SM与SP之间的联系
软件抽象里,认为任务分配到Block之后,所有的线程是并行执行的,这只是个逻辑上无懈可击的抽象,事实上我们不可能对一个任意大小的Block都给出一个同等大小的CUDA核心阵列去推动它的并行计算,来真正并行的执行它们。因而有了Warp这个概念。物理上,Block被划分成一块块的warp分别映射到CUDA核心阵列上执行,每一个warp就都可以理解为是一个线程的集装箱,为的是线程数量固定统一可以给他分配统一的硬件资源,每个集装箱只装一种货物,也就是下面同步执行的意思。
目前,CUDA中的Warp都是从threadIdx = 0开始,以threadIdx连续的32个线程为一组划分得到,即便最后剩下的线程不足32个,也将其作为一个Warp。CUDA kernel的配置中,我们经常把Block的size设置为32的整数倍,正是为了让它能够精确划分为整数个Warp(更深刻的原因和存储器访问性能有关,但这种情况下仍然和Warp的size脱不了干系)。
Warp是SM调度和执行的基础概念。Block被划分成32个线程组成的Warp。这样,大量的Warp生存在SM上,等待被调度到CUDA核心阵列去执行。
Warp中的活动线程由Warp Scheduler驱动。每一块SM中有单独的一个或者多个Warp Scheduler(举例:GM204中32个CUDA核心共享一个Warp Scheduler),以及多个CUDA核心。
当一个Warp执行中出现等待(存储器读写延迟等)后,Warp Scheduler就迅速切换到下一个可执行的Warp,对其发送指令直到这个Warp又一次出现等待,周而复始。这就是常说“用多线程掩盖延迟”。SM会从驻留在SM上的所有Warp中进行指令调度。(这里的驻留表示已经可以被执行的Warp,会从这里挑选,这时候挑选出来的Warp能来自于驻留在SM上的任何线程块)。
通常一个SM中的SP会分成几个Warp(也就是SP在SM中是进行分组的,物理上进行的分组)。
同步执行:Warp中的32个SP是一起工作的,执行相同的指令,如果没有这么多thread需要工作,那么这个Warp中的一些SP是不工作的,处于闲置状态。
2.3 Thread对应于SP
Thread在SP也就是CUDA Cores上执行
Thread会被分配Register/Local Memory,数据存在这里
SM上的CUDA核心是有限的,它们代表了能够在物理上真正并行的线程数(也就是优化到最佳情况下所能最大达到同一时刻在运行的并行数量)
每一个线程都有自己的寄存器内存和local memory,一个warp中的线程是同时执行的,也就是当进行并行计算时,线程数尽量为32的倍数,如果线程数不上32的倍数的话;假如是1,则warp会生成一个掩码,当一个指令控制器对一个warp单位的线程发送指令时,32个线程中只有一个线程在真正执行,其他31个 进程会进入静默状态。
3 软件抽象和硬件结构对应关系的例子
把GPU跟一个学校对应起来,学校里有教学楼、操场、食堂,还有老师和学生们;很快有领导(CPU)来检查卫生(需要执行的任务Host程序),因此这个学校的学生们要完成打扫除的工作(Device程序)。

软件抽象资源包括Thread、Warp、Block和Grid

硬件资源包括SP和SM

3.1 软件抽象
Grid对应的是年级

是抽象的划分组织方式

根据年级划分任务,Grid可以分为多个不同的班级

Block对应的是班级

是抽象的划分组织方式

每个班级有若干的同学(线程),可能一个两个不同的年级会出现在同一层楼(SM),或者一层楼只有一个班级,或者没有班级,但是每一层楼的班级最大数量是固定的

Warp对应的是兴趣小组

每个小组有32个学生;(同一时间他们一定是一个班级下的小组)

并且数量固定,即使凑不满这么多学生需要加进来不干活的学生,凑够一个小组

只要求他们有着一样的兴趣爱好(能执行相同的任务)

Thread对应的是学生

一个Thread对应一个SP

每个学生都有个课桌 ,放自己的物品,不能让别人用,表示每个Thread在软件上都有自己的空间(寄存器等)

3.2 硬件资源
SM对应的是教学楼的一个楼层

是实际存在的资源

一个楼层上可以有多个班级,年级和楼层并没有确定的对应关系,一个楼层中可以有很多来自不同的年级的Block

SM中的SP会被分成兴趣小组,承接不同的任务

SP对应的是学生

一个SP对应一个Thread

是实际存在的资源

每个学生都有个课桌 ,放自己的物品,不能让别人用,表示每个SP在硬件上都有自己的空间(local memory + registers);

在楼层中,有公共的空间(走廊、厕所等),这一层楼的所有同学都可以停留,表示一个SM中有shared memory,这个SM上的Block都可以访问;(shared memory是不是所有的block都可以访问)

学校里的公共区域,比如操场、食堂等,所有同学都可以去运动、吃饭,表示GPU中有一些公共的存储空间供所有的Grid访问。

3.3 执行任务
虽然GPU是并行运行,但也并不是我们理想中所有的Thread一起工作,在打扫卫生时,并不是所有学生一起干活,学生经过老师(这里我们理解为Wrap Scheduler)安排后,分为一组一组的小组,每一个小组都只会做一件一样的事情,如果有人先做完了或者不需要做,那么他也会在旁边等他的组员,处于等待状态idle。

4 用多线程掩盖延迟
Global Memory访存延迟可以达到数百个时钟周期,即便是最快的Shared Memory和寄存器在有写后读依赖时也需要数十个时钟周期。这似乎和CUDA强大的处理能力完全相悖。

为什么GPU具有这么高的计算能力?如果连寄存器都这么慢,怎么会有高性能呢?难道这不会成为最大的瓶颈吗?

因为这个高延迟的开销被掩盖了,掩盖在大量线程之下。更清楚的说,控制单元(Warp Scheduler)在多组线程之间快速切换,当一组线程Warp(一个线程组,在CUDA里叫做Warp)因为访存或其他原因出现等待时,就将其挂起,转而执行另一组线程,GPU的硬件体系允许同时有大量线程存活于GPU的SM(流多处理器)之中,这种快速切换保证资源的最大利用率——控制单元始终有指令可以发放,执行单元始终有任务可以执行,仍然可以保持最高的指令吞吐,每个单元基本都能保持充分的忙碌。

这就是GPU硬件设计中非常有特色的基本思想:用多线程掩盖延迟。这一设计区别于CPU的特点是,大量高延迟寄存器取代了少量低延迟寄存器,寄存器的数量保证了可以有大量线程同时存活,且可以在各组线程间快速切换。尽管每个线程是慢的,但庞大的线程数成就了GPU的数据吞吐能力。

下面图片可以说明:GPU用多个Warp掩盖延迟 / 与CPU计算模式的对比

GPU因为多个Warp可以快速切换来掩盖延迟,而CPU用快速的寄存器来减小延迟。两者的重要区别是寄存器数目,CPU的寄存器快但少,因此Context Switch代价高;GPU寄存器多而慢,但寄存器数量保证了线程Context Switch非常快。同时也是因为GPU对高延迟的容忍度比较高,他只追求在长时间内比较稳定的较大吞吐量,而不在意响应时间。

4.1 多少线程才能够掩盖掉常见的延迟呢?
对于GPU,最常见的延迟大概要数寄存器写后读依赖,即一个局域变量被赋值后接着不久又被读取,这时候会产生大约24个时钟周期的延迟。为了掩盖掉这个延迟,我们需要至少24个Warp轮流执行,一个Warp遇到延迟后的空闲时间里执行其余23个Warp,从而保持硬件的忙碌。在Compute Capability 2.0,SM中有32个CUDA核心,平均每周期发射一条指令的情况下,我们需要24 ∗ 32 = 768 24*32 = 76824∗32=768个线程来掩盖延迟。
保持硬件忙碌,用CUDA的术语来说,就是保持充分的Occupancy,这是CUDA程序优化的一个重要指标。

5 关于现代GPU如此进行软件抽象和硬件设计的一些思考
整个设计逻辑关系我觉得可以归结为如下的情况

目标是实现任务

发现任务具有如下的特性:允许一定的延迟;需要大吞吐量;有大量同样的操作或者计算

所以设计了现有的硬件体系架构,软件抽象模型

那么为什么这样的计算或者说任务可以被如上所说的硬件软件更好的完成呢?

其实是因为我们是在已知任务特性的情况下(我们实际使用中所需要完成的任务大概率属于这些,或者说这些任务在CPU上比较容易有掣肘),才把结构设计成这样的。

第一方面:

现实世界中应用在大规模数据上的计算,通常都涵盖在这一计算模式之中,因而考虑更复杂的模式本质上是不必要的。

比如计算大气的流动,每一点的风速仅仅取决于该点邻域上的密度和压强分布;

比如计算图像的卷积,每一个输出像素都仅是对应源点邻域和一个卷积核的内积。

从这些例子中我们可以看到,除了各个数据单元上进行的计算是一样的,计算中数据之间的相互影响也具有某种“局域性”,一个数据单元上的计算最多需要它某个邻域上的数据。这一点意味着线程之间是弱耦合的,邻近线程之间会有一些共享数据(或者是计算结果),远距离的线程间则独立无关。

这个性质反映在CUDA里,就是Block划分的两重天地:Block内部具有Shared Memory,线程间可以共享数据、通讯和同步,Block外部则完全独立,Block间没有通讯机制,相互执行顺序不影响计算结果。这一划分使得我们既可以利用线程间通讯做一些复杂的应用和算法加速,又可以在Block的粒度上自由调度计算任务,在不同计算能力的硬件平台上自适应的调整任务安排。

第二方面:

多个线程同步执行一致的运算,使得我们可以用单路指令流对多个执行单元进行控制,大幅度减少了控制器的个数和系统的复杂度

第三方面:

把注意力放在“几乎一致”这里。最简单的并行计算方案是多路数据上同时进行完全一致的计算,即SIMD(单指令多数据流)。这种方案是非常受限的。事实上我们可以看出,“完全一致”是不必要的。只要这些计算在大多数时候完全一致,就可以对它们做类似于SIMD的加速,不同点是在计算分叉时候,各个线程不一致的特殊情况下,只需要分支内并行,分支间串行执行即可,毕竟这些只是很少出现的情况。 这样,把“完全一致”这个限制稍微放松,就可以得到更广阔的应用范围和不输于SIMD的计算性能,即SIMT(单指令流多线程)的一个重要环节,这是GPU强大处理能力的原因。


原文链接:https://blog.csdn.net/qq_41554005/article/details/119760698

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值