通过“microbenchmark”解谜GPU的微架构

目录

摘要

一、简介

二、背景:GUP架构以及编程模型

        2.1 GPU的架构

         2.2 CUDA软件编程接口

三、测量手段

        3.1Microbenchmark 技术

        3.2 通过latency图推断cache设计参数

四、测试结果

 4.1 clock函数的开销和特性

 4.2 计算pipeline

 4.3 控制流



摘要

        在处理某些非图形计算任务时,图形处理器(GPU)仍然比传统的处理器快了几个数量级。因为GPU通常提供了类似C语言的编程抽象模型(比如Nvidia的CUDA),外界对GPU的了解一般仅限于芯片供应商的数据手册。本文提出了一种称为“microbenchmark”的套件,并且运用这个套件测量了CUDA编程模型中的Nvidia GT200(GTX280)的架构。众多未经官方披露的特性都进行了测试,包括元素处理、memory层级等等。这种分析揭露了一些可能会影响编程性能和正确性的特点,分析结果对提升编程性能、分析和建模GPU架构都是很有意义的,并且对于这款GPU的更新升级提供了一些意见。

一、简介

        GPU被用作非图形处理时,架构与传统的串行处理器有很大不同。对于从事GPU的开发人员或者GPU架构师、编译器开发人员来说,深入理解当前的GPU架构都是必要的。

        Nvidia旗下的G80和GT200都是可以用于非图形计算任务的GPU,它们都使用类C语言的CUDA编程接口。对于GPU的性能指标,CUDA编程手册里通过rules的形式来提供了一些指标。然而有时这些rules含糊不清,通过它很难理解底层硬件以及驱动硬件的方式。

        本文针对GPU架构的特定部分提供了一个microbenchmark套件。我们呈现的套件关注两个可能影响GPU性能的方面:

        1、算术运算core;

        2、为这些算术运算core提供指令与数据的memory层级;

        精确理解这些运算core以及对应的缓存层级有利于避免死锁、优化程序性能以及cycle级别的GPU性能建模。

        本文主要内容包括:

        1、验证了CUDA编程手册中的性能指标;

        2、探索了分支差异化以及thread的同步化隔离的详细功能。我们发现一些不直观的分支代码会导致死锁,这样的情况可以从GPU内部架构来分析原因。

        3、测量了缓存的层级和性能,包括Translation Lookaside Buffer(TLB),constant memory,texture memory和指令cache。

        4、呈现了本文的测量手段,我们相信这个测量手段对分析和建模其他GPU或类GPU的系统同样有用。

        本文的余下章节是这样组织的:第二节回顾了CUDA的编程模型;第三节描述了我们的测量手段;第四节展示测量结果;第五节回顾与我们相关的工作;第六节对我们的发现进行总结。

二、背景:GUP架构以及编程模型

        2.1 GPU的架构

        CUDA将GPU建模为一个多核系统。它将GPU中运行的thread级并行机制抽象为一个“thread金字塔”,即多个thread组成一个warp,多个warp组成一个block,多个block进一步组成grid。与这个“thread金字塔”相对应的有另一个“硬件资源金字塔”。一个block中的所有thread对应一个SM(Streaming Multiprocessor,如下图),它们都在这个SM中执行。虽然CUDA编程模型将所有的线程看成scalar thread,但是SM更像是一个元素位宽32bit,元素数量为8的vector处理器(即SIMD为512bit的vector处理器)。

         SM运转时是以warp为基本处理单元的。在GT200中,一个warp包含32个thread,在8个scalar 处理器上以8个thread为一组执行。Nvidia称这种运转方式为SIMT(Single-Instruction Multiple-Thread),因为同一个warp中的所有thread都执行同一条指令,但是允许每个thread有各自的执行方式。SM内部的资源只能被一个block中的thread使用,例如算术运算单元、share memory以及register file。多个SM进一步组成一个TPC(Thread Processing Cluster),TPC中除了SM之外还有一些可供这些SM共享的资源,例如cache、texture fetch unit等等,这些资源大部分都对编程人员不可见。

        从CUDA的角度,GPU就是有多个TPC、TPC互联网络、以及memory系统共同构成的。      ​​

        下表是Nvidia官方发布的GT200构成细节。

         2.2 CUDA软件编程接口

         CUDA使用了类似于C语言的编程模型来呈现GPU架构,其中将上述thread模型抽象成C语言的扩展。在CUDA模型中,host CPU可以通过在代码中调用device功能函数的方式来发动GPU中的kenerl。因为host CPU和GPU之间使用的指令集不同,CUDA的编译流程中采用不同的编译器来编译CPU和GPU代码。GPU代码首先编译成类似汇编代码的"PTX",进一步汇编成机器码。编译好的CPU和GPU代码最终融合成一个二进制码流。

        虽然PTX的描述是“GPU代码的汇编层级表达”,但是它仅仅是一种中间表达,对于细节的分析或者micro-benchmark作用不大。因为原始的指令集与PTX代码不一致,并且编译器会在PTX之上做优化,因此PTX代码不能很好的表示实际运行的机器指令。在大多数情况下,我们发现写出CUDA代码后使用decuda工具把原始编译码流转换为机器指令是最高效的。decuda是针对Nvidia机器指令级别的反汇编器,它通过分析Nvidia的编译器输出而来,因为Nvidia官方没有开源它们的指令集。

三、测量手段

        3.1Microbenchmark 技术

        为了探索GT200的架构,我们创造了micro-benchmark来揭露我们希望测量的各种特性。我们的结论是通过分析microbenchmark的执行时间来得出的。在分析指令cache参数时,decuda工具用来报告代码规模和位置,这与我们分析编码代码的结果一致。我们还使用decuda工具检验CUDA编译器生成的原始指令序列,以及分析为了解决分支发散和汇聚而生成的代码。

       microbenchmark由GPU的kenel代码组成,一般形式包含一段将会在GPU某个电路上运行的代码片段以及该代码片段前后的时间统计代码(该代码片段通常是一个展开的循环,需要执行多个cycle)。一个benchmark kenel需要完整的运行两次,抛弃第一次运行结果,因为第一次运行时会可能受到cache miss的影响。在所有的测试中,都确保kernel的代码规模小于L1 指令cache大小(4KB)。时间统计通过读取clock register来获得(使用 clock())。读取的clock值先存储在register中,在kernel末尾才写入global memory,从而避免了缓慢的global memory访问对时间统计的干扰。

        在研究cache 层次结构时,我们观测到当memory的访问请求跨越两层存储器时(例如跨越L3 cache和片外memory)不同的TPC有着不同的latency。我们将测量结果对于全部的10个TPC进行了平均,并报告出了相关的变化规律。

        3.2 通过latency图推断cache设计参数

        Cache和TLB的大部分参数测试都是通过stride访问不同大小的数组、并画出其平均延迟来得到的。同样的分析技术也可以用于分析CPU的cache 参数。我们揭露了指令cache和share cache在层级结构上的差异性。

        图4展示了一个通过cache访问平均latency图来获得cache size、way size、line size的例子。

这个例子中假设cache是采用LRU替换策略、组相连并且没有prefetch机制。在图4中可以按照下面的方法来推断cache参数:

        1、只要数组大小能够装进cache内,访问latency将是常值(一般是2-5个cycle),因此可以推出cache size为384KB。

        2、一旦数组大小超过cache容量,随着数组逐渐溢出各个set(从385Byte-512Byte),latency逐渐跳跃增加(每次跳跃增加的值约1000cycle量级);触发cache跳跃增加的数组大小等于cache line的大小(32Byte)

        3、当所有set都溢出后(即数组大小超过16 cache line),平均latency将是趋于常值(因为相比cache miss,cache 命中的latency忽略不计,导致平均latency主要受cache miss的影响)。

        4、cache的相关性(3)可以通过cache size(384Byte)除以cache way size(32Byte)得出,注意way size就是所有set数量乘以cache line大小。实际上上述cache参数有以下的关系:

        cache_size = cache_sets × line_size × associativity

        知道其中三个就可以求出第四个。

         listing1和2我们针对memory测试的microbenchmark。对于每种array size和stride,都是一些列相关的memory读取操作,读取操作的地址需要提前计算出来存在另一个array中,以消除地址计算在测量时带来误差。stride应该选择比cache line小的值使得所有的latency跳跃都可以观测出来,但是太小的值又不利于区分各个不同的latency跳跃。

四、测试结果

         本部分详细的展示我们的测试过程以及测试结果。我们的测试从函数clock()开始,随后探索各种SM中的计算pipeline、分支差异性以及同步隔离。与此同时还分析了SM内外的cache 层级结构,memory搬移以及TLB。

 4.1 clock函数的开销和特性

        所有的时间测量都使用clock()函数,它返回一个每个cycle都在自增1的counter。clock()函数实际上将会编译成两条指令:一条从clock register的move指令以及一条左移一位的指令,表明实际上的clock counter是系统时钟的半频。一个clock()函数将花费28cycle。

         在图5中进行的实验可以看出clock register实际上是每个TPC一个。图中的点是在block开始执行和结束执行时调用clock()函数所返回的时间戳。可以看到在同一个TPC中run的block都返回了同样的时间戳值,因此可以推断它们共享同一个clock register。如果clock register是全局同步的,那么同一个kernel的所有block的开启时间应该大致相同。相反,如果每个SM独占一个clock register,同一个TPC中的block就无法得到同样的时间戳。

 4.2 计算pipeline

        每个SM包含了3种不同的执行单元(在图1和表1中已经画出来了):

        1、8个scalar processor(SP)可以执行单精度浮点、整型和逻辑运算指令。

        2、2个special function Unit(SFU)负责执行先验和数学相关功能,比如逆平方根、sine函数、cosine函数,以及单精度浮点乘法。

        3、1个double precision unit(DPU)处理64bit浮点操作。

        表3展示了当所有操作数都在register中时上述三种执行单元的latency和throughput。

        为了测量这些单元的pipeline的latency和throughput,我们的测试由一串完全没有数据依赖的操作组成。测量latency的时候,我们只run了一个thread。测量throughput的时候,我们run了512个thread(已经是每个block可以支持的最大值了),这样确保这些执行单元的满负荷工作。表3和表4每条指令由对应的哪个执行单元执行,以及我们观测出的latency和throughput数据。

         表3展示了在同一个device上的单精度、双精度浮点multiplication、multiply-and-add(mad)的运行情况。比较奇怪的是32bit integer类型multiplication需要96个cycle来计算,这是因为它实际上是转化为4条原生指令来实现的。而32bit的mad则转换为5条原生指令共需要花费120个cycle。硬件上只支持24bit integer类型multiplication,对应的intrinsic是__mul24()。

        对于32bit integer和双精度浮点除法操作是通过子程序调用来实现的,因此它们的latency很高且throughput很低。而单精度浮点除法则是通过短内联的intrinsic序列来实现,latency小了许多。

        单精度浮点multiplication的throughput测量结果是11.2 ops/clock。这已经超过了一个SM中的全部SP数量(8个),因此可以推断multiplication运算命令同时发给了SP和SFU。进一步推断每个SFU可以在每个cycle输入约2个multiplication(共2个SFU完成输入4个multiplication操作),部署在SFU上的其他更为复杂的指令同样获得了2倍的throughput。但是单精度浮点mad指令的throughput仅为7.9 ops/clock,意味着mad操作并不能部署在SFU上。

        Decuda工具显示__sinf(),__cosf(),以及__exp2f()这些intrinsic都通过转化为2条独立的intrinsic来实现的。官方的编程指引内指出这些超越函数(transcendental)操作都在SFU上实现,但是对这些超越函数的latency/thoughput测量结果却不能匹配上SFU上执行的更为简单的的intrinsic测量结果(比如log2f),因此这些超越函数暂且作为遗留问题。sqrt()对应转化为两条指令:一条reciprocal和一条reciprocal-sqrt。

        图6展示了部署到同一个SM中的warp数增加时,SP上执行的无数据依赖的指令(integer 加法)的latency和throughput变化情况。当同时运行的warp数量少于6时,观测到的latency为24个cycle。因为所有的warp的latency都一样,warp scheduler是完全公平分配的。这个过程中throughput线性增加,因为执行单元的pipeline并没有占满,在pipeline完全占满后,throughput就固定为8 ops/clock了(因为SP数量是8)。官方的编程指引中描述到“6组warp-即192个thread可以很高效的隐藏register因为RAW数据依赖带来的latency”,然而6或7组warp并不能让warp scheduler做到SM中的pipeline满负荷工作。

 4.3 控制流

         1、分支差异性

        同一个warp中的所有thread都执行同一条指令。官方的编程指引中指出,当同一个warp中的thread之间由于与数据相关的条件分支导致执行路径不同时,该组warp将串行的执行每一个分支路径,不在该路径上的那些thread全部disable掉。我们观测到的现象与预期一致。图7是2组同时执行的warp的执行情况观测图,每组warp中的每个thread都利用自身的thread ID来选择运算操作,共有32条不同的执行路径。从图中可以看出,同一个warp中的不同分支是完全串行执行的,但是不同warp之间的执行则可以重叠。在同一个warp内部,选择相同路径的那些thread则可以同时执行。

        2、重聚合

        每当一条分支路径执行完后,thread都会汇聚到同一个位置。Decuda工具显示了complier会给每个可能的分支之前插入一条指令,用来提供硬件上的重汇聚点。Decuda工具同样展示了重汇聚点上的指令在编码上有一些区域进行了特殊标记。我们观测到在thread出现分支时,每条路径都被重汇聚点串联了起来执行。只有在上一条执行路径通过了重汇聚点,下一条路径才会开始执行。

        根据Lindholm在文章【6】中的描述,存在一个“分支同步堆栈”来管理那些出现分支和重汇聚的thread。我们用listing 3中的kenel来确认了这个描述。其中array C是一个包含了0-31数字的排列数据,通过不同的数字来实现不同的执行命令。我们观测到当一个warp到达一个条件分支时,它执行的路径总是这样的:对于每个if-elset条件判断else分支总是先执行,所以对应到listing 3最后的then (else if (tid == c[31]))执行路径总是先执行,而第一个then(if (tid == c[0] ))总是最后执行。

         图8展示了当c里面包含的数据是一个递增的序列({0,1,2……31})时上面这个kernel的执行时间轴。再这样的情况下,thread 31总是最先执行。当c包含的数据是递减的序列({31,30,……0})时,thread 0则是最先执行,可以看出thread ID并不影响执行顺序。观测到的执行顺序总是确定的,因为具体哪条路径应当先执行,哪条路径应当入栈保护的顺序是确定的。

         3、SIMT导致的序列化影响

        官方的编程指引内指出,为了保证编程正确性,编程人员可以忽略SIMT的行为。在本节中,我们通过展示了一个例子来说明如果thread是完全独立的就可以正常执行,但是因为SIMT的行为导致了死锁。在listing 4中,如果thread是相互独立的,第一个thread可以正常跳出while随后给sharedvar加1。这将导致后续的thread做出同样的操作:跳出while循环并给sharedvar加1(译者:这句话不太理解)。但是在SIMT中,当thread 0跳出循环时认为是发生了分支差异。complier将sharedvar++之前标记为重汇聚点。当thread 0达到重汇聚点时,其他路径才可以执行。thread 0必须等待其他的thread也执行完成后才可以进行sharedvar加1的操作。如果其他thread不能跳出循环来达到重聚合点,死锁就发生了。

未完待续。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值