从GPU架构看性能优化(1)Threadblocks,warps和指令执行

1,460 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情


本系列文章将从GPU硬件角度总结如何优化性能,主要资料来源于NVidia官方资料。本文硬件针对的是NVidia GPU,但原理可类推到其他厂商GPU。

从硬件角度看如何提高性能的总原则

  • 尽可能的并行运行
  • 尽可能高效的访问内存
  • 尽可能高效的执行指令

Shader在 GPU上的执行

本篇中,将分析shader指令执行的结构。我们知道GPU有很多核心,GPU每个核心执行一个线程。但核心不是完全独立执行的,32个连续的硬件线程组成一个warp(线程束),指令的分发是以warp为单位的。而我们分配warp是通过Threadblocks。

指令执行的基本方式:SIMT

首先,GPU是以SIMT的方式执行指令的,所谓SIMT即单指令多线程。

  • 一个指令发出后由一整个warp执行。一个warp = 32个连续的线程 (NVidia)
  • 每个线程使用它自己的参数执行操作。

warps和Threadblocks

什么是Threadblock

Threadblocks是用来帮助程序员方便的规划线程使用的概念,在Compute Shader中叫做线程组(ThreadGroup)。Threadblocks可以是1D,2D或者3D。根据使用的情景选择方便的描述方式。

  • 1D的例子:在GPU中并行处理场景中大量物体的AABB在视锥中的裁剪,可以指定数量为AABB个数的1D的线程组,让每个线程对应一个AABB进行裁剪。通过指定1D坐标,可以很方便的从compute shader的输入的结构数组中索引出相应的AABB数据。
  • 2D的例子:在GPU中处理一副图片,每个像素的坐标x,y对应了2D线程组的坐标,这样可以使用2D坐标索引出像素,可以读取和该像素位置对应的资源。比如读取深度图。

从硬件角度看,Threadblocks永远是1D的,它就是一个线程的数组。

连续的32个线程组成一个warp

  • 对于1D线程组:
    • warp 0: threads 0..31
    • warp 1: threads 32..63
  • 对于2D/3D:
    • 首先将2D/3D的线程组ID转换成1D:x是最快变化的维度,z是最慢变化的维度
    • 之后和1D一样组织warp

硬件如何为每个线程组(threadblock)分配warp?

硬件会为线程组分配足够数量的warp,但如果线程组中的线程数量不是warp size(32)的整数倍,那么最后一个warp中的部分线程就啥也不干。一个warp重来不会分开给不同的线程组使用。举个例子:

image.png 上图中,我们定义了一个size为40x2的2D threadblock,这将创建80个线程。硬件会分配给线程组3个warp,图中的蓝色,红色和绿色分别表示3个warp。前两个warp中的所有32个线程都被分配出去了,但是第3个warp(绿色那个)只能分配:80-32*2=16个线程,剩下的16个线程(虚线部分)就是非激活的。这样会造成GPU核心没有被充分利用,影响了性能。

控制流对warp执行的影响

不同的warps可以执行完全不同的代码

  • 每个warp都维护了自己的程序计数器(program counter)
  • 由于是完全不同的控制流,没有任何性能影响

如果一个warp只有一部分线程要执行一个操作

  • 没有参与的线程被“masked out”: 它们不会取操作数,也不会输出,这保证了正确性,但是它们仍然在这些指令上花费时间,因为它们不能执行其他指令。

warp内按条件执行(if/else)

image.png

  • 如果是不同的warps执行不同的代码:

image.png 可以看到,不同的warp互相不影响,各自执行不同的代码,所以对性能没影响。(因为不同的warp有自己的程序计数器,不必互相等待)

  • 如果是同一个warp内部执行不同的代码:

image.png

由于同一个warp内部的所有线程执行时是lock step的,大家必须一起往下走进。所以对于左边,部分线程执行if部分线程执行else的情况,必须要分别执行完if和else之后才能继续一起执行。而如果warp内所有的线程的条件判断都一样,比如右边,都是if,那就不需要执行else了。反之哪怕有一个线程需要执行else,也会让所有线程等待else执行完毕。

更靠近一些,进入指令级别

指令分发

  • 编译器将shader编译成指令序列,指令按序列顺序分发。如果某个指令不具备执行条件,则会挂起当前warp
  • 指令具备执行条件必须满足两个条件:
    • 流水线已准备好执行该指令,某些流水线步骤需要多个指令周期才能分发warp
    • 所有参数已准备好。如果参数需要之前的指令计算的结果,那么只有当结果已计算出来才算准备好。

延迟

  • 指令的延迟大概10-20个周期
  • DRAM内存的访问大概需要400-800个周期,显然内存访问比执行指令慢得多

指令级并行 (ILP)

指令虽然是顺序分发的,但是还是有可能并行执行

  • 相互依赖的指令之间的那些互相独立的指令可以被并行执行
  • ILP是否成立取决于编译器编译出的指令序列 举个例子:

image.png 图中两个FMUL指令是并行的(ILP),因为他们是相互独立的,但是它们所依赖的指令(FFMA)以及依赖它们的指令(ST.E)则不能并行。

warp切换

如果当前执行的warp的指令不具备执行条件,则会挂起warp。例如挂起了N个周期,那么会有N个其他的waprs的具备执行条件的指令被执行。也就是说同一个warp硬件上会来回切换执行不同的warp,已充分的利用硬件。 切换warp没有性能负担,因为寄存器,共享内存这些状态是划分给这些warp的,没有存储/恢复操作发生。

本篇总结

本篇中我们从硬件的角度了解了shader是如何按照线程组去分配到不同的warp上执行,warp内部是如何同步执行32个线程,每个线程上的指令是如何并发执行的以及warp何时会切换。

  • 理解线程组的尺寸必须是warp尺寸的整数倍(在NVidia硬件上,是32的倍数)
  • 理解分支对warp执行的影响,不同的warps没关系(执行不同的shader/变体),同一个warp看分支条件是否是动态的,如果是动态的就会造成分支等待,如果是静态的,同一个warp的所有32个线程是同一个条件,则没影响。
  • 指令的分发是顺序的,但是指令可能并行执行(ILP),指令的延迟比内存延迟低,指令不满足条件会让当前warp挂起,并切换到满足条件的warp。

参考资料

GTC2013: on-demand.gputechconf.com/gtc/2013/pr…