3.1 单环近似

文章详细介绍了GPU的SIMT执行模型,包括线程组织、执行掩码和SIMT堆栈的运作,以及如何处理分支和并发。同时,提出了SIMT死锁问题,特别是在原子操作和并发控制流中的表现,并描述了NVIDIAVolta架构中采用的无堆栈SIMT分支管理和独立线程调度策略,以避免死锁并提高效率。最后,讨论了warp调度策略及其对内存系统和局部性的影响。
摘要由CSDN通过智能技术生成

我们首先考虑具有单个调度器的 GPU。 如果只阅读 CUDA 编程手册中的硬件描述,这种简化的硬件外观与人们可能期望硬件执行的操作没有什么不同。

为了提高效率,线程被组织成 NVIDIA 称为“warp”的组和 AMD 称为“wavefronts”的组。 因此,调度的单位是warp。 在每个周期中,硬件选择一个 warp 进行调度。 在单循环近似中,warp 的程序计数器用于访问指令存储器,以找到要为warp 执行的下一条指令。 获取一条指令后,对该指令进行解码,并从寄存器文件中获取源操作数寄存器。 在从寄存器文件中获取源操作数的同时,确定了 SIMT 执行掩码值。 以下小节描述了如何确定 SIMT 执行掩码值,并将它们与现代 GPU 中也采用的谓词进行对比。

在执行掩码和源寄存器可用后,执行将以单指令、多数据的方式进行。 如果设置了 SIMT 执行掩码,则每个线程都在与通道关联的功能单元上执行。 与现代 CPU 设计一样,功能单元通常是异构的,这意味着给定的功能单元仅支持指令的一个子集。 例如,NVIDIA GPU 包含特殊功能单元 (SFU)、加载/存储单元、浮点功能单元、整数功能单元,以及 Volta 架构中的 Tensor Core。

所有功能单元名义上都包含与 warp 中的线程一样多的通道。 然而,一些 GPU 使用了不同的实现方式,其中单个warrp或wavefront在多个时钟周期内执行。 这是通过以更高的频率为功能单元提供时钟来实现的,这可以以增加能耗为代价实现更高的单位面积性能。 为功能单元实现更高时钟频率的一种方法是流水线化它们的执行或增加它们的流水线深度。

3.1.1 SIMT 执行掩码

现代 GPU 的一个关键特征是 SIMT 执行模型,从功能(尽管不是性能)的角度来看,它向程序员展示了各个线程完全独立执行的抽象。 这种编程模型有可能仅通过谓词来实现。 然而,在当前的 GPU 中,它是通过传统谓词与我们称之为 SIMT 堆栈的谓词掩码堆栈的组合来实现的。

SIMT 堆栈有助于有效地处理当所有线程都可以独立执行时出现的两个关键问题。 第一个是嵌套控制流。 在嵌套控制流中,一个分支依赖于另一个分支的控制。 第二个问题是完全跳过计算, warp 中的所有线程都跳过了控制流路径。 对于复杂的控制流,这可以带来显著的节省。 传统上,支持谓词的 CPU 通过使用多个谓词寄存器来处理嵌套控制流,并且在文献中提出了支持跨通道谓词测试的CPU。

GPU 使用的 SIMT 堆栈可以处理嵌套控制流和跳过计算。 专利和指令集手册中描述了几种实现方式。 在这些描述中,SIMT 堆栈至少部分由专用于此目的的特殊指令管理。 但是在此,我们将描述一个学术著作中引入的稍微简化的版本,该版本假定硬件负责管理 SIMT 堆栈。

 

我们使用一个示例来描述 SIMT 堆栈。 图 3.2 是在 do-while 循环中的包含两个嵌套分支的 CUDA C 代码,图 3.3 是相应的 PTX 程序集。 图 3.4再现了 Fung 等人著作的图 5。 [Fung et al., 2007] 该图说明了这段代码如何与 SIMT 堆栈交互,假设 GPU 每个 warp 有四个线程。

图 3.4a 说明了与图 3.2 和 3.3 中的代码对应的控制流图 (CFG)。 如流图中 顶部节点内的标签“A/1111”所示,最初 warp 中的所有四个线程都在执行基本块 A 中的代码,该代码对应于图 3.2 中第 2 至 6 行的代码和 图 3.3 中的第 1 至第 6 行中的代码。 这四个线程在执行图 3.3 中第 6 行的分支后遵循不同的(发散的)控制流,这对应于图 3.2 中第 6 行的“if”语句。 具体来说,如图 3.4a 中的标签“B/1110”所示,前三个线程落入基本块 B。这三个线程分支对应图 3.3 中的第 7 行(图 3.2 中的第 7 行)。 如图 3.4a 中的标签“F/0001”所示,执行完分支后,第四个线程跳转到基本块 F,对应于图 3.3 中的第 14 行(图 3.2 中的第 14 行)。

类似地,当在基本块 B 中执行的三个线程到达图 3.3 中第 9 行的分支时,第一个线程分叉到基本块 C,而第二个和第三个线程分叉到基本块 D。然后,所有三个线程都到达基本块 E,如图 3.4a 中的标签“E/1110”所示一起执行。 在基本块 G,所有四个线程一起执行。

GPU 硬件如何做到,在采用每个周期只允许执行一条指令的 SIMD 数据路径的同时使 warp 中的线程遵循代码中不同的路径?当前 GPU 中使用的方法是串行化执行给定的 warp 中的不同路径 . 这在图 3.4b 中进行了说明,其中箭头代表线程。 实心箭头表示线程正在执行相应基本块中的代码(由每个矩形顶部的字母表示)。 空心箭头表示线程被屏蔽了。 如底部的箭头所示,时间在图中向右推进。 最初,基本块A中的每个线程都在 执行。然后,在分支之后,前三个线程执行 基本块B 中的代码。请注意,此时线程 4 被屏蔽掉了。 第四个线程在别的时间执行基本块 F 的代码路径(本例中稍后的几个周期)。

要实现不同代码路径的这种串行化,一种方法是使用如图 3.4c–e 所示的堆栈。 此堆栈上的每个条目包含三个内容:重汇聚程序计数器 (RPC)、下一条要执行的指令的地址 (Next PC) 和活跃掩码。

图 3.4c 说明了 warp 执行了图 3.3 中第 6 行的分支后堆栈的状态 。 由于三个线程分支到 基本块B,一个线程分支到 基本块F,因此栈顶 (TOS) 添加了两个新条目。 warp 执行的下一条指令是使用栈顶 (TOS) 条目中的 Next PC 值来确定。 在图 3.4c 中,此 Next PC 值为 B,表示基本块 B 中第一条指令的地址。相应的 Active Mask 中“1110”表示 warp 中只有前三个线程应执行此指令。 warp 中的前三个线程继续执行来自基本块 B 的指令,直到它们到达图 3.3 中第 9 行的分支。 如前所述,在执行此分支后,它们会发散。 这种分支发散导致堆栈发生三个变化。 首先,将执行分支之前的TOS条目的Next PC条目(在图3.4d中标记为(i)),修改为分支的收敛点,即基本块E中第一条指令的地址。然后, 添加了图 3.4d 中标记为 (ii) 和 (iii) 的两个条目,用于执行分支后 warp 中线程所遵循的每个路径。

重汇聚点是程序中的一个位置,在该位置可以强制发散的线程以锁步方式继续执行。 通常重汇聚点越近越好。 在给定程序执行中,在编译时能保证发散的线程可以再次锁步执行的最早点是导致分支发散的分支的直接后继重聚点。 在程序运行时,有时可以在程序的较早点重新汇聚 [Coon and Lindholm, 2008, Diamos et al., 2011, Fung and Aamodt, 2011]。

一个有趣的问题是“在分支发散之后应该使用什么顺序将条目添加到堆栈中?” 要将重新汇聚堆栈的最大深度降低为 warp 中线程数的对数,最好先将具有更多活跃线程的条目放入堆栈,然后是具有较少活跃线程的条目 [AMD,2012]。 在图 3.4 (d) 中,我们遵循此顺序,而在 3.4(c) 中,我们使用相反的顺序。

3.1.2 SIMT 死锁和无堆栈 SIMT 架构

最近,NVIDIA 披露了他们即将推出的 Volta GPU 架构的细节 [NVIDIA Corp., 2017]。 他们强调的一个变化是发散下的掩码行为及其与同步的相互作用。 基于堆栈的 SIMT 实现可能导致死锁情况,ElTantawy 和 Aamodt [2016] 将其称为“SIMT 死锁”。 一些学术工作描述了用于 SIMT 执行的替代硬件 [ElTantawy et al., 2014],只需稍作改动 [ElTantawy and Aamodt, 2016],即可避免 SIMT 死锁。 NVIDIA 将他们新的线程分支管理方法称为独立线程调度 Independent Thread Scheduling。 独立线程调度的描述表明它们实现的行为类似于上述学术建议所获得的行为。 下面,我们首先描述 SIMT 死锁问题,然后描述一种避免 SIMT 死锁的机制,该机制与 NVIDIA 对独立线程调度的描述一致,并且在最近的 NVIDIA 专利申请 [Diamos et al., 2015] 中公开。

图 3.5 的左侧给出了一个 CUDA 示例来说明 SIMT 死锁问题,中间部分显示了相应的控制流图。 A 行将共享变量 mutex 初始化为零,以指示锁是空闲的。 在 B 行,warp 中的每个线程都执行 atomicCAS 操作,该操作对包含互斥量的内存位置执行比较和交换操作。 atomicCAS 操作是编译器内在的,它被转换为 atom.global.cas PTX 指令。 从逻辑上讲,比较和交换操作首先读取互斥量的内容,然后将其与第二个输入 0 进行比较。如果互斥量的当前值为 0,则比较和交换操作将互斥量的值更新为第三个输入 1。 atomicCAS返回的值是mutex的原始值。 重要的是,比较和交换为每个线程自动执行上述逻辑操作序列。 因此,对同一 warp 中不同线程来说,atomicCAS 对任何单个位置的多次访问是串行化的。 由于图 3.5 中的所有线程都访问相同的内存位置,因此只有一个线程会将 mutex 的值视为 0,而其余线程会将值视为 1。接下来,在牢记 SIMT 堆栈的同时,考虑 atomicCAS 返回后B 行的 while 循环发生了什么。 不同的线程看到不同的循环条件。 具体来说,一个线程将想退出循环,而其余线程想留在循环中。 退出循环的线程将到达重新收敛点,因此将不再在 SIMT 堆栈上处于活动状态,因此无法执行 C 行的atomicExch 操作以释放锁。留在循环中的线程将处于活动状态 ,一直在 SIMT 堆栈的顶部,并将无限期地旋转。 由此产生的线程之间的循环依赖引入了一种新形式的死锁,ElTantawy 和 Aamodt [2016] 将其称为 SIMT 死锁,如果在 MIMD 架构上执行线程,这种死锁将不会存在。

 

 

接下来,我们总结了一种无堆栈分支充汇聚机制,就像 NVIDIA 最近的美国专利申请 [Diamos et al., 2015]。 该机制与 NVIDIA 迄今为止对 Volta 的重汇聚处理机制的描述一致 [Nvidia, 2017]。 关键思想是用每个 warp 的汇聚barrier替换堆栈。 图 3.6 显示了 NVIDIA 专利申请中描述的每个 warp 维护的各种字段,图 3.8 提供了相应的示例来说明汇聚的操作。 实际上,该提案提供了多路径 IPDOM [ElTantaway 等人,2014 年] 的替代实现,这将在第 3.4.2 节中与早期的学术作品一起描述。 收敛障碍机制与 Fung 和 Aamodt [2011] 中描述的扭曲障碍的概念有一些相似之处。 为了帮助解释下面的收敛障碍机制,我们考虑在图 3.8 中的代码上执行单个 warp,它显示了从 CUDA 代码产生的控制流图,如图 3.7 所示。

接下来,我们描述图 3.6 中的字段。 这些字段存储在寄存器中并由硬件 warp 调度程序使用。 每个 Barrier Participation Mask 用于跟踪 warp 中的哪些线程参与给定的汇聚屏障。 对于某个warp,可能有多个Barrier Participation Mask。 在常见情况下,由给定Barrier Participation Mask跟踪的线程将在发散分支后等待彼此到达程序中的公共点,从而重新汇聚在一起。 为了支持这一点,Barrier State字段用于跟踪哪些线程已到达给定的汇聚barrier。 对于 warp 中的每个线程,Thread State跟踪线程是否准备好执行、是否阻塞在汇聚barrier(如果是,是哪个barrier)或是否已经屈服。 yielded 状态可用于使 warp 中的其他线程在可能导致 SIMT 死锁的情况下向前推进通过汇聚barrier。 Thread rPC 字段为每个不活动的线程跟踪下一条要执行的指令的地址。 Thread Active 字段是1bit,指示 warp 中的相应线程是否处于活动状态。

假设一个 warp 包含 32 个线程,barrier participation mask位宽是 32 bit。 如果设置了一个bit,则表示着 warp 中的相应线程参与了这个汇聚barrier。 线程在执行分支指令时发散,例如图 3.8 中基本块 510 和 512 末尾的那些指令。 这些分支对应于图 3.7 中的两个“if”语句。 warp 调度程序使用barrier participation mask在特定的汇聚barrier位置停止线程,该位置可以是分支的直接后支配者或其他位置。 在任何给定时间,每个 warp 可能需要多个barrier participation mask 来支持嵌套控制流结构,例如图 3.7 中的嵌套 if 语句。 图 3.6 中的寄存器可能使用通用寄存器或专用寄存器或两者的某种组合来实现(专利申请中没有说明)。 鉴于barrier participation mask只有 32 bit宽,如果每个线程都有一个屏障参与掩码的副本,那么如果朴素地使用通用寄存器文件来存储它可能会是多余的。 然而,由于控制流可以嵌套到任意深度,给定的warp可能需要任意数量的屏障参与掩码,使得掩码的软件管理成为可取的。

为了初始化汇聚barrier participation mask,采用了特殊的“ADD”指令。 当 warp 执行此 ADD 指令时,所有处于活跃状态的线程都在 ADD 指令指示的汇聚barrier中设置了它们的对应的bit。 在执行分支后,一些线程可能会发散,这意味着要执行的下一条指令的地址(即 PC)将不同。 当发生这种情况时,调度器将选择具有公共 PC 的线程子集并更新 Thread Active 字段以启用对 warp中这些线程的执行。 学术提案中将这样的线程子集称为“warp split”[ElTantawy et al., 2014, ElTantawy and Aamodt, 2016, Meng et al., 2010]。 与基于堆栈的 SIMT 实现相比,通过收敛屏障实现,调度器可以在不同线程组之间自由切换。 当一些线程获得了锁而其他线程没有获得锁时,这可以在 warp 中的线程之间向前推进。

“WAIT”指令用于在达到汇聚barrier时停止 warp split。 正如 NVIDIA 的专利申请中所描述的,WAIT 指令包括一个操作数来指示汇聚barrier的ID。 WAIT 指令的作用是将 warp split 中的线程添加到barrier的 Barrier State 寄存器中,并将线程的状态更改为阻塞。 一旦barrier participation mask中的所有线程都执行了相应的 WAIT 指令,线程调度器就可以将所有线程从原始 warp split切换为活跃状态,这保持了 SIMD 效率。 图 3.8 中的示例有两个汇聚barrier,B1 和 B2,在基本块 516 和 518 中带有 WAIT 指令。为了能够在 warp split之间切换,NVIDIA 描述了使用 YIELD 指令以及其他细节,例如支持间接分支,我们在此不讨论 [Diamos et al., 2015]。

 

图 3.9 显示了基于堆栈的重新收敛的时序示例,图 3.10 说明了使用独立线程调度的潜在时序,如 NVIDIA 的 Volta 架构白皮书中所述。 在图 3.10 中,我们可以看到 Volta 架构中将语句 A 和 B 与语句 X 和 Y 交织在一起,这与图 3.9 中的行为形成对比。 此行为与上述汇聚barrier机制(以及多路径 IPDOM [ElTantaway 等人,2014])一致。 最后,图 3.11 说明了无堆栈架构如何执行图 3.5 中的自旋查找代码以避免 SIMT 死锁。

3.1.3 WARP 调度

GPU 主机中的每个核都包含许多 warp。 一个非常有趣的问题是这些 warp 应该按什么顺序调度。为了简化讨论,我们假设每个 warp 在被调度时只发出一条指令,而且在第一条指令完成执行之前 warp 不会发出另一条指令 . 我们将在本章后面重新讨论这个假设。

如果内存系统是“理想的”并且在某个固定的延迟内响应内存请求,那么从理论上讲,设计一个支持足够多warp的内核并使用细粒度多线程隐藏这种延迟是可能的。 在这种情况下,我们可以通过以循环调度来调度 warp 来减少芯片面积。 在循环调度中,warp 被赋予一些固定的顺序,例如以线程ID来排序,并且 warp 由调度器按此顺序选择。 这种调度顺序的一个特性是它允许每条发出的指令完成执行的时间大致相等。 如果核心中的 warp 数量乘以每个 warp 的发出时间超过内存延迟,则核心中的执行单元将始终保持忙碌状态。 因此,增加warp 数量原则上可以增加每个核的吞吐量。

然而,有一个重要的权衡:要使不同的 warp 在每个周期发出一条指令,每个线程都必须有自己的寄存器(这避免了在寄存器和内存之间复制和恢复寄存器状态的需要)。 因此,增加每个核的 warp 数量会相当于在执行单元的面积不变的情况下增加专用于寄存器文件存储的芯片区域。 对于固定的芯片面积,增加每个核的warp将减少每个芯片的核总数。

实际上,内存的响应延迟取决于应用程序的位置属性和片外内存访问遇到的争用量。 让我们考虑 GPU 的内存系统时,调度会产生什么影响? 这是过去几年大量研究的主题,在我们的 GPU 微体系结构模型中添加有关内存系统的更多细节后,我们将回到这个问题。 然而,简而言之,局部性可以支持或阻止循环调度:当执行不同的线程时,它们在相似点共享数据时,例如当访问图形像素着色器中的纹理贴图时,各线程取得相同的进展是有益的,这会增加片上缓存中“命中”的内存引用数量,这是循环调度 [Lindholm et al., 2015] 所鼓励的。 类似地,当地址空间中的附近位置在时间上附近被访问时,访问 DRAM 会更有效率,这也是循环调度 [Narasiman et al., 2011] 所鼓励的。 另一方面,当线程主要访问不相交的数据时(这种情况往往发生在更复杂的数据结构中),重复调度一个线程以最大化局部性可能是更好的 [Rogers et al., 2012]。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值