太坑了!为什么我新写的GPU算子利用率超不过50%?

大家好,我是Tim。

最近在网上某论坛看到一个求助贴,该用户用CUDA实现一个简单的纯计算类的GPU算子函数。

然后直接封装调用函数进行执行,结果不管如何调整算子函数的实现方式,其整体的GPU利用率一直无法超过50%。

我们都知道CUDA GPU利用率在很大程度上代表了算子的执行性能,但GPU算子的利用率低不一定是算法实现的问题,很多CUDA新手其实并没有意识到这个问题。

决定GPU算子GPU利用率的因素包括,算子线程数量设置合不合理、访存模式合不合理、数据传输次数是否过多、硬件特性是否被充分利用都可能会导致GPU利用率低。

当然该用户GPU利用率低的最终原因是因为,该用户为GPU算子申请了不合理的块和线程的数量。

那么为什么不合理的块和线程的数量会导致GPU利用率低呢?

今天我们就来详细说下资源分配是如何影响算子的GPU利用率的。

SM结构详解与Warp Scheduler

GPU利用率是用来表示对GPU资源的占用情况,用GPU的专业术语来说就是表示其分配给 SM 的 warp 数量与其可支持的最大数量的比率。

那么什么是SM 呢 ?

如下图所示,我们可以看到在整个GPU架构中存在着大量的流式多处理器(Streaming Multiprocessor,SM),它占据着GPU的大部分空间。

从上面SM的展开图可以看出,每个SM都有自己的寄存器文件、共享内存和缓存等资源,并且拥有很多Core资源,可以同时执行多个线程,可以说SM是GPU中的可独立计算单元。

如果把GPU比作军队中的”师“,那么SM就是独立的”旅“。那为什么GPU要划分SM呢?

首先,通过划分SM的主要目的是提高GPU的并行计算能力和资源利用率。

在划分SM后,GPU就可以通过将将计算任务分解成多个小部分的工作分配给不同的SM并行执行,从而加快计算速度。

其次,划分SM还可以避免不同计算任务之间的资源竞争,提高GPU并行性能。

举个比较形象的例子:假如说将一个汽车制造工厂比喻成GPU芯片,那么每个流水线工作区域就像一个SM。

每个流水线工作区域(SM)都有自己的资源和能力,例如工人、机器和材料,可以独立工作。

同时每个流水线工作区域(SM)还能担当多个任务,例如组装车辆的不同部件,同样地,每个SM可以同时执行多个线程块。

工厂(GPU)将整个汽车制造过程划分为不同的阶段,每个阶段由不同的流水线工作区域(SM)负责,提高并行能力。而不同的流水线工作区域(SM)之间又相互独立,避免了彼此干扰。

从上面图片可以看出一个SM由多个CUDA core组成,每个SM根据GPU架构不同有不同数量的CUDA core。

虽然说SM中还配有register和shared memory等存储资源,但他们属于稀缺资源。

在这种情况下,由哪些线程资源来占有这些稀缺资源执行任务,就离不开Warp Scheduler调度器。

那么什么是Warp Scheduler呢?

warp(线程束)是最基本的执行单元,一般一个warp包含32个并行thread,这些thread只能执行相同的指令

例如,假如一个SM最大只能存储1024个线程的信息,但一个SM可以拥有超过1024个线程。

那么这时在SM内存的warp线程的调度单元就是Warp Scheduler。

一个SM的CUDA core会分成几个warp(即CUDA core在SM中分组),由warp scheduler负责调度。尽管warp中的线程从同一程序地址,但可能具有不同的行为,比如分支结构。

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

CUDA编程模型与GPU的映射关系

在CUDA编程模型中,计算任务是以thread和thread block和grid的形式进行组织的。

我们通常会将计算任务切分成多个可并行的子块,交给多个thread block计算。在thread block内部,我们再将任务进一步划分成多块,由每个thread计算。

GPU硬件也是采用了分层次的组织方式,被划分成多个SM,每个SM内部又有多个CUDA Core。CUDA thread和thread block最终是运行在CUDA Core和SM上面。

从上面图片可以看出,一个Grid可以包括多个SM,也可以访问Global Memory和Constant Memory。

一个Block只能在一个SM中,且一个SM包含多个Block,Block可以访问Shared Memory。

一个Block中有多个Thread,而一个Thread只能访问Registers或local Memory。

具体如下所示:

所以本质上,CUDA编程就是在GPU硬件上启动了线程集合,为了更好的调度线程,GPU采用了分层的架构,在最高层的Grid负责将任务分配到哪些SM硬件上,在SM内部将由Warp调度那些线程来执行当前的任务。

另外需要注意的是,一旦一个线程块被启动在一个流处理器(SM)上,它的所有线程束(warps)都会一直停留在该SM上,直到它们的执行完成。

如果一个warp其操作数之一或两者都还没有准备好(例如,尚未从全局内存中获取),就会发生一种称为“上下文切换”的过程,将控制权转移到另一个线程束上。

但与CPU不同的是,在切换离开特定线程束时,该线程束的所有数据仍然保留在寄存器文件中,以便在其操作数准备好时能够快速恢复执行。

为什么GPU占用率不满?

由于一个SM中的共享资源等是有限的,而这些资源是以线程block为单位分配的。这也就导致一个SM中可执行的线程块的数量是有限的。

那么一个SM能够同时执行多少个线程块呢?

因为一个SM需要有足够多的warp才能够进行并发和切换warp保证性能。

而每个SM能同时执行的warp数上限取决于硬件限制、kernel参数设置和线程与线程块资源使用所导致实际能够执行的数量限制。

首先,硬件限制和kernel参数设置(每个线程块和每个SM的线程和线程块数量是固定的可以通过接口查询)。

当资源充足大于线程和线程块使用的资源时,这时每个SM执行的warp数量受限于kernel设置的参数。

比如每个线程块的线程数太少,那么由于SM同时执行的线程块数量有限,这就导致SM同时执行的线程数不够。

一般一个线程块的线程数要达到128、256才能充分用满SM,这个参数可以进行调节从而找到一个最优值。

其次,线程和线程块资源使用导致实际能够执行的数量限制。如果线程块的shared mem使用太多,比如一个线程块就用完了所有的shared mem的一半以上,这样一个SM最多只能执行1个线程块。

为了保证一个SM能同时执行多个线程块,显然每个线程块只能用每个SM总的shared mem的几分之一。

寄存器使用也是一样,寄存器使用合理时一个warp能够同时执行32个线程,同时一个子块的资源能满足同时驻留多个warp。而寄存器使用太多一个子块无法驻留多个warp,甚至极端情况一个warp的资源所有连32个线程都不够用。

例如,在安培(Ampere)架构的NVIDIA GPU中,每个SM可以同时执行最多64个线程块。图灵(Turing)架构的NVIDIA GPU中,每个SM可以同时执行最多4个线程块。帕斯卡(Pascal)架构的NVIDIA GPU中,每个SM可以同时执行最多2个线程块。

现在,让我们看一个例子,以了解线程块的资源分配如何影响SM的占用率。

例如,在Nvidia H100上,每个SM最多可以处理32个线程块、64个warp(即2048个线程)和每个块1024个线程。

如果我们在申请资源时使用32作为线程块的大小,并总共需要申请使用2048个线程,那么需要64个这样的线程块。

然而,每个SM一次只能处理32个块,即使一个SM完全可以运行2048个线程,但由于线程块的限制,每个SM只能运行32*32个线程,因此它需要两个SM来运行,且每个只能占用50%的GPU资源。

同样,每个SM有65536个寄存器。要同时执行2048个线程,每个线程最多可以有32个寄存器(65536/2048 = 32)。

如果在我们程序中一个内核中每个线程需要64个寄存器,那么每个SM上就只能运行1024个线程,同样会导致50%的占用率。

此外,一个grid应该需要有足够多的线程块。

一个kenel是对应于一个grid,里面要有足够的线程块才能充分利用好整个GPU所有的SM。一方面一个SM本身就需要驻留多个线程块,那么整个GPU几十上百个SM用满的线程块数量应该要乘以一个比较大的倍数才够。

这里举一个深度学习中一个实际的reduce/layer_norm计算的例子:

假如我们计算一个[200, 768] tensor最内部维度每一行的reduce mean,如果naive的想法每个线程计算一行那么总共的线程数才200。

这样只能够生出一两个线程块,只能用上一两个SM,显然性能极差。而如果我们用一个warp来计算一行,那么就有200个warp,如果一个线程块4个warp则有50个线程块,能用上大部分SM。

当然还可以使用一个线程块来计算一行,那么我们就有200个线程块,SM利用率更高。

总结

GPU利用率是指GPU资源的占用情况,其中SM是GPU中的可独立计算单元,将计算任务分解成多个小部分的工作分配给不同的SM并行执行,从而加快计算速度。

Warp Scheduler是SM内存的线程调度单元,负责为每个线程块分配共享内存和为每个线程束中的线程分配独立的寄存器。

CUDA编程模型以thread、thread block和grid的形式组织计算任务,运行在CUDA Core和SM上面,CUDA编程与GPU硬件之间存在着一定映射调度关系。

由于硬件和调度在使用中资源的限制,是的一个SM中可执行的线程块的数量有限,此外还应保证一个grid应该需要有足够多的线程块。

因此,合理设计线程块和warp的数量是非常重要的,才能充分利用GPU资源,提高计算性能。



如果觉得这篇文章对你有所帮助,
请点一下或者在看,是对我的肯定和支持~

请使用浏览器的分享功能分享到微信等