大数跨境
0
0

快速提升性能,如何更好地使用GPU(上)

快速提升性能,如何更好地使用GPU(上) 极市平台
2024-05-14
1
↑ 点击蓝字 关注极市平台
作者丨oldpan
来源丨oldpan博客
编辑丨极市平台

极市导读

 

在GPU成本升高,日益短缺的情况下,如何提升GPU性能的方法? >>加入极市CV技术交流群,走在计算机视觉的最前沿

本篇根据HOW CUDA PROGRAMMING WORKS的讲解,整理下如何更好地使用GPU的一些细节,主要有三点:

  • 让GPU别闲着(Use it at all)
  • 把GPU中所有资源都利用起来(Use all of it)
  • 高效的利用资源(Use it efficiently)
充分利用GPU资源

在我们平常的项目中,除了优化 kernel 性能,能够快速拿到收益的是整体 pipeline 的优化。

我遇到大部分项目在优化性能的时候,kernel 优化一般都是最后一步,很多项目一开始可以先从整体 pipeline 上考虑,拿到可以拿到的性能收益,最后实在扣不出来再考虑优化 kernel。

让GPU别闲着

首先明确一点,GPU是异步的,一般cpu发完命令后(也就是launch了一个 kernel 之后)就没事了。

发kernel指令这一步很快,理论上任务给了gpu之后cpu就能干自己的事儿了。有异步就会有同步,依赖gpu处理结果的cpu线程需要同步操作来等待当前gpu处理完才能进行下一步,比如处理图片:

但是上述gpu利用率并不高(看右面的曲线),这里等gpu执行完的时候其实cpu啥也没干,gpu和cpu之前的数据拷贝需要时间,然后gpu处理完了cpu会做一些后续操作,最后才会处理下一个图片,再加上一些语言层面的 overhead,GPU的利用率就很低了。

其实我们可以把所有的任务一股脑扔给GPU,因为GPU是异步的,cpu把任务传递过去可以通过同步来获取gpu处理完的数据。传递过来的kernel任务会被GPU排入队列一个一个去执行,这个时候GPU的利用率就会高些,毕竟相对没有那么闲了:

这就是最基本的让GPU别闲下来的通用办法,尽量做到:

  • 减少不必要的 CPU-GPU synchronization
  • 在CPU-GPU之间拷贝数据时也可以执行Kernel
  • 很多任务,能通过异步提升性能就尽量异步

将 GPU 中所有资源都利用起来

当我们拿到一张显卡或者说计算卡后,对算力敏感的人一般都会比较计较这个卡的计算资源,比如A100:

A100有108个SM(流处理单元),一共有221184个线程,每个SM都有自己的寄存器、共享显存,共享L2缓存等,我们要做的就是尽可能充分利用这些资源。如果我们某一时刻只用其中一个线程,然后一直调用(kernel一直跑),此时虽然用nvidia-smi命令查出来的GPU utilization是100%,但其实对着张显卡的使用量超级低。

比如这个单block单thread的kernel,理论上只会占用一个SM:

__global__ void simple_kernel() {  
    while (true) {}  
}  
  
int main() {  
    simple_kernel<<<1, 1>>>();  
    cudaDeviceSynchronize();  

但通过nvidia-smi指令得到的GPU Util是100%,这个是要注意的。

GPU如何工作

让我们先快速了解一下Cuda是如何将kernel算法分解成一块一块的,以便将它们分布到这些SM上的。

首先我们把数据分成大小相等的块,这样就可以并行独立地运行每个块。因为这些块是独立的,它们可以按任意顺序和任何时间安排。这样硬件就有尽可能大的自由度来高效地打包事物。每个块的一个保证就是它有一定数量的线程,并且它们都保证在同一个SM上同时运行。

接下来,我们要讨论的是如何使用这些 block 中的线程。每个 block 中有一个特定数量的线程,它们都被分配到同一个 SM 中。SM 中的线程会被分成一定数量的组,这些组被称为 warps。每个 warp 包含特定数量的线程(通常是 32 个),并且这些线程会在同一时钟周期内被执行。这就是为什么我们要使用大量线程的一个原因——保持 GPU 中的所有 SM 和线程都尽可能繁忙。

现在我们回到刚才说的用满

当一个 SM 上运行的所有 block 都已经满了,就不会再有新的 block 被添加到该 SM 上,直到该 SM 上的线程开始完成它们的工作,空出一些空间。在CUDA中,每个流多处理器(SM)可以同时处理多个块,具体数量取决于块的大小,最多可以处理32个。GPU会持续调度块到各个SM上,直到所有块都被处理完毕。

这就是所谓的“occupancy”,它是一个重要的 GPU 性能指标。

我们先看一下如何编写代码来利用这些 GPU 资源。CUDA 提供了一种编程模型,称为 kernel 函数,它是在 GPU 上执行的代码。这些函数可以通过在主机上调用它们来启动 GPU 上的计算。kernel 函数的一个重要特点是,它可以被调用多次,每次使用不同的数据块。这种方式可以使得 GPU 尽可能高效地利用其并行计算资源。

为了使代码能够在 GPU 上运行,我们需要在主机代码中使用一些特殊的关键字和函数,以便让 CUDA 编译器将其转换为 GPU 可以执行的代码:

注意看相关的硬件资源和代码对应的地方,这个kernel计算多个点之间的距离来展示并行计算。程序中使用了共享内存和寄存器,共享内存允许块内的线程进行通信,而寄存器则为每个线程提供必要的工作空间。在GPU上,寄存器的用途与CPU不同,它们提供了大量立即可访问的数据空间,这对于执行复杂的数学运算尤其重要:

在GPU编程中,为每个线程分配大量寄存器是常见的,这与CPU上的情况有显著不同。首先假设当前写的kernel需要使用的资源如下(下图右侧),每个block是256个线程、每个线程使用64个寄存器,每个block的共享显存是48KB:

然后A100的一个SM的资源如下,我们写的这个kernel可以安排三个block到这个SM上,再多了共享显存不够放了:

如果我们把算法重构一下,减少shared memory的使用,比如从48K→32K,那么就可以塞4个block到这个SM上了,这个时候occupancy也就提升了:

所以为了保持GPU的忙碌状态,我们可以进一步尝试填补空闲时间。实际上,GPU可以同时运行多个程序,最多可以同时运行128个不同的程序。因此,如果我们没有完全填满GPU,就可以尝试将另一个程序“俄罗斯方块”式地放置在旁边。

比如我们想象另一个case,这次是一个大量线程、很少寄存器和没有共享内存的情况。实际上就是一个数据移动kernel的典型表示,可能是对数据进行排序或只是将数据从一个地方复制到另一个地方。

GPU很聪明,它会尝试将一个绿色网格块填充到空隙中,如果不能再放置绿色网格块,那么就会尝试另一个。

现在我们每个SM使用的线程比之前增加了三分之二,这几乎是免费的性能提升。这个“俄罗斯方块”问题又增加了一个维度,但硬件会为你解决这个问题,让内核去帮你处理就行了。

公众号后台回复“数据集”获取100+深度学习各方向资源整理

极市干货

技术专栏:多模态大模型超详细解读专栏搞懂Tranformer系列ICCV2023论文解读极市直播
极视角动态欢迎高校师生申报极视角2023年教育部产学合作协同育人项目新视野+智慧脑,「无人机+AI」成为道路智能巡检好帮手!
技术综述:四万字详解Neural ODE:用神经网络去刻画非离散的状态变化transformer的细节到底是怎么样的?Transformer 连环18问!

点击阅读原文进入CV社区

收获更多技术干货


【声明】内容源于网络
0
0
极市平台
为计算机视觉开发者提供全流程算法开发训练平台,以及大咖技术分享、社区交流、竞赛实践等丰富的内容与服务。
内容 8155
粉丝 0
极市平台 为计算机视觉开发者提供全流程算法开发训练平台,以及大咖技术分享、社区交流、竞赛实践等丰富的内容与服务。
总阅读3.2k
粉丝0
内容8.2k