CUDA 03 - 延迟隐藏

SM依赖线程级并行, 以最大化功能单元的利用率, 因此利用率与常驻线程束的数量直接相关. 在指令发出和完成之间对时钟周期被定义为指令延迟. 每当一个时钟周期中所有的线程调度器都有一个符合条件的线程束时, 可以达到计算资源的完全利用. 这就可以保证, 通过在其他常驻线程束中发布其他指令, 可以隐藏每个指令的延迟.

与在CPU上相比, 延迟隐藏在CUDA编程中尤为重要. CPU核心是为同时最小化延迟一个或两个线程而设计的, 而GPU则是为了处理大量并发和轻量级线程以最大化吞吐量而设计的. GPU的指令延迟被其他线程束的计算隐藏.

指令可以被分为两种基本类型:

  • 算数指令
  • 内存指令

算数指令延迟是一个算数操作从开始到他产生输出之间的之间. 内存指令延迟是指发送出的加载或存储操作和数据到达目的地之间的时间. 对于每种情况, 相应的延迟大约为:

  • 算数操作: 10-20个时钟周期
  • 全局内存访问: 400-800个时钟周期

下图表示线程束0阻塞执行流水线的一个事例. 线程束调度器选取其他线程束执行, 当线程束0符合条件再执行他.

warp_scheduler

根据利特尔法则(Little‘s Law)可以估算出隐藏延迟所需要的活跃线程束的数量, 他起源于队列理论中的一个定理, 也可以应用于GPU中:

所需线程束数量 = 延迟 x 吞吐量

假设在内核里面一条指令的平均延迟是5个周期. 为了保持在每个周期内执行6个线程束的吞吐量, 则至少需要30个未完成的线程束.

littles_law

这里要注意吞吐量和带宽这两个概念经常被混淆, 根据实际情况他们可以被交换使用. 吞吐量和带宽都是用来度量性能的速度指标. 带宽通常是指理论峰值, 而吞吐量是指已达到的值. 带宽通常是用来描述单位时间内最大可能的数据传输量, 而吞吐量是用来描述单位时间内任何形式的信息或操作的执行速度, 例如, 每个周期完成多少个指令.

对于算数运算来说, 其所需的并行可以表示成隐藏算数延迟所需要的操作数量. 我们以一个32位的浮点数乘加运算(a + b x c)为例, 表示在每个SM中每个时钟周期内的操作数量:

GPU模型 指令延迟(周期) 吞吐量(操作/周期) 并行(操作)
Fermi 20 32 640
Kepler 20 192 3840

吞吐量由每个周期内的操作数量确定, 而执行一条指令的一个线程束对应32个操作. 因此, 为保持计算资源的充分利用, 对于Fermi GPU而言, 每个SM中所需的线程束数量为640 / 32 = 20个线程束. 因此, 算术运算所需的并行可以用操作的数量或线程数的数量来表示. 着个简单的单位转化表明, 由两种方法可以提高并行:

  • 指令级并行(ILP): 一个线程中由很多独立的指令
  • 线程级并行(TLP): 很多并发地符合条件的线程.