在计算能力方面,之前我们曾提到过Midgard可以支持64位计算,猜猜看它是如何实现的呢?实际上Midgard是通过一个完整的128位的SIMD来分解操作较小位宽的计算。比如一个128位的SIMD可以分解成2个64位操作,也可以分解成4个32位操作甚至8个16位操作。这样的设计一方面增加了灵活性,另一方面使得尽可能多的相同操作可以填充SIMD流水线,提高了效率。
一般来说,使用SIMD或者类似SIMD设计的GPU还是比较多的,但是设计得如此灵活的SIMD架构非常少见。其它体系架构师都在强调效率和强调灵活之间做出权衡:当灵活性很高的时候,效率肯定有所损失,反之亦然。不过ARM采用这样的设计,肯定是考虑到需要满足所有的计算需求,因此才设计了这样一个128bit的灵活SIMD。
在计算能力方面,一个完整的Midgard计算单元每时钟能够输出17FLOPS FP32的性能,它包含了4个矢量加、4个矢量乘法、1个标量加法、一个标量乘法、一个点积(7FLOPS)。除了上述内容外,每个架构还有一些SFU单元用于处理点积、微积分以及其他复杂的计算。这些特殊计算单元的计算能力往往不会被统计在FLOPS中。大多数架构统计FLOPS都是通过统计大量的MAD指令来完成。比如Tegra K1拥有192个FP32 ALU,每个ALU每周期可以执行2次MAD计算,因此它的总计算能力达到了384 FLOPS每周期。
除了常见的FP32外,FP64双精度计算也是Midgard的特性之一。不过相比FP32而言,Midgard的FP64能力要低得多,大约只有每周期每核心5FPLOS。从乐观的角度来说,Midgard的FP64性能中的4FLOPS来自于矢量单元(2个FP64 MAD指令),而剩余的1个FLOPS来自于标量单元—如果假定是正确的话,标量单元每周期基本上难以完成一个FP64 MAD指令。不过基于FP64的性能虽然仅仅是FP32的一半,但这对桌面GPU来说也是非常高的水平,因此本文的估计很可能还是过于乐观了,在这方面ARM没有公布更多的资料,所以我们的猜测也只能到此为止。
作为一个单指令多数据流的VLIW GPU、或者是ARM官方的连续超长指令集(Sequential Long Instruction Word)架构来说,在执行阶段为了提升效率,编译器应该会编译出尽可能符合硬件架构的指令来试图塞满整个计算管线。对VLIW来说这需要一定程度的指令级并行调整数据,并查找可以放在一起的SIMD操作,终填补Midgard架构中可能存在的任何空闲操作。在没有超标量体系结构的情况下,充分利用Midgard的VLIW架构的办法就是:将几个合适的指令捆绑成一个,直接塞入Midgard的架构中。从目前使用在ARM SoC上的GPU核心设计也能看出,诸如英伟达的Kepler以及IT的PowerVR,都或多或少的引入了指令级并行的一部分内容,这两者也同样使用了超标量架构(虽然和Midgard还是有很大差异)。
历史上还有其他的一些公司大规模使用VLIW,比如AMD。AMD在桌面的HD 2000一直到HD 6000系列GPU都使用了VLIW架构,直到2011年在GCN架构上,AMD才放弃了VLIW。当时AMD遇到的难题是研究人员发现应用程序在GPU上工作是往往无法达到VLIW的设计吞吐能力,甚至这样的状况变得越来越不理想。AMD终使用了全新的GCN架构,彻底抛弃了指令级并行,转投线程级并行的怀抱。
继续回到Midgard上来。实际上,对一个以移动SoC为目标的GPU来说,怎样设计合适的规模和架构以适应移动SoC的需求才是重要的。目前看来,Midgard采用的VLIW架构相对来说是比较稳妥和合适的,因为对Midgard来说,它拥有128bit的矢量处理能力,可以映射为ARGB计算,也就是每个SIMD处理一个颜色通道,标量单元刚好可以辅助处理那些不太好处理的特殊计算部分。因此,虽然从GPU架构发展来看,线程级并行是大趋势,但是在当前也不能说指令级并行就没意义了。
在确定了Midgard使用指令级并行后,就需要关心一下它的ALU流水线设计了。为了满足目前的VLIW方案并尽可能提高效率,整个Midgard的流水线管道有128级,算术管道部分有30级深,不过Midgard的三管线设计每个管线都有自己不同的深度。从GPU的角度来看,Midgard简直就是一个极深的、高延迟、大吞吐量、交织着大量线程以保持流水线效率的GPU。在任何情况下,ARM都允许Midgard通过绕过一些错误,比如失败的读取或者写入等来避免等待,同时会重启这些指令,而不是令其堵塞在流水线管道。
AMD在从指令级并行转换至线程级并行是在2011年的GCN架构上完成的。
还有一些其它消息更令人感兴趣,那就是Midgard完全没有全局的线程级并行性设计。所有目前的主流GPU,无论来自英伟达、AMD还是英特尔,无论是否依赖于指令级并行,都至少和线程级并行相关。在这些GPU的设计中,很多线程被捆绑在一起直接发送至ALU单元。一般来说这些被捆绑的每个线程都代表了一个像素,只是由于空间位置存在一定的相关性,因此它们有几乎相同的指令算法,一堆类似的线程可以同时发送操作而不需要一个个计算。因此,我们在目前的GPU设计中看到了诸如波次这样的概念,比如AMD GCN是16个线程每波次,而英伟达在Kepler中是32个。
但是,这样的情况在Midgard这里并不存在。从上一代架构开始一直到Midgard,它们的GPU中每一个线程都是独立的,似乎和线程的并行处理无关。而做出这样的设计,是因为Midgard的每个算术管道都是自己的“CPU”,它们可以对线程进行独立的处理和计算。即使是诸如Mali-T760这样只有两组运算单元的GPU,这两组运算单元也会彼此分开独立工作。如果扩大的更大一些,比如T760MP16,它拥有32个计算单元,依旧是彼此独立工作。
对Midgard这样的设计,其实可以分为两个方面来看。首先,Midgard的设计非常正确,它使用了一种特殊的方法解决了没有线程级并行的缺陷。一般来说,在图形处理中,线程级并行由于可以很方便的得知像素的空间位置,计算中会捆绑一些线程,比如16个或32个每个波次,带来了效率的提升。但是,以线程级并行为核心的设计也有自己的缺点。比如需要一些分支操作时,由于无法绑定,因此线程级并行的效率会降低。此外,如果一次操作无法满足一个波次的需求,效率也会降低。
总的来说,目前Midgard的设计相当的独特,ARM在GPU的设计上展示了一种完全不同于目前所有既定思维的方案。当然,这样的设计终是否成功,不但有硬件和架构方面的原因,还有软件方面的因素,比如Midgard就更为适合分支更多的程序。
所谓指令级并行,是指目前所执行的一组指令之间相互独立,不互相等待结果、不互相访问同样的内存单元、不互相使用计算单元或功能部件,它们在处理器内部并行地执行。指令级并行的方法和传统的提高处理器计算能力的方法相关。如果你想提高一个处理器的处理能力,那么好的办法是给这个处理器中加入大量的高效率、专用的处理模块,这样无论有怎样的任务处理器都可以见招拆招。
指令级并行在很长一段时间内都是处理器提高性能的不二法门。不过它的问题在于处理器效率不够高。例如在理想情况下,VLIW的效率应该是100%—这要求每次的数据都恰好满足“ARGB+特殊”这样的形式,但是在很多情况下,某次数据来了“A”,某次数据是“RGB”,某次数据是“GB+特殊”,也就是说实际情况下绝大部分指令都并不符合“ARGB+特殊”这样的设计需求。为了解决这个问题,人们设计了比如超标量、多级缓存、预测执行等手段来提升并行度,此外,加入了更深的流水线来细分指令,力求使得指令级并行能够提升效率。
随着计算机技术的发展,指令级并行规模大、效率低的弊端已经越来越明显,传统的指令级并行很难适应现代计算模型中复杂多变的、不规则的计算特性。线程级并行恰好可以用于进一步增大整个处理器的吞吐能力。举例来说,线程级并行中的流水线在当前任务上如果遇到无法解决的等待或者延迟,可以迅速切换到新的工作,线程级并行中的执行也没有了固定的排序方式,除了部分特殊计算外,其余指令都会面对几乎相同的计算单元。以AMD的GPU为例,新的GCN架构就是典型的线程级并行,它的CU单元拥有4组、每组16个ALU单元,由于取消了所谓的固定任务格式,GPU只需要根据需求将所有内容直接塞入CU单元即可,大大提升了效率。总的来说,目前桌面级别的处理器,都在向线程级并行转进,在移动SoC的GPU上,受制于功耗等原因,可能还存在一些指令级并行设计。毕竟目前移动SoC无论是性能还是特性水平依旧远远落后于桌面产品。在恰当的时候选择合适的架构,也是非常重要的。
GPU中的波次,图中展示了随着时间不同波次的计算任务被依次填充进入计算单元。
Midgard的每个计算单元,似乎都存在自己的线程判断和执行单元。
Midgard支持前后帧之间的图像比较,如果有部分没有改变,那么Midgard就会跳过这些部分,同时可以节省大量带宽和资源。