Demystifying GPU Microarchitecture through Microbenchmarking
文章标题:通过微基准测试揭秘GPU微架构
作者/机构:Henry Wong, Misel-Myrto Papadopoulou, Maryam Sadooghi-Alvandi, and Andreas Moshovos (Department of Electrical and Computer Engineering, University of Toronto)
A1 主要贡献
本文旨在通过开发一套微基准测试程序,来测量和揭示Nvidia GT200 (GTX280) GPU中对CUDA可见的架构特性。由于制造商提供的文档信息有限且有时含糊不清,开发者、架构和编译器研究人员需要对现代GPU设计有更深入的理解。本文的研究重点是影响GPU性能的两个主要部分:算术处理核心和为这些核心提供指令与数据的内存层次结构。
核心问题与研究目标:
- 核心问题: 业界对GPU架构的了解主要依赖于制造商的文档(如NVIDIA的CUDA编程指南),但这些文档中的信息往往不够详尽或模糊,缺乏对底层硬件组织的深入解释。这给性能优化、死锁避免以及精确的性能建模带来了挑战。
- 研究目标: 通过微基准测试方法,精确测量Nvidia GT200 GPU的处理核心和内存层次结构的各种未公开特性,从而为性能优化、分析和建模提供详细、准确的硬件信息。
主要贡献:
* 验证官方性能特性: 验证了CUDA编程指南中列出的部分性能特性。
* 揭示控制流细节: 深入探究了分支分化(branch divergence)和屏障同步(barrier synchronization)的详细功能。发现了会导致死锁的非直观分支代码序列,并阐明了通过理解内部架构可以避免这些问题。
* 测量内存与缓存层次结构: 测量了内存缓存层次结构的组织和性能,包括翻译后备缓冲区(TLB)层次结构、常量内存(constant memory)、纹理内存(texture memory)和指令内存(instruction memory)的缓存。
* 分享测量技术: 详细介绍了所使用的测量技术,这些技术对于分析和建模其他GPU及类GPU系统,以及提高GPU性能建模与仿真的保真度具有参考价值【索引2, A. Bakhoda 等人, Analyzing CUDA Workloads Using a Detailed GPU Simulator, ISPASS 2009】。
GPU架构概览:
论文中展示了GT200的层级化硬件资源结构,线程块(Blocks of threads)在流式多处理器(Streaming Multiprocessors, SM)中执行。多个SM组成一个线程处理集群(Thread Processing Clusters, TPC),整个GPU则由多个TPC和内存系统构成。
A3 背景知识与测量方法
GPU架构与编程模型
-
GPU架构抽象 CUDA将GPU架构建模为一个多核系统,并将GPU的线程级并行性抽象为一个线程层级(网格(grids)包含线程块(blocks),线程块包含线程束(warps),线程束包含线程)【索引1, Nvidia, Compute Unified Device Architecture Programming Guide Version 2.0】。这些线程被映射到硬件资源的层级结构上。线程块在流式多处理器(SM,如图1)内执行。尽管编程模型使用标量线程的集合,但SM的实际工作方式更像一个在32位宽向量上操作的8路向量处理器。
表 I:根据Nvidia官方文档列出的GT200参数【索引1, Nvidia, CUDA Programming Guide Version 2.0】【索引3, Nvidia, GeForce GTX 200 GPU Architectural Overview, 2008】。 -
执行单元与SIMT模型 SM中执行流的基本单位是线程束(warp)。在GT200中,一个warp是32个线程的集合,并在8个标量处理器(SP)上以8个为一组执行。Nvidia将这种安排称为“单指令多线程”(SIMT),即一个warp中的每个线程步调一致地执行相同的指令,但允许每个线程独立分支。SM包含算术单元和其他对线程块和线程私有的资源,如每个线程块的共享内存和寄存器文件。SM被组织成线程处理集群(TPC,如图2)。TPC还包含SM之间共享的资源(例如缓存、纹理获取单元),其中大部分对程序员不可见。从CUDA的角度来看,GPU由TPC集合、互连网络和内存系统(DRAM内存控制器)组成,如图3所示。
-
CUDA软件编程接口 CUDA通过一个带有扩展的类C语言来呈现GPU架构,以抽象线程模型。在CUDA模型中,主机CPU代码可以通过调用在GPU上执行的设备函数来启动GPU内核。由于GPU使用与主机CPU不同的指令集,CUDA编译流程使用不同的编译器为CPU和GPU代码生成不同的指令集。GPU代码首先被编译成PTX“汇编”,然后“汇编”成本地代码。编译后的CPU和GPU代码随后被合并成一个“胖二进制”(fat binary)【索引4, The CUDA Compiler Driver NVCC】。
-
PTX与本地代码分析 尽管PTX被描述为GPU代码的汇编级表示,但它只是一个中间表示,对于详细分析或微基准测试并不适用。由于本地指令集不同,并且编译器会在PTX代码上进行优化,因此PTX代码不能很好地代表实际执行的机器指令。在大多数情况下,我们发现最有效的方法是使用CUDA C编写代码,然后使用decuda【索引5, W. J. van der Laan, Decuda】在本地代码级别验证生成的机器代码序列。使用decuda主要是为了方便,因为生成的指令序列可以在本地的cubin二进制文件中得到验证。Decuda是Nvidia机器级指令的反汇编器,通过分析Nvidia编译器的输出来开发,因为本地指令集没有公开文档。
测量方法
-
微基准测试设计 为了探索GT200架构,我们创建了微基准测试来揭示我们希望测量的每个特性。我们的结论是通过分析微基准测试的执行时间得出的。在测量指令缓存参数时,使用Decuda报告代码大小和位置,这与我们对编译代码的分析一致。我们还使用Decuda检查CUDA编译器生成的本地指令序列,并分析为处理分支分化和再收敛而生成的代码。
-
通用基准测试结构 微基准测试的通用结构包含GPU内核代码,其中在一个代码段(通常是一个多次运行的展开循环)周围放置了计时代码,该代码段用于测试被测量的硬件。一个基准内核会完整运行代码两次,忽略第一次迭代以避免冷指令缓存未命中的影响。在所有情况下,内核代码大小都足够小,可以放入L1指令缓存(4KB,见IV-K节)。计时测量是通过读取时钟寄存器(使用
clock()
)完成的。时钟值首先存储在寄存器中,然后在内核结束时写入全局内存,以避免慢速的全局内存访问干扰计时测量。 -
TPC位置差异性处理 在研究缓存层次结构时,我们观察到跨越互连网络的内存请求(例如,访问L3缓存和片外内存)的延迟会根据执行代码的TPC而变化。我们将测量结果在所有10个TPC位置上取平均值,并在相关时报告其变化。
-
从延迟图推断缓存特性 我们大部分的缓存和TLB参数测量都使用对不同大小数组的跨步访问(stride access),并绘制平均访问延迟。本节描述的基本技术也用于测量CPU缓存参数。我们为指令缓存和共享缓存层次结构开发了变体。图4展示了一个从平均延迟图中提取缓存大小、路大小(way size)和行大小(line size)的例子。此例假设采用LRU替换策略、组相联缓存且无预取。
图4:三路12行组相联缓存及其延迟图
(a) 一个384字节、3路、4组、32字节行大小的缓存的延迟图 -
延迟图分析方法 缓存参数可以从图4(a)的示例图中推断如下:只要数组能装入缓存,延迟保持不变(大小为384及以下)。一旦数组大小开始超过缓存大小,延迟会出现阶梯式增长,阶梯数量等于缓存组的数量(四组),因为这些组一个接一个地溢出(大小在385-512之间,即缓存路的大小)。触发每个平均延迟增长阶梯所需的数组大小增量等于行大小(32字节)。当所有缓存组都溢出时,延迟达到平台期(大小≥16个缓存行)。缓存的相联度(三路)可以通过将缓存大小(384字节)除以路大小(128字节)得到。此计算不需要行大小或缓存组数。还有其他计算这四个缓存参数的方法,因为知道其中任意三个就可以通过公式“缓存大小 = 缓存组数 × 行大小 × 相联度”推导出第四个。
列表1:设置依赖读取序列(CPU主机代码)
列表2:依赖读取序列(GPU内核代码) -
内存微基准测试实现 列表1和列表2展示了我们内存微基准测试的结构。对于每个数组大小和步长,微基准测试执行一系列依赖读取,预先计算好的步长访问模式存储在数组中,从而消除了计时内循环中的地址计算开销。步长应小于缓存行大小,以便可以观察到延迟图中的所有阶梯,但又应足够大,以使延迟阶梯之间的过渡不至于太小而难以清晰区分。
A2 方法细节
本节详细介绍我们的测试和结果。我们首先测量clock()
函数的延迟,然后研究SM的各种算术流水线、分支分化和屏障同步。我们还探讨了SM内部及周围的内存缓存层次结构,以及内存转换和TLB。
表 II:算术流水线延迟与吞吐量
时钟开销和特性
-
clock()
函数特性 所有计时测量都使用clock()
函数,该函数返回一个每个时钟周期递增的计数器的值【索引1, Nvidia, CUDA Programming Guide Version 2.0】。clock()
函数会转换为一个从时钟寄存器移动数据的操作,然后进行一个依赖的左移一位操作,这表明计数器是以着色器时钟频率的一半递增的。一个clock()
调用后跟一个非依赖操作需要28个周期。
图5:连续两次内核启动(分别为10个和30个块)的时间戳。内核调用是串行的,表明TPC拥有独立的时钟寄存器。 -
时钟寄存器的作用域 图5中的实验表明时钟寄存器是每个TPC独有的。图中的点显示了在一个线程块执行的开始和结束时调用
clock()
返回的时间戳值。我们看到,在同一个TPC上运行的线程块共享时间戳值,因此共享时钟寄存器。如果时钟寄存器是全局同步的,那么一个内核中所有线程块的开始时间将大致相同。反之,如果时钟寄存器是每个SM独有的,那么一个TPC内的线程块的开始时间将不会共享相同的时间戳。
算术流水线
-
执行单元类型 每个SM包含三种不同类型的执行单元(如图1和表I所示):
- 八个标量处理器(SP),执行单精度浮点和整数算术逻辑指令。
- 两个特殊功能单元(SFU),负责执行超越函数和数学函数,如平方根倒数、正弦、余弦,以及单精度浮点乘法。
- 一个双精度单元(DPU),处理64位浮点操作数的计算。
-
延迟与吞吐量测量方法 表II显示了当所有操作数都在寄存器中时这些执行单元的延迟和吞吐量。为了测量流水线延迟和吞吐量,我们使用由一连串依赖操作组成的测试。对于延迟测试,我们只运行一个线程。对于吞吐量测试,我们运行一个包含512个线程的线程块(每个线程块的最大线程数)以确保单元被充分占用。表III和表IV显示了每个操作使用的执行单元,以及观察到的延迟和吞愈量。
表 III:算术和逻辑运算的延迟与吞吐量
表 IV:数学内置函数的延迟与吞吐量。“执行单元”列中的“–”表示该操作映射到一个多指令例程。 -
指令映射与性能 表III显示,单精度和双精度浮点乘法以及乘加(mad)操作各自映射到一条设备指令。然而,32位整数乘法会转换为四条本地指令,需要96个周期。32位整数乘加操作会转换为五条依赖指令,需要120个周期。硬件仅通过
mul24()
内置函数支持24位整数乘法。对于32位整数和双精度操作数,除法会转换为子程序调用,导致高延迟和低吞吐量。然而,单精度浮点除法被转换为一个短的内联指令序列,延迟要低得多。 -
SFU与SP的协同工作 测得的单精度浮点乘法吞吐量约为11.2 ops/clock。这大于SP的吞吐量8,表明乘法操作同时分派给了SP和SFU单元。这暗示每个SFU能够每周期执行约2次乘法(两个SFU共4次),是映射到SFU的其他(更复杂的)指令吞吐量的两倍。单精度浮点乘加的吞吐量为7.9 ops/clock,表明
mad
操作不能由SFU执行。 -
内置函数实现 Decuda显示
sinf()
、cosf()
和exp2f()
内置函数各自转换为一个操作单个操作数的两指令依赖序列。编程指南指出SFU执行超越运算,然而,这些超越指令的延迟和吞吐量测量结果与由这些单元执行的更简单指令(例如log2f
)不匹配。sqrt()
映射到两条指令:一个平方根倒数后跟一个倒数。 -
流水线利用率与Warp调度 图6显示了随着SM上并发warp数量的增加,依赖的SP指令(整数加法)的延迟和吞吐量。当并发warp少于6个时,观察到的延迟为24个周期。由于所有warp观察到相同的延迟,warp调度器是公平的。吞吐量在流水线未满时线性增加,一旦流水线满载,则在每时钟8个操作(SP单元数量)处饱和。编程指南指出6个warp(192个线程)应足以隐藏寄存器写后读的延迟。然而,当SM中有6或7个warp时,调度器未能完全填满流水线。
图6:SP吞吐量和延迟。6或7个warp并不能完全利用流水线。
控制流
-
分支分化 (Branch Divergence) 一个warp的所有线程在同一时间执行一条共同的指令。编程指南指出,当一个warp的线程由于数据依赖的条件分支而发生分化时,warp会串行执行每个被采用的分支路径,并禁用不在该路径上的线程【索引1, Nvidia, CUDA Programming Guide Version 2.0】。我们的观察结果与预期行为一致。图7显示了在一个线程块中两个并发warp的执行时间线,这两个warp的线程都发生了32路分化。每个线程根据其线程ID选择不同的路径,并执行一系列算术操作。该图显示,在单个warp内部,每个路径都是串行执行的,而不同warp的执行可能会重叠。在一个warp内部,选择相同路径的线程是并发执行的。
图7:两个32路分化warp的执行时间线。上方的系列显示Warp 0的时间,下方显示Warp 1的时间。 -
再收敛 (Reconvergence) 当分化路径的执行完成时,线程会收敛回相同的执行路径。Decuda显示,编译器会在一个可能分化的分支之前插入一条指令,该指令向硬件提供了再收敛点的位置。Decuda还显示,再收敛点的指令通过指令编码中的一个字段进行标记。我们观察到,当线程分化时,每个路径的执行会串行化直到再收敛点。只有当一个路径到达再收敛点后,另一个路径才开始执行。
图8:列表3所示内核的执行时间线。数组c包含递增序列{0, 1, ..., 31} -
分支同步栈 根据Lindholm等人的研究,一个分支同步栈被用来管理分化和收敛的独立线程【索引6, E. Lindholm et al., NVIDIA Tesla: A Unified Graphics and Computing Architecture, IEEE Micro 2008】。我们使用列表3中所示的内核来证实这一说法。数组c包含0到31之间数字集合的一个排列,指定了线程的执行顺序。我们观察到,当一个warp到达一个条件分支时,被采纳(taken)的路径总是首先被执行:对于每个if语句,else路径是被采纳的路径并首先执行,因此最后一个then子句(
else if (tid == c[31])
)总是最先执行,而第一个then子句(if (tid == c[0])
)最后执行。
列表3:再收敛栈测试 -
执行顺序与分支栈机制 图8显示了当数组c包含递增序列{0, 1, ..., 31}时此内核的执行时间线。在这种情况下,线程31是第一个执行的线程。当数组c包含递减序列{31, 30, ..., 0}时,线程0是第一个执行的,这表明线程ID不影响执行顺序。观察到的执行顺序与被采纳的路径首先执行,而未被采纳的路径(fall-through path)被压入一个栈的机制相符。其他测试表明,一个路径上的活动线程数量对哪个路径先执行也没有影响。
列表4:因SIMT行为而出错的示例代码 -
SIMT导致的串行化效应 编程指南指出,为了保证正确性,程序员可以忽略SIMT行为。本节中,我们展示了一个例子,该代码如果线程是独立的则可以工作,但由于SIMT行为而导致死锁。在列表4中,如果线程是独立的,第一个线程将跳出while循环并增加
sharedvar
。这将导致每个后续线程做同样的事情:跳出while循环并增加sharedvar
,从而允许下一个线程执行。在SIMT模型中,当线程0的while循环条件失败时,发生分支分化。编译器将再收敛点标记在sharedvar++
之前。当线程0到达再收敛点时,另一个(串行化的)路径被执行。线程0在其他线程也到达再收敛点之前无法继续执行并增加sharedvar
。这导致了死锁,因为这些线程永远无法到达再收敛点。
屏障同步
-
_syncthreads()
基本特性 单个线程块内warp之间的同步是通过_syncthreads()
完成的,它起到一个屏障的作用。_syncthreads()
被实现为一条单一指令,对于单个warp执行一系列_syncthreads()
的延迟为20个时钟周期。编程指南建议仅在条件对整个线程块求值相同时才在条件代码中使用_syncthreads()
。本节的其余部分研究了当违反此建议时_syncthreads()
的行为。 -
_syncthreads()
的粒度:warp而非线程 我们证明了_syncthreads()
是针对warp而非线程的屏障。我们表明,当一个warp的线程由于分支分化而串行化时,一个路径上的任何_syncthreads()
不会等待来自另一路径的线程,而只等待在同一线程块内运行的其他warp。 -
单Warp内
_syncthreads()
行为 编程指南指出_syncthreads()
是同一块中所有线程的屏障。然而,列表5中的测试表明,_syncthreads()
是同一块中所有warp的屏障。此内核针对单个warp执行,其中warp的前半部分在共享内存中为后半部分产生值以供其消费。如果_syncthreads()
等待块中的所有线程,那么此示例中的两个_syncthreads()
将作为一个共同的屏障,迫使生产者线程(warp的前半部分)在消费者线程(warp的后半部分)读取值之前写入值。此外,由于分支分化会串行化分化warp的执行(见IV-C1节),每当在分化的warp中使用_syncthreads()
时,内核都会死锁(在此示例中,一组16个线程会等待另一组串行化的16个线程到达其_syncthreads()
调用)。我们观察到没有死锁发生,并且warp的后半部分没有读取到shared_array
数组中的更新值(else子句先执行,见IV-C1节),这表明_syncthreads()
并不像编程指南的描述可能暗示的那样同步一个warp内部分化的线程。
列表5:展示_syncthreads()
在warp粒度上同步的示例代码
列表6:因_syncthreads()
导致死锁的示例代码。测试使用两个warp运行。 -
跨多Warp的
_syncthreads()
行为_syncthreads()
是一个屏障,它会等待所有的warp都调用_syncthreads()
或终止。如果有一个warp既不调用_syncthreads()
也不终止,_syncthreads()
将无限期等待,这表明缺乏超时机制。列表6展示了这样一个死锁的例子(没有分支分化),其中第二个warp在自旋等待第一个warp在_syncthreads()
之后生成的数据。
列表7:因_syncthreads()
产生意外结果的示例代码。 -
_syncthreads()
与分支分化的交互 列表7阐释了_syncthreads()
和分支分化之间交互的细节。鉴于_syncthreads()
在warp粒度上操作,人们会期望硬件要么忽略分化warp内部的_syncthreads()
,要么分化warp以与没有分化的warp相同的方式参与屏障。我们表明后者是正确的。在这个例子中,第二个_syncthreads()
与第三个同步,第一个与第四个同步(对于warp 0,代码块2在代码块1之前执行,因为块2是分支的被采纳路径,见IV-C1节)。这证实了_syncthreads()
在warp的粒度上操作,并且分化的warp也不例外。每个串行化的路径分别执行_syncthreads()
(代码块2在屏障处不等待1)。它会等待块中所有其他warp也执行_syncthreads()
或终止。
寄存器文件
-
容量与分配规则 我们确认寄存器文件包含16,384个32位寄存器(64 KB),与编程指南所述一致【索引1, Nvidia, CUDA Programming Guide Version 2.0】。一个线程使用的寄存器数量向上取整到4的倍数【索引4, The CUDA Compiler Driver NVCC】。尝试启动每个线程使用超过128个寄存器或一个块中总共使用超过64 KB寄存器的内核会导致启动失败。在图9中,当每个线程使用少于32个寄存器时,寄存器文件无法被完全利用,因为每个块允许的最大线程数为512。当每个线程使用超过32个寄存器时,寄存器文件容量限制了可以在一个块中运行的线程数量。
图9:一个块使用的总寄存器数限制为16,384个(64 KB)。当受寄存器文件容量限制时,一个块中的最大线程数量被量化为64的倍数。 -
线程数量化与逻辑Bank 图9显示,当受寄存器文件容量限制时,一个块中的最大线程数被量化到64的倍数。这表明一个线程的寄存器被分配到64个逻辑“bank”之一。每个bank的大小相同,因此每个bank可以容纳相同数量的线程,当受寄存器文件容量限制时,线程数被限制为64的倍数。注意,这与量化总寄存器使用量不同。
-
物理实现推测 由于所有八个SP在任何给定时间总是执行相同的指令,因此64个逻辑bank的物理实现可以共享SP之间的地址线,并使用更宽的内存阵列而不是64个真实的bank。每个时钟周期每个SP能够进行四次寄存器访问(四个逻辑bank),这为每时钟周期执行三读一写的操作数指令(例如,乘加)提供了足够的带宽。一个线程会跨越多个周期访问其寄存器,因为它们都位于单个bank中,而多个线程的访问会同时发生。
-
额外带宽与双发射 每个SP拥有八个逻辑bank可以为使用SFU的“双发射”特性(见IV-B节)以及与算术操作并行执行内存操作提供额外带宽。
-
与编程指南的关联 编程指南暗示倾向于使用64的倍数个线程,建议为避免bank冲突,“最佳结果”是在每个块的线程数是64的倍数时实现。我们观察到,当受寄存器数量限制时,每个块的线程数被限制为64的倍数,而我们并未观察到bank冲突。
共享内存
- 特性与延迟 共享内存是一个非缓存的、每个SM独有的内存空间。它被一个块的线程用来通过与同一块中的其他线程共享数据进行协作。每个块允许的共享内存量为16 KB。内核的函数参数也占用共享内存,因此略微减少了可用的内存大小。我们使用如列表1和2中的跨步访问测量到读取延迟为38个周期。Volkov和Demmel在GT200的前身8800GTX上报告了类似的36个周期的延迟【索引7, V. Volkov and J. W. Demmel, Benchmarking GPUs to Tune Dense Linear Algebra, SC ’08】。编程指南指出共享内存延迟与寄存器访问延迟相当。通过改变微基准测试的内存占用和步长,我们验证了共享内存没有缓存。
全局内存
- 特性与延迟 全局内存可被所有正在运行的线程访问,即使它们属于不同的块。全局内存访问是无缓存的,并且有文档记录的延迟为400-600个周期【索引1, Nvidia, CUDA Programming Guide Version 2.0】。我们的微基准测试执行一系列对全局内存的指针追踪式依赖读取,类似于列表1和2。在没有TLB未命中的情况下,我们测量的读取延迟在436-443个周期之间。IV-I2节提供了更多关于内存转换对全局内存访问延迟影响的细节。我们还调查了缓存的存在,没有观察到缓存效应。
纹理内存
-
特性概述 纹理内存是一个带缓存的、只读的、全局可见的内存空间。在图形渲染中,纹理通常是二维的并表现出二维局部性。CUDA支持一维、二维和三维纹理。我们测量绑定到线性内存区域的一维纹理的缓存层次结构。我们的代码执行对纹理的依赖纹理拾取,类似于列表1和2。图10显示了使用64字节步长时存在两个级别的纹理缓存,L1和L2缓存大小分别为5 KB和256 KB。我们预计高维(二维和三维)纹理的内存层次结构不会有显著不同。二维空间局部性通常通过使用地址计算将纹理元素重新排列成“瓦片(tiles)”来实现,而不是需要专门的缓存【索引8, Z. S. Hakura and A. Gupta, The Design and Analysis of a Cache Architecture for Texture Mapping, SIGARCH Comput. Archit. News 1997】【索引9, Intel, G45: Volume 1a Graphics Core, Intel 965G Express Chipset Family and Intel G35 Express Chipset Graphics Controller Programmer’s Reference Manual (PRM), 2009】【索引10, AMD, ATI CTM Guide, Technical Reference Manual】。
图10:纹理内存。5 KB L1和256 KB、8路 L2缓存。使用64字节步长测量。 -
L1纹理缓存 L1纹理缓存是5 KB、20路组相联,缓存行为32字节。图11聚焦于5 KB处的第一个延迟增长,并显示了使用8字节步长的结果。对于一个5 KB的缓存,256字节的路大小意味着20路组相联。我们看到L1命中延迟(261个时钟周期)超过了主存延迟(499个时钟周期)的一半,这与编程指南的说法一致,即纹理缓存不减少拾取延迟,但确实减少了DRAM带宽需求。
图11:L1纹理缓存。5 KB、20路、32字节行。使用8字节步长测量。同时显示了所有TPC位置上的最大和最小平均延迟:L2具有TPC位置依赖的延迟。 -
L2纹理缓存 L2纹理缓存是256 KB、8路组相联,缓存行为256字节。图10显示对于一个256 KB的缓存,路大小为32 KB,意味着8路组相联。图12放大了前一个图在256 KB附近的区域,揭示了延迟阶梯的存在,这表明缓存行大小为256字节。我们也可以在图11中看到,L2纹理缓存的访问时间与TPC位置有关,这表明L2纹理缓存不位于TPC内部。
图12:L2纹理缓存。256 KB、8路、256字节行。使用64字节步长测量。
内存翻译
-
TLB测量方法 我们使用跨步访问的依赖读取来研究TLB的存在,类似于列表1和2。测量TLB参数与测量缓存类似,但数组大小更大,步长也更大,与页面大小相当。全局内存和纹理内存的详细TLB结果分别在IV-I1和IV-I2节中呈现。
-
全局内存翻译 图13显示全局内存有两个TLB级别。L1 TLB是全相联的,持有8 MB内存的映射,包含16个行,TLB行大小为512 KB。32 MB的L2 TLB是8路组相联的,行大小为4 KB。我们使用术语TLB大小来指代TLB可以映射的页面总大小,而不是TLB中存储的条目的原始大小。例如,一个8 MB的TLB描述了一个当页面大小为4 KB时可以缓存2K个映射的TLB。此外,如果TLB行大小为512 KB,那么TLB将被组织成16行,每行包含128个连续页面的映射。
图13:全局内存。8 MB全相联L1和32 MB 8路L2 TLB。使用512 KB步长测量。
图14:全局L1 TLB。16路全相联,行大小512 KB。 -
全局TLB参数与延迟 在图13中,第一个约440个周期的延迟平台表示L1 TLB命中(即IV-G节中测量的全局内存读取延迟)。第二个约487个周期的平台表示L2 TLB命中,而L2 TLB未命中则需要约698个周期。我们通过访问固定数量的元素并改变步长来测量L1 TLB的16路相联性。图14描述了访问16个和17个数组元素时的结果。对于大步长,所有元素都映射到同一个缓存组(例如8 MB),访问16个元素总是L1 TLB命中,而访问17个元素在步长为512 KB及以上时会发生L1 TLB未命中。我们还可以看到L1 TLB只有一个缓存组,这意味着它是全相联的,行大小为512 KB。如果有至少两个组,那么当步长不是2的幂且大于512 KB(一个缓存路的大小)时,一些元素会映射到不同的组。当访问17个元素时,它们不会全部映射到同一个组,并且会存在某个步长,使得没有L1未命中发生。我们从未在步长超过512 KB时看到L1 TLB命中(例如608、724和821 KB)。然而,我们可以看到(在步长超过4 MB时)L2 TLB不是全相联的。
-
全局L2 TLB参数 图13显示L2 TLB的一路大小为4 MB(32到36 MB;见III-B节)。由于L2 TLB大小为32 MB,因此L2 TLB的相联度为八。扩展测试没有发现多级分页的证据。尽管L1 TLB行大小为512 KB,但L2 TLB行大小更小,为4 KB。我们使用了一个微基准测试,该测试使用两组各10个元素(共20个),每个元素之间相隔2 MB的步长。两组元素之间相隔2 MB+偏移量。第i个元素的地址为
(i < 10) ? (i × 2MB) : (i × 2MB + offset)
。我们需要访问超过16个元素,以防止16路的L1 TLB隐藏了访问。由于L2 TLB的一路大小为4 MB,我们使用2 MB的步长,当偏移量为零时,我们的20个元素映射到两个L2组。图15显示了4 KB的L2 TLB行大小。当偏移量为零时,我们的20个元素占用两个组,每组10个元素,导致在8路相联的L2 TLB中发生冲突未命中。当偏移量增加超过4 KB的L2 TLB行大小后,每组5个元素不再导致L2 TLB中的冲突未命中。我们认为4 KB的页面大小是一个合理的选择,尽管页面大小可能小于4 KB的L2 TLB行大小。我们注意到Intel x86架构使用多级分页,主要是4 KB页面,而Intel的GPU系列使用单级4 KB分页【索引9, Intel, G45: Volume 1a Graphics Core, Intel 965G Express Chipset Family and Intel G35 Express Chipset Graphics Controller Programmer’s Reference Manual (PRM), 2009】【索引11, Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A: System Programming Guide, Part 1, 2009】。
图15:全局L2 TLB。4 KB TLB行大小。 -
纹理内存翻译 我们使用与IV-I1节相同的方法来计算纹理内存TLB的配置参数。为简洁起见,此处不再重复该方法。纹理内存包含两个级别的TLB,分别具有8 MB和16 MB的映射,如图16所示(使用256 KB步长)。L1 TLB是16路全相联的,每行持有512 KB内存的翻译。L2 TLB是8路组相联的,行大小为4 KB。在512 KB步长下,虚拟索引的20路L1纹理缓存隐藏了L1纹理TLB的特性。使用512 KB步长测量的访问延迟为497(TLB命中)、544(L1 TLB未命中)和753(L2 TLB未命中)个时钟周期。
图16:纹理内存。8 MB全相联L1 TLB和16 MB 8路L2 TLB。L1 TLB未命中延迟544个时钟,L2 TLB未命中延迟753个时钟。使用256 KB步长测量。
常量内存
-
特性与缓存层次 常量内存有两个段:一个是用户可访问的,另一个由编译器生成的常量(例如,用于分支条件的比较)使用【索引4, The CUDA Compiler Driver NVCC】。用户可访问的段限制为64 KB。图17中的图表显示了三个级别的缓存,大小分别为2 KB、8 KB和32 KB。测量的延迟包括两个算术指令(一个地址计算和一个加载)的延迟,因此原始内存访问时间大约要低48个周期(L1命中、L2命中、L3命中和L3未命中的延迟分别为8、81、220和476个时钟周期)。我们的微基准测试执行依赖的常量内存读取,类似于列表1和2。
图17:常量内存。2 KB L1、8 KB 4路L2、32 KB 8路L3缓存。使用256字节步长测量。同时显示了所有TPC位置上的最大和最小平均延迟:L3具有TPC位置依赖的延迟。 -
L1常量缓存 一个2 KB的L1常量缓存位于每个SM中(见IV-J4节)。L1的缓存行大小为64字节,是4路组相联,有8个组。路大小为512字节,表明在一个2 KB的缓存中是4路组相联。图18显示了这些参数。
图18:常量L1缓存。2 KB、4路、64字节行。使用16字节步长测量。 -
L2常量缓存 一个8 KB的L2常量缓存位于每个TPC中,并与指令内存共享(见IV-J4和IV-J5节)。L2缓存的缓存行大小为256字节,是4路组相联,有8个组。图17中8,192字节附近的区域显示了这些参数。在一个8 KB的缓存中,2 KB的路大小表明相联度为四。
-
L3常量缓存 我们观察到一个32 KB的L3常量缓存,在所有TPC之间共享。L3缓存的缓存行大小为256字节,是8路组相联,有16个组。我们在图17中32 KB附近的区域观察到缓存参数。L3缓存的最小和最大访问延迟(图17,8-32 KB区域)根据执行测试代码的TPC而显著不同。这表明L3缓存位于连接TPC到L3缓存和内存的非均匀互连网络上。即使在访问主存时(数组大小 > 32 KB),延迟方差也不随数组大小增加而改变,这表明L3缓存位于主存控制器附近。
-
L3常量缓存带宽 我们还测量了L3缓存的带宽。图19显示了当不同数量的块发出并发L3缓存读取请求时,L3缓存的聚合读取带宽,其中每个线程内的请求是独立的。当运行10到20个块时,观察到的L3常量缓存的聚合带宽约为9.75字节/时钟。我们运行了两个版本的带宽测试:一个版本每个块使用一个线程,另一个版本使用八个线程以增加TPC内的常量缓存获取需求。两个测试在20个块以下时表现出类似的行为。这表明当运行一个块时,即使块内需求增加(来自多个线程),一个SM也只能获取约1.2字节/时钟。在八线程情况下,超过20个块的测量是无效的,因为没有足够多的唯一数据集,并且每个TPC的L2缓存隐藏了一些对L3的请求,导致表观聚合带宽增加。超过30个块时,一些SM会运行多个块,导致负载不平衡。
图19:常量L3缓存带宽。9.75字节/时钟。 -
缓存共享范围 L1常量缓存对每个SM是私有的,L2在TPC上的SM之间共享,L3是全局的。这是通过使用两个并发块并改变其位置(同一个SM,同一个TPC,两个不同的TPC)来测量的。两个块将竞争共享缓存,导致观察到的缓存大小减半。图20显示了此测试的结果。在所有情况下,观察到的缓存大小减半至16 KB(L3是全局的)。当两个块放置在同一个TPC上时,观察到的L2缓存大小减半至4 KB(L2是每个TPC的)。同样,当两个块在同一个SM上时,观察到的L1缓存大小减半至1 KB(L1是每个SM的)。
图20:常量内存共享。每SM一个L1缓存,每TPC一个L2缓存,全局L3缓存。使用256字节步长测量。 -
与指令内存的缓存共享 有人提出部分常量缓存和指令缓存层次结构是统一的【索引12, H. Goto, Gt200 over view, 2008】【索引13, D. Kirk and W. W. Hwu, ECE 489AL Lectures 8-9: The CUDA Hardware Model, 2007】。我们发现L2和L3缓存确实是指令和常量缓存,而L1缓存是单一用途的。与IV-J4节类似,我们测量了指令获取和常量缓存获取之间在不同位置下的干扰。结果绘制在图21中。即使块在同一个SM上运行,L1的访问时间也不受指令获取需求的影响,因此L1缓存是单一用途的。
图21:常量内存与指令缓存共享。L2和L3缓存与指令共享。使用256字节步长测量。
指令供给
-
指令缓存层次 我们检测到三个级别的指令缓存,大小分别为4 KB、8 KB和32 KB(如图22所示)。微基准测试代码由不同大小的独立8字节算术指令(
abs
)块组成,以最大化获取需求。图22中可见8 KB的L2和32 KB的L3缓存,但4 KB的L1不可见,可能是因为少量的指令预取隐藏了L2的访问延迟。
图22:指令缓存延迟。8 KB 4路L2,32 KB 8路L3。此测试未能检测到4 KB L1缓存。 -
L1指令缓存 4 KB的L1指令缓存位于每个SM中,缓存行大小为256字节,4路组相联。通过在同一TPC上的另外两个SM上运行并发代码块来引入对L2缓存的争用,从而测量了L1缓存的参数(图23),这样从4 KB开始的L1未命中就不会像图22中那样被隐藏。256字节的行大小是可见的,以及4个缓存组的存在。L1指令缓存是每个SM独有的。当同一TPC上的其他SM淹没其指令缓存层次结构时,被观察的SM的4 KB指令缓存大小没有减少。
图23:L1指令缓存。4 KB、4路、256字节行。通过增加L2缓存的争用来使L1未命中可见。同时显示了所有TPC位置上的最大和最小平均延迟:L3具有TPC位置依赖的延迟。 -
L2指令缓存 8 KB的L2指令缓存位于TPC上,缓存行大小为256字节,4路组相联。我们在IV-J5节中已表明L2指令缓存也用于常量内存。我们已验证L2指令缓存参数与L2常量缓存参数匹配,但因空间限制省略了结果。
-
L3指令缓存 32 KB的L3指令缓存是全局的,缓存行大小为256字节,8路组相联。我们在IV-J5节中已表明L3指令缓存也用于常量内存,并已验证指令缓存的缓存参数与常量内存的匹配。
-
指令获取 SM似乎每次从L1指令缓存中获取64字节的指令(8-16条指令)。图24显示了我们测量代码的执行时间线,该代码由36个连续的
clock()
读取(72条指令)组成,是微基准测试执行10,000次的平均结果。当一个warp运行测量代码时,同一SM上运行的七个“驱逐warp”通过循环执行24条指令(192字节)来重复驱逐图中大点所示的区域,这些指令会在指令缓存中引起冲突未命中。驱逐warp会以高概率重复驱逐测量代码使用的缓存行,具体取决于warp调度。由驱逐引起的缓存未命中延迟只会在指令获取边界(图24中的160、224、288、352和416字节处)被观察到。我们看到,当发生冲突时,整个缓存行(跨越代码区域160-416字节)被驱逐,并且缓存未命中的影响只在64字节的块之间观察到,而不是在块内部。
图24:指令获取大小。SM似乎以64字节的块从L1缓存中获取指令。代码跨越三个256字节的缓存行,边界在160和416字节处。
A4 实验环境
- 硬件配置:
- GPU: Nvidia GT200 (GTX280)。
- 架构参数: 拥有10个线程处理集群 (TPC),每个TPC包含3个流式多处理器 (SM),每个SM包含8个标量处理器 (SP) 和2个特殊功能单元 (SFU)。详细参数见表I。
- 软件配置:
- 编程语言/接口: CUDA C。
- 编译器: Nvidia的
nvcc
编译器驱动。 - 分析工具: 使用
decuda
【索引5, W. J. van der Laan, Decuda】反汇编器来检查和验证编译器生成的本地机器码。
- 实验方法:
- 基准测试: 自行开发的一套微基准测试程序,每个程序针对特定的硬件单元(如算术流水线、缓存、TLB等)进行设计。
- 计时: 使用GPU内置的
clock()
函数来测量代码段的执行时间,该函数返回一个以时钟周期为单位的计数器值。 - 数据处理: 为了消除不同TPC带来的延迟差异,所有跨TPC的测量结果都在全部10个TPC上运行并取平均值。
A4 实验结果
本文通过一系列微基准测试,揭示了Nvidia GT200 GPU的多项微架构细节,主要实验结果总结如下:
-
算术流水线:
- SP流水线延迟: 测量得到SP(标量处理器)的流水线延迟为24个时钟周期。实验表明,需要超过6或7个并发Warp才能完全隐藏该延迟,这与CUDA编程指南中6个Warp即可的建议略有出入 (图6)。
- SFU功能: 特殊功能单元 (SFU) 不仅处理超越函数,还能与SP协同执行单精度浮点乘法,使得总乘法吞吐量超过8 ops/clock (表III)。
- 整数运算: 32位整数乘法和乘加操作会被编译成多条指令,延迟较高 (96-120周期),而硬件原生支持的
mul24()
效率更高。
-
控制流与同步:
- 分支分化: 验证了分化Warp会串行执行不同路径的机制,并发现硬件使用一个分支同步栈来管理分化与再收敛,且总是优先执行“taken”路径 (图7, 图8)。
- SIMT死锁: 发现特定的代码模式在SIMT模型下会导致死锁,这是因为分化路径的线程必须等待其他路径的线程到达再收敛点才能继续执行 (列表4)。
_syncthreads()
粒度: 证明了_syncthreads()
同步的粒度是Warp级别而非线程级别。在一个分化的Warp内部,不同路径上的_syncthreads()
不会相互等待,这可能导致非预期的行为甚至数据竞争 (列表5, 列表7)。
-
内存层次结构:
- 寄存器文件: 确认了每个SM拥有16,384个32位寄存器 (64 KB),并发现当受寄存器容量限制时,线程块内的线程数会被量化为64的倍数,这暗示了其内部存在64个逻辑Bank (图9)。
- 共享内存: 测量得到16KB共享内存的读取延迟为38个周期。
- 全局内存: 测得无缓存的全局内存访问延迟为436-443周期(无TLB未命中)。
- 纹理缓存: 发现了两级纹理缓存:一个5 KB、20路的L1缓存和一个256 KB、8路的L2缓存。L2缓存的访问延迟与TPC位置相关,表明其位于TPC外部 (图10, 图11, 图12)。
- 常量缓存: 发现了三级常量缓存:2 KB每SM的L1,8 KB每TPC的L2,以及32 KB全局共享的L3。其中L2和L3缓存与指令缓存共享 (图17, 图20, 图21)。
- 指令缓存: 同样存在三级缓存,分别为4 KB每SM的L1,8 KB每TPC的L2,以及32 KB全局共享的L3。L2和L3与常量缓存共享。SM从L1指令缓存中以64字节为单位获取指令 (图23, 图24)。
-
内存地址翻译 (TLB):
- 全局内存TLB: 发现了两级TLB。一个8 MB、全相联的L1 TLB和一个32 MB、8路的L2 TLB (图13)。
- 纹理内存TLB: 同样存在两级TLB。一个8 MB、全相联的L1 TLB和一个16 MB、8路的L2 TLB (图16)。
A5 结论
本文介绍了对Nvidia GT200 GPU的分析以及我们的测量技术。通过我们开发的微基准测试套件,揭示了处理核心和内存层次结构的架构细节。GPU是一个复杂的设备,我们不可能对每个细节都进行逆向工程,但我们相信我们已经研究了一部分有趣的特性。下表 V 总结了我们的架构发现。
我们的结果验证了CUDA编程指南【索引1, Nvidia, CUDA Programming Guide Version 2.0】中提出的一些硬件特性,但也揭示了一些未记录的硬件结构的存在,例如控制流机制以及缓存和TLB层次结构。此外,在某些情况下,我们的发现与文档记录的特性有所偏离(例如,纹理和常量缓存)。
我们还介绍了用于架构分析的技术。我们相信这些技术对于分析其他类GPU架构和验证类GPU性能模型将非常有用。最终目标是更好地了解硬件,以便我们能够充分发挥其潜力。
GT200架构总结表:
表 V: GT200架构总结
A6 附录
本文档不包含附录。
方法细节中的引用汇总
-
【1】Nvidia, “Compute Unified Device Architecture Programming Guide Version 2.0,” http://developer.download.nvidia.com/compute/cuda/2_0/docs/NVIDIA_CUDA_Programming_Guide_2.0.pdf.
- 引用位置: A1, A3, A2-方法细节(时钟、算术流水线、控制流、寄存器文件、全局内存), A5
- 原文描述: 引用CUDA编程指南作为Nvidia官方提供的主要技术文档,用以对比和验证测量结果,例如关于SIMT模型、
_syncthreads()
用法、寄存器文件大小、全局内存延迟等。
-
【2】A. Bakhoda, G. L. Yuan, W. W. L. Fung, H. Wong, and T. M. Aamodt, “Analyzing CUDA Workloads Using a Detailed GPU Simulator,” in Performance Analysis of Systems and Software, 2009. ISPASS 2009. IEEE International Symposium on, April 2009, pp. 163–174.
- 引用位置: A1
- 原文描述: 引用该文献说明本研究提出的测量技术有助于提高GPU性能建模与仿真的保真度。
-
【3】Nvidia, “NVIDIA GeForce GTX 200 GPU Architectural Overview,” http://www.nvidia.com/docs/IO/55506/GeForce_GTX_200_GPU_Technical_Brief.pdf, May 2008.
- 引用位置: A3-背景知识
- 原文描述: 与[1]一同作为官方文档来源,提供了GT200的架构参数(见表I)。
-
【4】“The CUDA Compiler Driver NVCC,” http://www.nvidia.com/object/io_1213955090354.html.
- 引用位置: A3-背景知识, A2-方法细节(寄存器文件、常量内存)
- 原文描述: 引用该文档说明CUDA的编译流程(生成胖二进制文件)、寄存器分配规则(向上取整到4的倍数)以及常量内存的用途。
-
【5】W. J. van der Laan, “Decuda,” http://wiki.github.com/laanwj/decuda/.
- 引用位置: A3-背景知识, A4-实验环境
- 原文描述: 介绍
decuda
是用于分析和验证本地机器码的反汇编工具,对于理解编译器行为和硬件指令至关重要。
-
【6】E. Lindholm, J. Nickolls, S. Oberman, and J. Montrym, “NVIDIA Tesla: A Unified Graphics and Computing Architecture,” IEEE Micro, vol. 28, no. 2, pp. 39–55, 2008.
- 引用位置: A2-方法细节(控制流)
- 原文描述: 引用该文献提出的“分支同步栈”概念,并通过实验证实了GT200使用该机制来管理分支分化和再收敛。
-
【7】V. Volkov and J. W. Demmel, “Benchmarking GPUs to Tune Dense Linear Algebra,” in SC ’08: Proceedings of the 2008 ACM/IEEE Conference on Supercomputing. Piscataway, NJ, USA: IEEE Press, 2008, pp. 1–11.
- 引用位置: A2-方法细节(共享内存)
- 原文描述: 引用该文献对8800GTX(GT200的前代)的基准测试结果,其测量的共享内存延迟(36周期)与本文测量结果(38周期)相近。
-
【8】Z. S. Hakura and A. Gupta, “The Design and Analysis of a Cache Architecture for Texture Mapping,” SIGARCH Comput. Archit. News, vol. 25, no. 2, pp. 108–120, 1997.
- 引用位置: A2-方法细节(纹理内存)
- 原文描述: 引用该文献说明二维空间局部性通常通过地址计算(tiling)而非特殊缓存实现。
-
【9】Intel, G45: Volume 1a Graphics Core, Intel 965G Express Chipset Family and Intel G35 Express Chipset Graphics Controller Programmer’s Reference Manual (PRM), January 2009.
- 引用位置: A2-方法细节(纹理内存、内存翻译)
- 原文描述: 引用该文献作为行业参考,说明tiling技术和单级4KB分页在其他GPU(Intel)中的应用。
-
【10】AMD, ATI CTM Guide, Technical Reference Manual.
- 引用位置: A2-方法细节(纹理内存)
- 原文描述: 引用该文献说明tiling技术是行业内的常见做法。
-
【11】Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A: System Programming Guide, Part 1, September 2009.
- 引用位置: A2-方法细节(内存翻译)
- 原文描述: 引用该文献说明Intel x86 CPU架构使用多级4KB分页,与GPU的单级分页形成对比,以支持4KB页面大小的合理性假设。
-
【12】H. Goto, “Gt200 over view,” http://pc.watch.impress.co.jp/docs/2008/0617/kaigai_10.pdf, 2008.
- 引用位置: A2-方法细节(常量内存)
- 原文描述: 引用该文献提出的“常量缓存和指令缓存层次结构部分统一”的观点,并用实验证实了L2和L3缓存是共享的。
-
【13】D. Kirk and W. W. Hwu, “ECE 489AL Lectures 8-9: The CUDA Hardware Model,” http://courses.ece.illinois.edu/ece498/al/Archive/Spring2007/lectures/lecture8-9-hardware.ppt, 2007.
- 引用位置: A2-方法细节(常量内存)
- 原文描述: 同[12],引用该文献提出的缓存统一的观点。
-
【14】I. Buck, K. Fatahalian, and M. Houston, “GPUBench,” http://graphics.stanford.edu/projects/gpubench/.
- 引用位置: V. RELATED WORK (未在总结中体现)
- 原文描述: 在相关工作中提及,GPUBench是另一套基于OpenGL ARB着色语言的GPU基准测试,但其抽象层次更高,难以揭示硬件细节。
-
【15】S. Ryoo, C. I. Rodrigues, S. S. Baghsorkhi, S. S. Stone, D. B. Kirk, and W. W. Hwu, “Optimization Principles and Application Performance Evaluation of a Multithreaded GPU using CUDA,” in PPoPP ’08: Proceedings of the 13th ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming. New York, NY, USA: ACM, 2008, pp. 73–82.
- 引用位置: V. RELATED WORK (未在总结中体现)
- 原文描述: 在相关工作中提及,说明现有的优化研究依赖于已发布的官方规范,而本文提供了更详细的参数。
💬 评论讨论
欢迎在这里分享您的想法和见解!