发表时间: 2017-04
文章标题:面向现代GPU的局部性感知CTA聚类
作者/机构:
一句话结论 本文提出了一种纯软件的 CTA 聚类(CTA-Clustering)技术,通过绕开不可控的硬件调度器将具有数据重用的线程块绑定到同一个流式多处理器上,在多代 GPU 上显著减少了 L2 缓存事务并实现了平均 1.4 倍以上的性能加速。
要解决什么问题 现代 GPU 程序的全局数据重用中有 45% 发生在不同的协作线程阵列(CTA,即线程块)之间,但这种 CTA 间局部性长期被浪费。原有做法卡在 GPU 底层的 CTA 调度器(GigaThread 引擎)完全由硬件控制,对软件既不可见也无法修改。硬件通常采用轮询或随机策略将 CTA 分发给各个流式多处理器(SM),导致原本访问同一块数据的 CTA 被打散到不同的 SM 上,或者在时间上错开执行。由于 GPU 核心内的 L1 缓存容量极小,且采用写驱逐策略,这种无序调度会引发严重的缓存颠簸,导致 L1 命中率极低,迫使 SM 频繁向延迟极高且带宽有限的 L2 缓存和全局内存发起请求。如果不能在机制上接管调度权,把具有潜在重用关系的 CTA 强行聚集到同一个 SM 上并发或连续执行,就无法利用这部分巨大的局部性收益。
怎么做的 作者提出了 CTA 聚类技术,核心思路是废弃默认的内核启动方式,通过纯软件的代码转换构建一个映射机制,欺骗或完全规避硬件调度器,强制有局部性的 CTA 在同一个 SM 上执行。关键设计由分区、反转和绑定三个步骤构成。首先是分区(Partitioning),负责将原始内核中的所有 CTA 划分为 $M$ 个平衡的簇,使得簇内保留最多的 CTA 间局部性。作者通过分析数组引用对网格维度的依赖性来决定是按 X 还是 Y 维度划分,其启发式分区函数定义为:$$f(v) = \left( \lfloor v/M \rfloor - \left( \lfloor (v\%M)/M \rfloor * (\lfloor |V|/M \rfloor + 1) + \lfloor v/M \rfloor * \lfloor |V|/M \rfloor \right), v\%M \right)$$ 其中 $v$ 是原始 CTA ID,$|V|$ 是总 CTA 数,$M$ 是簇的数量,输出结果为簇 ID $i$ 和该 CTA 在簇内的位置 $w$。其次是反转(Inverting),负责在已知簇 ID 和簇内位置时,反向推导出原始的 CTA ID,其反转函数定义为:$$v = f^{-1}((w, i)) = i * (\lfloor |V|/M \rfloor + 1) + w + \min(i - (|V|\%M), 0)$$ 最后是绑定(Binding),负责将实际执行的计算资源与划分好的簇对应起来。为了彻底绕开黑盒的硬件调度器,作者设计了基于代理的聚类(Agent-based Clustering)。该机制在每个 SM 上只启动少量在内核执行期间持久驻留的 CTA(称为代理),代理通过读取特殊寄存器 `%smid` 获取当前物理 SM 的 ID,从而知道自己该负责哪个簇。代理内部运行一个任务循环,不断获取簇内位置 $w$,结合所在簇 $i$,利用反转函数计算出原始 CTA ID $v$,进而执行原始内核的计算逻辑。作为补充,该方法还引入了 CTA 节流机制,通过限制每个 SM 上活跃的代理数量来缓解缓存争用,并配合缓存旁路和重塑顺序后的 CTA 预取来进一步掩盖内存延迟。
效果如何 实验在 Fermi、Kepler、Maxwell 和 Pascal 四代 NVIDIA GPU 上搭建,测试了来自 Rodinia、Parboil 等基准测试集的 23 个应用。对比基线包括默认硬件调度的基线(BSL)、基于轮询假设的重定向聚类(RD)、基于代理的聚类(CLU)、结合 CTA 节流的代理聚类(CLU+TOT)、缓存旁路(BPS)以及 CTA 预取(PFH)。量化结果显示,对于具有算法相关局部性的应用,结合节流的代理聚类在四代架构上分别实现了平均 1.46 倍、1.48 倍、1.45 倍和 1.42 倍的性能加速,最高加速达 3.83 倍,L2 缓存事务数分别锐减了 55%、65%、29% 和 28%。该方法存在明确的适用边界:它对算法相关和缓存行相关的应用极为有效,但对数据相关、写相关或纯流式应用几乎没有收益。此外,架构差异会带来显著的代价波动,在较新的 Maxwell 和 Pascal 架构上,由于 L1 缓存行较小(32B)且被划分为多个扇区,阻碍了跨扇区的数据重用,加上动态 warp 绑定机制引入了额外的原子操作和同步开销,其聚类效果略逊于拥有 128B 大缓存行的老架构。作者也承认,对于像矩阵乘法这种数据重用距离远超 L1 缓存容量、且每个 CTA 包含大量 warp 导致并发代理数受限的应用,时间与空间局部性均难以被有效捕获,性能提升十分有限。
本文探讨了现代GPU中一种长期被忽视但具有性能提升潜力的数据局部性——CTA间局部性(inter-CTA locality)。利用这种局部性面临三大挑战:硬件可行性不明确、底层的CTA调度器未知且无法访问、以及核心内缓存容量小。
为解决这些问题,本文做出了以下主要贡献:
总体架构。GPU处理器由多个称为流式多处理器(Streaming Multiprocessors, SMs)的SIMD核心组成。图1展示了现代NVIDIA GPU的通用架构图。通常,一个SM包含标量处理器、寄存器文件、特殊功能单元、加载/存储单元、共享内存和不同类型的缓存。表1列出了四种代表性产品的基本架构规格。Fermi和Kepler GPU的每个SM都有一个48KB可配置、128B缓存行、写驱逐(write-evict)的L1数据缓存,用于通用片外DRAM访问,并与共享内存共享同一片上存储。然而,近期的Maxwell和Pascal GPU将该存储完全用于共享内存,而依赖于纹理缓存(Texture cache,32B缓存行,非相干)提供L1缓存能力,称为L1/Tex统一缓存【28,NVIDIA. GTX980 Whitepaper: Featuring Maxwell, the Most Advanced GPU Ever Made, 2014】。同时,GPU中所有的SM通过一个片上网络(NoC)连接到一个共享的L2缓存。L2缓存是分块(banked)且可写的(写回和写分配【29,Inderpreet Singh, et al. Cache coherence for GPU architectures. 2013, HPCA】),缓存行大小为32B。传统上GPU不启用硬件缓存预取【30,Sara S. Baghsorkhi, et al. Efficient Performance Evaluation of Memory Hierarchy for Highly Multithreaded Graphics Processors. 2012, PPoPP】。但用户可以通过特定的编译器选项【7,NVIDIA. CUDA Programming Guide, 2015】启用/禁用L1缓存。值得注意的是,L1缓存行大小大于或等于L2,这对后续讨论很重要。
执行模型。GPU SM遵循单指令多线程(Single-Instruction-Multiple-Threads, SIMT)执行模型【6,Erik Lindholm, et al. NVIDIA Tesla: A unified graphics and computing architecture. 2008, IEEE Micro】。一组32个线程构成一个执行向量,称为warp。Warp是SM指令获取和解码的基本单位,一个warp内的所有线程以锁步方式前进。Warp在SM的warp插槽中注册(见表1)。多个warp构成一个称为线程块(thread block)或协作线程阵列(Cooperative-Thread-Array, CTA)的块,它封装了所有的线程同步和屏障操作。CTA是向SM交付作业的基本单位。从硬件角度看,CTA之间不应有依赖关系——一个内核在任意CTA调度/执行顺序下都应获得正确结果。这一特性确保了当CTA调度策略和/或SM架构被修改时,应用程序保持不变。这种“无序”特性对本文也至关重要:可以操纵CTA调度而不会危及一致性。
CTA调度。GPU上的默认CTA调度策略被认为是轮询(Round-Robin, RR)【11,Adwait Jog, et al. OWL: Cooperative Thread Array Aware Scheduling Techniques for Improving GPGPU Performance. 2013, ASPLOS】、【27,Minseok Lee, et al. Improving GPGPU resource utilization through alternative thread block scheduling. 2014, HPCA】、【31,Onur Kayıran, et al. Neither more nor less: optimizing thread-level parallelism for GPGPUs. 2013, PACT】、【32,Hyeran Jeon, et al. CTA-aware Prefetching for GPGPU. 2014, CENG-2014-08】、【33,Jin Wang, et al. LaPerm: Locality Aware Scheduler for Dynamic Parallelism on GPUs. 2016, ISCA】。首先,CTA调度器(即GigaThread引擎)为每个SM分配至少一个CTA。如果一个SM仍有足够的资源(如寄存器、共享内存、warp插槽等)来支持额外的CTA,则会进行第二轮分配。这个轮询分配过程会重复进行,直到所有SM都饱和,要么受资源限制,要么受硬件限制【32,Hyeran Jeon, et al. CTA-aware Prefetching for GPGPU. 2014, CENG-2014-08】。之后,每当一个现有的CTA完成退出,一个新的CTA就会被分配给一个SM。需要注意的是,CTA调度完全由硬件实现的GigaThread引擎管理,没有任何明确的软件策略可以影响默认的CTA调度,也无法修改CTA如何分派到SM。此外,一旦一个CTA被分配给一个SM,它就不能被抢占或重新分配到另一个SM【7,NVIDIA. CUDA Programming Guide, 2015】。这种对CTA调度器缺乏控制,为利用调度提升GPU性能带来了主要障碍【25,Bo Wu, et al. Enabling and exploiting flexible task assignment on GPU through SM-centric program transformations. 2015, ICS】、【26,Kunal Gupta, et al. A study of persistent threads style GPU programming for GPGPU workloads. 2012, InPar】。最后,RR调度终究是一个假设,因为GigaThread引擎中的实际调度算法从未被公开过【25,Bo Wu, et al. Enabling and exploiting flexible task assignment on GPU through SM-centric program transformations. 2015, ICS】。
设计微基准测试验证CTA间局部性。我们设计了一个微基准测试(如清单3所示),以验证GPU L1缓存是否有能力利用被分派到同一个SM上的CTA之间的数据重用,无论是同时执行(空间局部性)还是相继执行(时间局部性)。评估的GPU平台配置列于表1,包括Fermi、Kepler、Maxwell和Pascal。如代码18-21行所述,我们分别为这四个平台启动了480、960、1024、1280个CTA,对应每个SM 4、4、2、2个轮次(turnaround)。每个CTA包含一个warp,且基本上只利用主线程以避免CTA边界内的warp内合并和warp间冲突效应。由于每个CTA只有一个warp,一个SM的所有硬件CTA插槽(见表1)实际上都被占满。因此,在每个轮次中,一个SM可以在这四个平台上分别同时执行8、16、32、32个CTA。为了更准确地测量内存访问延迟,在Maxwell和Pascal上运行时,我们在第12行前后增加了两条同步指令,因为在Maxwell之后,计时指令(第10和14行)和内存访问指令(第12行)可以同时发出,导致计时结果倾斜。CTA屏障(_syncthreads())用于禁止多发射。
实验观察与分析。图2显示了在两种设置下,分派到容纳第一个CTA(即CTA-0)的特定SM上的CTA的平均内存访问延迟:(A)默认CTA调度,和(B)交错执行。我们将这个SM表示为“SM 0”。x轴代表分派到SM 0的CTA-ID。我们还使用CUDA分析器分析了另外两个缓存指标(即L1读取事务数和L1到L2读取事务数)。总结观察结果如下:
DELAY变量控制交错的程度,设置得足够长(例如1200个周期),以便前一个CTA请求的数据可以在同一轮次中其他CTA获取数据之前到达L1。这也使所有并发的CTA保持存活和活动状态。图2-(B)展示了同时启动的CTA之间的这种空间重用。例如,只有第一轮中的第一个CTA表现出比其余CTA长得多的延迟。
CTA间局部性的量化。我们量化了CTA间重用在总全局数据重用中所占的百分比,这个计算基于SM产生的所有内存请求在进入L1缓存之前的数据重用情况。由于当前的GPU分析器无法追踪特定内存请求的地址和来源(如CTA-ID),我们仅为演示目的,使用GPGPU-Sim【21,Ali Bakhoda, et al. Analyzing CUDA workloads using a detailed GPU simulator. 2009, ISPASS】来追踪所有内存访问请求的数据重用,并估算CTA间重用在整体数据重用中的百分比。需要注意的是,这个估算是数据驱动的,与缓存设计或CTA调度策略无关。每次运行的结果都是一致的。图3量化了33个常见GPU应用程序的CTA间重用情况,清楚地表明这些应用程序中的CTA间重用占整体数据重用的非常重要的部分(平均为45%)。这表明旨在改善CTA间重用的策略可能会带来显著的整体性能效益,超越了仅试图促进warp内和warp间数据重用的传统方法【16,R. Ausavarungnirun, et al. Exploiting Inter-Warp Heterogeneity to Improve GPGPU Performance. 2015, PACT】、【34,Veynu Narasiman, et al. Improving GPU performance via large warps and two-level warp scheduling. 2011, MICRO】、【35,A. Sethia, et al. Mascar: Speeding up GPU warps by reducing memory pitstops. 2015, HPCA】、【36,Adwait Jog, et al. Orchestrated Scheduling and Prefetching for GPGPUs. 2013, ISCA】。
CTA间局部性的来源分类。然而,这些显著的CTA间局部性是否能在GPU硬件上被利用以提升性能?为了回答这个问题,我们需要理解CTA间局部性的来源。通过对广泛的GPU应用程序(详见表2)进行特征分析,我们将GPU CTA间局部性的来源分为以下五个类别,其模式如图4所示:
a[i + 1]的缓存行在另一个CTA写入a[i]时被驱逐),就会发生这种情况。NW是一个典型的写相关应用。
基于可利用性的两种优化策略。如前所述,一些应用程序(如算法相关的)具有丰富的CTA间局部性,而另一些(如流式)则没有。根据应用程序是否具有可利用的CTA间局部性,我们提出了一种操纵GPU上CTA调度的软件方法,即CTA聚类(CTA-Clustering)。我们将应用程序是否具有“可利用的”CTA间局部性定义如下:
需要注意的是,对于某些具有非常特定运行时访问模式的数据相关应用,它们在内存中的数据组织可能可以在运行时之前预测【37,Lingda Li, et al. A Graph-based Model for GPU Caching Problems. 2016, arXiv:1605.02043】,从而使其CTA间局部性变得可利用。例如,先前的工作【38,Eddy Z. Zhang, et al. On-the-fly elimination of dynamic irregularities for gpu computing. 2011, ASPLOS】、【39,Jianqiao Liu, et al. Hybrid CPU-GPU scheduling and execution of tree traversals. 2016, ICS】建议在运行时前使用一个轻量级的检查器内核(inspector kernel)来分析某些图处理应用(如BFS的前几层)的局部数据访问,以预测全局数据组织,从而优化运行时访问。图5概述了在GPU上利用CTA间局部性的总体流程。对于具有可利用CTA间局部性的应用程序,我们应用CTA聚类来最大化它们在L1上的CTA间数据重用(第4.2节和4.3-(I)(II)节)。对于没有可利用CTA间局部性的应用程序,我们首先应用CTA聚类来重塑默认的CTA调度模式(不是为了利用CTA间局部性,而是为了施加一个特定的CTA执行顺序),然后使用CTA预取(CTA-Prefetching)在当前CTA结束前预加载后续CTA所需的数据(第4.3-(III)节)。
CTA聚类的核心思想与步骤。CTA聚类的基本思想是启动一个新内核来替换原始内核,从而建立一个预定义的聚类规则。目标是将具有CTA间局部性的CTA聚集在一起,并在同一个SM上并发或连续执行。我们使用O、N、C分别表示原始内核、新内核和簇。CTA聚类本质上是找到一个映射:$N \rightarrow O$,该映射受制于C,如图6所示。这个过程包括三个步骤:
f定义。g定义。简而言之,寻找映射 $N \rightarrow O$ 的过程如下:
以矩阵乘法(MM)为例。为了清晰地展示理论,我们使用一个著名的应用——来自CUDA SDK【40,NVIDIA. CUDA SDK Code Samples, 2015】的矩阵乘法(MM)来展示聚类过程。根据MM的源代码,其CTA内数据重用完全由共享内存处理(即,矩阵A和B的局部缓冲区在共享内存中声明,用于线程间的CTA内数据共享)——CTA间局部性没有被显式探索。然而,如图8-(A)所示,MM固有地具有算法相关的CTA间局部性,这可能被利用来提升性能。由于篇幅限制,我们不在此展示MM的源代码,本节中将称其为“MM内核体”。
分区问题的形式化定义与启发式解法。分区问题的形式化定义是:给定一个无向图 $G(V, E_\mu)$,其中每个统一的顶点代表一个CTA,每条加权边代表度为 $\mu$ 的CTA间重用,将该图划分为M个平衡的簇,使得簇内边的权重总和最大化。我们将 $y_{v,i} = 1$ 定义为顶点v被分配到簇i($i \in [M]$);否则 $y_{v,i} = 0$。因此,我们可以将簇i定义为:$C_i = [(v, e)|v \in V, y_{v,i} = 1]$。分区问题可以表述为:对于所有顶点 $v \in V$,$\sum_{i=1}^{M} y_{v,i} = 1$(一个顶点分配到一个簇);对于所有簇 $i \in [M]$,$\sum_{v \in V} y_{v,i} = |V|/M$(簇是平衡的)。一般的平衡图划分是NP完全问题【41,Konstantin Andreev and Harald Racke. Balanced graph partitioning. 2006, Theory of Computing Systems】。然而,应用开发者可以根据他们对算法的了解生成一个期望的划分函数。在这项工作中,我们提供一个启发式解决方案,作为自动化框架(第4.4节)的一部分来确保聚类的平衡性。我们将划分函数定义为:
其中v是O中的CTA ID,w是$C_i$中的ID。例如,如果一个簇包含3个CTA,则$w \in [0, 1, 2]$。由于V定义了O中CTA的顺序,而$|V|$是O中CTA的数量,我们的解决方案是通过以下方式将$|V|$分成M个块:
然而,$|V|$不一定是M的倍数。为了尽可能平衡地分配这些块,我们将公式3扩展为一个条件方程:如果 $v\%M \le |V|\%M$,则:
否则:
CTA索引方法与分区策略。与传统观念【27,Minseok Lee, et al. Improving GPGPU resource utilization through alternative thread block scheduling. 2014, HPCA】、【36,Adwait Jog, et al. Orchestrated Scheduling and Prefetching for GPGPUs. 2013, ISCA】不同,我们的划分方法不只是简单地将CTA分成连续的块,因为顶点v的顺序完全由CTA在O中的索引方式决定。图7展示了2D网格的四种主要CTA索引方法。对于默认的行主序索引,如果使用CUDA术语,v = blockIdx.y * gridDim.x + blockIdx.x,它会将连续的或行相邻的CTA聚类在一起,即沿着Y维度进行划分(Y-partitioning)。如果采用列主序索引(即v = blockIdx.x * gridDim.y + blockIdx.y),CTA将沿着X维度进行划分(X-partitioning)。此外,基于瓦片(Tile-based)的2D索引可以同时沿着X和Y维度进行划分,但可能因复杂的索引计算而产生更高的开销(第5.2节)。最后,也可以通过选择自定义索引方法来聚类任意的CTA。现在回顾第4.1节,我们描述了两种具有可利用CTA间局部性的应用程序的划分方法:
blockIdx.x)是数组引用的最后一个或唯一的维度(如$A[\alpha(by) + bx + \epsilon(tx,ty)]$或$A[\beta(bx)]$),这表明它可能在X方向上具有CTA间局部性,我们则使用行主序索引进行Y-partitioning。否则,如果基于Y的变量(如blockIdx.y)是数组引用的最后一个或唯一的维度(如$A[\alpha(bx) + by + \epsilon(tx,ty)]$或$A[\beta(by)]$),我们则使用列主序索引进行X-partitioning。3D内核的划分过程与2D类似,但选项更多。然而,大多数常见的GPU应用程序只包含2D内核网格。MM分区示例。如第4.1节所述,一些数据相关的应用如果用户提供了详细的自定义分区,也可以利用CTA间局部性,但这超出了本文的范围。以图8-(A)中的MM为例,A和B分别在S和T区域有CTA间重用。但是,是沿着Y(为了A的局部性)还是沿着X(为了B的局部性)划分CTA,取决于A的高度是否大于B的宽度(即方向性局部性强度)。在这种情况下,我们以A为目标,使用Y-partitioning(即行主序索引)。通过已知参数:M=2,网格宽度=3,网格高度=2,如图8-(A)所示,我们有$|V|=6$和$|V|\%M = 0$,这总是$\le v\%M$。因此,我们可以使用公式5来获得划分函数$f(v)$,并用它来定位O中任意CTA v应该被分派到哪个簇。例如,CTA-(0,1)可以被定位为簇C1中的第0个元素,通过计算$v = 1 * 3 + 0 = 3$,然后$f(3)=(3/2, 3\%2)=(1,1)$,再通过公式5计算w:$w = 3/2 - (1 * (6/2) + min(3\%2 - 1, 0)) = 1 - (3+0) = -2$ (原文此处似乎有误,应为 $w=\lfloor v/M \rfloor - (\lfloor (v\%M)/M \rfloor * (\lfloor |V|/M \rfloor + 1) + \lfloor v/M \rfloor * \lfloor |V|/M \rfloor ...)$,按论文公式5计算 $w = \lfloor 3/2 \rfloor - (1 * \lfloor 6/2 \rfloor + \min(3\%2 - 1, 0)) = 1 - (3+0) = -2$,这显然是错的,原文公式可能存在印刷错误或者我理解有偏差。根据意图,v=3,M=2,应该在cluster 1,w为1。我们遵循论文的意图,即CTA-(0,1) v=3,应映射到(w=1, i=1)。)
反转函数的推导。由于公式3是一个一对一的映射函数,我们可以得到其反函数为:
类似地,公式4和5的反函数为:
这两个方程可以统一为:
MM反转示例。给定一对(w, i)(一个簇i和该簇中的位置w),它对应的CTA ID v可以通过公式7在O中定位。如果索引方法也已知,我们可以进一步获得它在O中的坐标。对于MM,一个在C1中标记为(w=2, i=1)的CTA可以通过以下方式找到其在O中对应的v:$v = f^{-1}((2, 1)) = 1 * (\lfloor6/2\rfloor + 0) + 2 + \min(1 - 0, 0) = 1*3+2+0=5$ (此处原文公式应用似乎也有问题,根据意图(w=2,i=1)应对应v=5,即坐标(1,2),公式8: $v=1*(\lfloor 6/2 \rfloor+0)+2+min(1-0,0) = 5$,此计算正确)
绑定问题的定义与两种绑定方法。绑定问题的形式化定义是:给定新内核N,如何将N中的CTA与簇C关联起来,以便C中的所有项都能被完整而精确地执行?此步骤的目标是找到从$N \rightarrow C$的映射,或者本质上是$N \rightarrow (w, i)$。换句话说,给定新内核N中的任意CTA u,如何获知它负责的目标任务(w, i)。由于GigaThread引擎是未知且不可访问的,根据在N的上下文中如何获得(w, i),我们提出了两种方法来欺骗或规避硬件CTA调度器:
在MM中,对于一个ID为4的N中的CTA,其关联的(w, i)可以通过公式8计算:$(w,i)=(4/2, 4\%2)=(2,0)$。
asm("mov.u32 %0,%%smid;":"=r"(sm_id));。为了识别其在簇中的位置w,一个CTA必须与同一个SM上的其他CTA同步以避免冲突(即两个CTA获得相同的w)。我们发现在Fermi和Kepler上,CTA绑定到硬件warp插槽的方式基本上是连续和固定的。因此,一个CTA可以根据其硬件warp插槽ID除以WARPS_PER_CTA来决定其在簇中的位置w,如清单5的第5-6行所示。然而,在Maxwell和Pascal上,来自不同CTA的warp是动态绑定到硬件warp插槽的【42,Ang Li, et al. SFU-Driven Transparent Approximation Acceleration on GPUs. 2016, ICS】。因此,我们依赖于一个全局原子操作和共享内存广播(清单5的第16-19行)。两种CTA聚类方法的提出。结合所有三个步骤,我们基于两种绑定方案提出了两种CTA聚类方法:
MAX_AGENTS),以占据CTA插槽,从而强制代理均衡分布,但在运行时只激活其中的一部分(图10中的ACTIVE_AGENTS),这取决于其代理ID(清单5第7和20行)。通过这种方式,我们可以控制节流程度,同时保证代理均匀分布到SM。此外,我们利用__launch_bounds__【7,NVIDIA. CUDA Programming Guide, 2015】在CTA数量少于允许数量时,在编译期间增加寄存器使用,从而利用寄存器重用并隐藏指令延迟。注意,节流并非对所有应用程序都是必需的,它仅在聚类后性能未改善甚至下降时应用(即减少容量未命中)。为了在运行时决定活动代理的数量,我们参考了一个类似于【12,Ang Li, et al. Adaptive and transparent cache bypassing for GPUs. 2015, SC】中使用的动态CTA投票方案。框架流程与局部性来源估计。图11展示了我们基于软件的利用CTA间局部性的优化框架。前面的小节已经解释了,一旦我们对目标内核有了某些了解(例如,它是否具有可利用的CTA间局部性),如何进行相应的优化。本节主要讨论框架如何估计应用程序的CTA间局部性来源。这个过程在图11中以蓝色突出显示。该框架应用几种简单的粗粒度技术来估计应用程序的CTA间局部性来源,然后才对其进行进一步的优化分析。由于CTA内局部性通常在CTA范围内捕获(例如,通过共享内存),改变CTA调度只会影响CTA间局部性。因此,我们首先应用简单的基于重定向的聚类(图9)来施加一个新的CTA执行顺序(X-或Y-聚类),并检查性能和/或L1命中率是否发生变化(这是CTA间局部性潜力的一个指标)。潜力较高的内核(例如,L1命中率有显著变化)可能属于算法相关或缓存行相关。注意,为了这次验证,最好减小问题规模(总CTA数),因为每个SM的CTA数量过多会引发严重的颠簸,通常使L1命中率接近于零。此外,我们可以打开/关闭L1缓存,看是否有任何性能变化。如果关闭L1后L2缓存访问显著减少,这很可能是由大的L1缓存行引起的,暗示了缓存行相关。如果L2事务没有变化,但CUDA分析器报告的合并度很高,这个内核可能是流式的。然而,如果合并度低,内存访问行为可能更随机,表明是数据相关的。最后,框架评估内核内部对数组的引用。如果内核读写同一个数组,但引用有移位或偏斜,那么写的结果很可能稍后会被其他CTA重用,表明是写相关的。
性能提升:如图12所示,对于算法相关的应用,CTA聚类及其相关优化在Fermi、Kepler、Maxwell和Pascal GPU上分别取得了平均1.46倍、1.48倍、1.45倍和1.42倍的性能加速,最高分别达到3.83倍、3.63倍、3.1倍和3.32倍。对于缓存行相关的应用,在L1缓存行较大的Fermi和Kepler GPU上,也分别取得了平均1.47倍和1.29倍的加速,最高达到2.57倍和1.75倍。
缓存性能改善:如图13所示,该技术显著改善了缓存性能。对于算法相关的应用,在四种架构上L2事务数分别减少了55%、65%、29%和28%。对于缓存行相关的应用,在Fermi、Kepler和Maxwell上,L2缓存事务数分别减少了81%、71%和34%。
两种聚类方法对比:基于重定向的聚类(RD)由于依赖于不准确的RR调度假设,通用性不强,尽管对某些应用(如NN, IMD)有效。相比之下,基于代理的技术(CLU, CLU+TOT)效果更好,尤其是在Fermi和Kepler上,其静态CTA到warp-slot的绑定使得基于SM的绑定成本更低。
跨架构效果分析:CTA聚类对算法相关的应用更有效,因为其CTA间重用是算法固有的。对于缓存行相关的应用,该技术主要对Fermi和Kepler有效,因为它们的L1缓存行尺寸(128B)远大于Maxwell和Pascal(32B),大缓存行有利于跨CTA边界的空间重用。
补充优化效果分析:
节流的必要性:多数算法相关应用(KMN除外)无需节流即可从聚类中直接获益。仅通过控制共享内存来节流的传统方法有时性能甚至更差,表明节流是聚类的补充,仅在容量冲突成为瓶颈时使用。
性能评估指标:对比图12和图13,证实了L2缓存事务数是比L1命中率更好的GPU性能评估指标。L2事务数下降通常对应性能提升。
MM应用的局限性分析:尽管MM被用作示例,但其性能提升不显著。原因有三:(1) CTA间数据重用距离远超L1缓存容量,导致时间局部性难以利用;(2) 每个CTA的warp数多(32个),限制了SM上并发代理的数量,使得空间局部性难以利用;(3) Maxwell和Pascal的L1/Tex缓存是分扇区的,阻碍了跨扇区的数据重用。
平台差异总结:CTA聚类在Fermi和Kepler上表现略好于Maxwell和Pascal,因为:(i) 前者有更大的L1缓存行,利于空间重用;(ii) 后者由于动态warp-slot映射,需要承担SM绑定带来的原子和同步开销;(iii) 后者的L1/Tex缓存分扇区,阻碍了代理间的数据重用。
本文提出了一种新颖的聚类技术,旨在发掘并利用一种长期被忽视的局部性——CTA间局部性。首先,通过实验证明了现有GPU硬件有能力在L1或L1/Tex统一缓存上利用空间和时间上的CTA间局部性。接着,通过量化其在广泛应用中的存在并分析其来源,验证了这种局部性的潜力。基于这些洞察,本文提出了CTA聚类的概念及其相关的软件技术。最后,在所有现代NVIDIA GPU架构上对这些技术进行了评估。实验结果表明,所提出的聚类技术能够显著提升片上缓存性能,从而带来可观的整体性能改进。
在论文的核心方法章节(第4节)中,引用了以下文献来支持其设计和论证:
__launch_bounds__这一CUDA特性,作者利用它在CTA数量较少时增加寄存器使用以优化性能。%smid)在运行时获取CTA所在的物理SM ID,这是作者提出的“基于SM的绑定”方法的关键技术基础。