Dual 'Issue' 并发执行
Nvidia的微架构设计中,吞吐量与延迟之间的关系十分微妙。SP执行一条指令再怎么也有至少4个周期的延迟,而SM每两个处理器周期就能发射一条指令。SM在发射一条warp指令后,SP需要一段时间才能执行完毕,那么就有可能在这段时间里再发射一条指令,这种能力被Nvidia称为‘dual issue’双发射超标量并行。‘dual issue’实际上是让不同种类的功能单元能够同时运行。SP单元在工作时,其他的执行单元也能执行其他的warp指令。
SM每两个时钟周期就能发射一条warp指令。在第一个周期,一条MAD指令被发射到FPU单元。两个时钟周期以后,一条MUL指令被发射到了SFU单元。又过了两个时钟周期以后,FPU单元开始执行另一条MAD指令。再过了两个时钟周期,SFU开始执行一条占用很长时间的超越函数指令。应用这项技术能够使渲染核心的计算吞吐量提高50%,同时每两个时钟周期只需要发射一条指令就能满足需要,大大降低了标志设置和优先级逻辑的复杂度。不是所有的指令组合都能够并发执行。例如,双精度浮点单元和单精度浮点单元共享了一部分逻辑,因此无法同时使用。
我们已经介绍了SM如何执行一条指令。既然有了这些执行单元,那么当然也需要一套机制来从显存读写数据。接下来我们将介绍GPU如何对显存进行访问。
纹理,渲染和存储器流水线
现代GPU使用纹理流水线和渲染流水线进行数据的输入输出。由于CPU需要保证存储器一致性,因此它的读取和存储单元是核心中密不可分的一个部分;而GPU中的纹理和渲染输出流水线则与GPU的计算核心相对独立,同过一个互连总线连接。在GT200中,每个TPC中拥有三个SM,一条纹理流水线和一个与渲染输出单元(render output units, ROP)通信的端口。由于本文将GPU作为计算设备来介绍,因此也就不去深究纹理流水线和ROP单元在图形学的作用,而把重点放在如何在通用计算中使用它们。GT200的读取和存储流水线的结构分为两个部分,A部分属于一个TPC,而B部分则属于一个存储器控制器一端。A部分和B部分通过GPU片上互连连接,两者不是一一对应关系,一个TPC可以对GPU中的任意一个存储器控制器提出访问请求。
用于访问显存的装载(Load)和存储(Store)指令是由工作在GPU核心频率的SM生成的,但这些指令会被发射到一个工作在存储器IO频率的硬件上执行,因此SM控制器需要协调纹理单元和SM的不同时钟。读取和存储指令首先从SM被发送到TPC中的SM控制器,再由SM控制器负责对存储器流水线访问的管理。在这里,指令被分为两路:直接对显存进行读操作的装载流水线,对显存进行写操作的存储(ROP)流水线,以及通过纹理机制访问现存的纹理流水线。纹理流水线和装载取流水线共享了一部分硬件,所以不能被同时使用。
通过装载流水线读取显存中的数据,首先要计算地址。地址可以由存储器访问指令中的寄存器中的值与地址偏移量相加得到,然后将计算得到的40bit虚拟地址(在G80中是32bit)转换为存储器控制器使用的物理地址。在地址计算完成后,读取指令将以一个warp为单位通过片内互连发射到存储器控制器。对储存命令的处理与装载类似:首先计算地址,然后向通过片内互连发送到ROP单元,再发送到存储器控制器执行。原子操作指令同样也需要经过存储流水线和ROP单元发送。每次存储器访问时指令是按照warp发射的,但存储器控制器是按照half warp执行这些指令的,也就是一次最多能够并行处理16个访问请求。
为提高访存效率,存储器控制器会尽量成批执行对同一显存bank的同方向的读或写操作。因此在CUDA程序中thread数量较多的block通常可以获得更好的存储器访问性能,不过较多的线程会使每个线程可以使用的寄存器数量较少。
存储器流水线拥有两级纹理缓存。纹理缓存与CPU使用的缓存有一些不同之处。首先,CPU的缓存往往是一维的,因为大多数的架构中的存储器地址是线性的。当访问一个只有4-8Byte的数据字时,会取出一个缓存单元中所有的64Byte数据。因为CPU处理的数据往往有很强的相关性,因此多取出的相邻的50-60Byte数据也有可能会被用到。CPU处理的数据是大多只有一个维度,因而其缓存也在一个维度上连续,只需要预取“前后”的数据,;GPU需要处理的纹理是连续的二维图像,因此纹理缓存也必须是在两个维度上连续分布的,需要预取“上下左右”的数据。典型的GPU纹理流水线会计算需要装载数据的地址,将二维的纹理存储器空间映射为一维。
其次,纹理缓存是只读的,并且不满足缓存数据一致性。当显存中的纹理被修改以后,纹理缓存中的数据并不会被修改。每次修改纹理必须更新整个纹理,而不是纹理中被修改的一小部分。
第三,纹理缓存的主要功能是为了节省带宽和功耗,而CPU的缓存则是为了实现较低的延迟。纹理缓存提高不了随机访问数据的性能,也无法减小访存延迟。但使用纹理缓存可以通过利用数据的局部性提高性能,节省存储器带宽。
GT200中,每个TPC拥有的一级纹理缓存是24KB,但是被分为三个8KB的块。L2纹理缓存位于存储器控制单元中,每个大小是32KB,所以整个器件已共有256KB。
使用装载流水线和存储流水线对global的存储器访问时没有缓存,因此显存的性能对GPU至关重要。影响显存访问效率的因素主要有三个:对齐,合并访问,以及分区。
为了高效的访问显存,读取和存储必须对齐到4Byte宽度,也就是说每个数据占用的存储器空间必须是4Byte的整数倍,否则读写将被编译器拆分为多次操作,极大的影响效率。
一个half-warp的读写操作如果能够满足合并访问(coalesced access)条件,那么多次访存操作会被合并成一次完成,从而提高访问效率。
在1.0和1.1器件的合并访存条件十分严苛。首先,访存的开始地址必须对齐:16x32bit的合并必须对齐到64Byte(即访存起始地址必须是64Byte的整数倍);16x64bit的合并访存起始必须对齐到128Byte;16x128bit合并访存的起始地址必须对齐到128Byte,但是必须横跨连续的两个128Byte区域。其次,只有当第K个线程访问的就是第K个数据字时,才能实现合并访问,否则half warp中的16个访存指令就会被发射成16次单独的访存。
在新的1.2版本以上的硬件的存储器控制器有很大改进,更加容易实现合并访问。这些改进并不会提高GT200的纸面性能,但在某些应用中却可以将性能提高一个数量级。
首先,一次合并访问中的线程编号和数据字位置不需要相等,顺序可以随意。
其次,新的存储器控制器可以支持对8bit和16bit数据字的合并访问(分别使用32Byte和64Byte传输)。
最后,存储器控制器可以首地址没有对齐的访问进行拆分。当访问128Byte数据时如果地址没有对齐到128Byte,在老版本硬件中会产生16次访存指令发射,而在新版本中只会产生两次合并访存。例如,一次128Byte访存中有32Byte在一个区域中,另外一个区域中有96Byte,那么只会产生一次32Byte合并访存(对有32Byte数据的区域)和一次128Byte(对有96Byte数据的区域),而不是两次128Byte合并访问。
分区冲突(partition camping)问题是由多个存储器控制器之间不均衡引起的。Tesla架构中的每个存储器控制器可以提供64bit位宽,每个存储器控制器负责对若干片显存颗粒的操作,称为一个分区(partition)。整个GPU的带宽就是所有存储器控制器带宽之和。在G80数据在显存上的分配方式是:相邻的分区存储相邻的数据块,每个数据块的大小为256Byte。
如果从SPA产生的读取和装载指令能够均匀的被发射到各个存储器控制器,那么所有的存储器控制器就能同时并行工作,可用的带宽就比较大;如果所有的访问请求都被集中发射到少数存储器控制器,那么有效带宽就只由这一部分存储器控制器提供,而其他存储器控制器则处于闲置状态。
可以看出,global memory分区冲突的产生机制与shared memory的bank conflict类似,都是由存储器bank间的存储器访问请求负载不均衡引起的。分区问题是在一个比较大的宏观范围内产生的,与数据并行划分方法关系比较紧密;而bank conflict和合并访问问题则是在一个half-warp内的微观层次上产生的。一般来说,合并访问对性能的影响最大,在访存延迟被充分隐藏后bank conflict的影响开始突出,分区冲突的影响通常只有在转置等少数应用中需要考虑。
更多内容请点击:
CUDA专区:http://cuda.it168.com/
CUDA论坛:http://cudabbs.it168.com/