找回密码
 立即注册
查看: 183|回复: 0

通用图形处理器架构 #4 SIMT核心(2)

[复制链接]
发表于 2022-6-6 08:23 | 显示全部楼层 |阅读模式
更进一步

在我们之前描述的架构中,我们只有在执行完一条指令后才会去取下一条指令,但是就像在流水线CPU中那样,我们可以通过流水线来提高指令吞吐量。也就是说我们需要添加一个scheduler来判断下一条指令与当前指令是否存在冒险,例如结构冒险或数据冒险。为了实现这个特性,GPU中设计了instruction buffer,并且一个新的scheduler被用来决定在指令缓存中哪一些指令可以被发射到流水线中。
指令存储器被设计为一个L1缓存,并且在L1缓存后还有一个或多个L2缓存,instruction buffer还可以与MSHR(miss状态保存寄存器)相结合来实现non-block cache的特性,在发生cache miss后不会影响其他cache请求,当前cache miss的信息会被存入MSHR等待处理。通常我们会在指令缓存中为每一个warp存储一条或多条指令。
在传统CPU上想要检测流水线冒险可以通过计分牌或Tomasulo中的保留站实现。保留站可以通过寄存器重命名消除名相关的依赖,但是带来的能源和芯片面积开销较大。计分牌策略可以在顺序执行或乱序执行时使用,对于一个单线程顺序执行的流水线,计分牌策略是非常简单的:每一个寄存器在计分牌中被一个单独的bit表示,如果该寄存器被读或写,对应的bit会被标记。任何想要读写一个已经被标记的寄存器的指令都会stall直到标记被重置,这样可以消除RAW和WAW,在顺序执行与计分牌相结合后我们也可以消除WAR。由于计分牌策略不会消耗太多的芯片面积和能量,因此GPU采用计分牌对流水线冒险进行探测。但是对于多个warp来说,计分牌策略也面临很大的挑战。
在GPU中,计分牌面临的第一个问题就是GPU中拥有大量的寄存器需要监控。对于每一个warp最多可以使用128个寄存器,每一个核心最多可以拥有64个warp,因此最多需要8192个bit来监控寄存器状态。另一个问题是在我们遇到寄存器被占用时,我们不知道什么时候这个寄存器会被释放,因此需要不断访问对应的寄存器bit直到寄存器被释放。对于单线程来说这可能不是什么问题,但是我们拥有一个顺序执行的多线程处理器,这可能会导致大量的线程等待上一条指令执行结束。如果所有的指令都需要监控计分牌上的状态,那么就需要大量的read port。对于现在支持64个warp的核心以及需要同时访问4个操作数(一个周期可以发送两条指令),那么最多需要265个read port。一种减小开销的方法是限制每一个周期可以监控计分牌的warp数量,这种情况下如果我们检查的指令都有依赖,即使没有检查的指令中有无依赖的指令我们也无从得知,因此就不能发射指令。
为了解决这个问题,我们可以使用三到四个项保存在寄存器中。每一个项可以保存一个指令具体使用的寄存器,而不是原有的通过bit指示寄存器是否被使用。原有的计分牌是在指令发出和完成时进行更新的,而新的计分牌策略是在指令被放入instruction buffer和指令完成时被更新的。
当一条指令从instruction cache中被取会到instruction buffer时,会将目标寄存器和源寄存器与对应warp的计分牌项进行比较,通常一个warp会有3-4个寄存器,对应需要3-4个bit来对应寄存器。如果操作数在计分牌中被对应上,那么将对应的bit置为1即可,这个短向量会被拷贝到instruction buffer中,后续scheduler决定是否发射就是根据这个短向量进行判断的。
只有短向量中所有bit都被置为0时这条指令才会被scheduler考虑发射(此时依赖被消除了),这可以通过一个NOR门实现。如果一个warp中所有存储状态的寄存器都被使用完,那么我们只能stall或是丢弃当前指令,后续可能需要重新取回。当一条指令被执行完成,他会清除当前warp中所有等待使用其占有的寄存器状态位(释放寄存器)。
我们在前一篇文章中看到了指令如何被从cache中取回并解码发射,在这一部分我们看到了用于判断指令之间依赖的计分牌策略。
朴素的寄存器访问架构

为了支持warp切换来隐藏延迟,我们需要一个很大的寄存器堆来保证每一个warp拥有其独立的存储空间。在最近的GPU架构中包含了256KB的寄存器用于不同的warp,SRAM的面积正比于其读写port的数目。这就带来了一个问题,如果每一个寄存器都拥有独立的读写port,大量的port会占据不可接受的面积和功耗,并且也会存在大量的浪费。为了解决这个问题,我们可以将多个寄存器组成一个bank,每一个bank拥有一个逻辑读写port,在指令集中将bank的读写port暴露给程序员。在现在的GPU中通常使用operand collector来实现更加透明的操作。这一部分其实和我之前写过的向量处理器基本是一样的。


上图是一个简易的寄存器访问架构,通过四个bank访问寄存器,后面使用一个crossbar连接到SIMD执行部件。通常如果寄存器堆比较大的时候,每一个逻辑读写port会对应多个物理读写port。仲裁器会控制对每一个bank的访问,并最终将操作数送入对应的流水线寄存器中。


上图显示了寄存器堆内的状态,bank中每一个位置都由两个标识符确定,例如(w1:r4)表示了warp1中的r4寄存器值放在这个位置,以此类推。如果寄存器编号超过4,例如r6,我们使用6%4=2就可以得到r6应该被存放在bank2中。


上面的图展示了三个warp执行两条指令i1和i2的过程。第一个指令时一条乘加指令,这条指令会从bank1、0、2中分别读取r5、r4和r6的值。第二条指令是加法指令,它将从r5和r1中读取,这两个寄存器都被分配在bank1中。中间的图展示了指令发射的顺序,在第0个周期w3的i1被发射,第1个周期w0的i2被发射,第4个周期w1的i2被发射,w1的i2延迟了两个周期是由于访问寄存器时发生了bank conflict,后面会详细说明。
第三张图显示了bank在每个周期被访问的情况,cycle 1时,w3的i1可以同时访问r4、r5和r6,这是由于这三个寄存器分布在三个不同的bank中,因此没有发生bank conflict。cycle 2是,我们需要访问r5和r1,但是前面提到了这两个寄存器都存在在bank 1中,由于一个bank只有一个逻辑读写port,因此我们在一个周期内只能访问一个寄存器。在这里我们在cycle 2访问了r1,在cycle 3访问了r5。在cycle 3,w3的指令需要将结果写回寄存器r2,r2位于bank 2,没有发生bank conflict,因此可以和r5的访问并行执行。对于w1的i2指令也是相同的情况,在cycle 4访问r1和r5时发生了bank conflict,因此在cycle 4我们选择访问r1。当来到cycle 5时我们出现了w0的i2需要将计算结果写回r5寄存器,与我们w1的i2访问r5发生了bank conflict,在这里我们选择先写回w0的r5(w0的优先级更高),而w1的读取被推迟到了cycle 6。
Operand Collector概述



Operand Collector的架构如上图所示,与之前的架构最主要的区别在于原有的流水线寄存器被替换为Collector Units。当一条指令进入寄存器读取阶段后,在Collector Units中都会给它分配一个unit。由于我们有多个units的存在,我们就可以实现多条指令重叠访问寄存器堆,进而改善发生bank conflict时的吞吐量。对于多条指令带来的海量操作数存取需求,我们可以近似看作在bank层面实现了并行化。
Operand collector可以在发生bank conflict时通过调度解决冲突。


为了减少bank conflict,我们可以考虑对寄存器堆的存储方式进行优化。如上图所示,我们对w1所需的八个寄存器进行了一个偏移,即reg_Id%4+warp_Id为最终的bank Id。这样的排布可以减少指令间的bank conflict。在每个warp进度相似时,就会出现这种情况。


上图展示了bank在时间上是如何被访问的,但是在看的时候我发现w1:r5应该也可以在cycle 1的bank 2被访问,不是很清楚这里为什么被移动到了cycle 2访问。不过这样也可以体现引入collector units后我们可以在不同warp之间进行调度,选择没有bank conflict的指令访问寄存器堆。
一个潜在的问题时WAR读后写,如果我们有两条处于同一个warp的指令,第一条指令会读取一个寄存器,这个寄存器在第二条指令中会被写入。如果第一条指令由于反复的bank conflict发生,那么显然第二条指令会在寄存器中写入一个新的值,这时第一条指令就不能读取到正确的数值。一个简单的解决方案时强制operand collector以代码顺序执行,一些学者开发了另外三种潜在的解决方案:release-on-commit warpboard、release-on-read warpboard和bloomboard。
潜在的结构冒险

当一条指令在GPU流水线上发生结构冒险了怎么办?对于一般的CPU来说我们可以简单的暂停当前指令,直到结构冒险消除再继续执行。但是这种方法在高吞吐量的系统中并不适用,停滞的指令可能处于任务的关键路径上进而影响到任务的完成时间,并且大量的停滞需要额外的缓冲区来存储寄存器信息。同时停滞一个指令可能会停滞其他完全不必要停滞的指令,降低了系统的吞吐量。
在GPU中我们可以尝试使用instruction replay来解决这个问题。instruction replay最早是在CPU的推测执行中作为一种恢复机制出现的,当我们执行了错误的分支,正确的指令会被重新取回并执行,消除错误分支的影响。在GPU中我们一般会避免推测执行,因为这会浪费宝贵的能源以及吞吐量。GPU实现instruction replay是为了减少流水线阻塞以及芯片面积和对应的时间开销。
在GPU上实现instruction replay可以通过在instruction buffer中保存当前指令直到这条指令已经执行完成,然后再将其移出。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-5 04:58 , Processed in 0.170123 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表