l 论文四摘要
n 题目:Fast Implementation of DGEMM on Fermi GPU
n 摘要:
u 使用了分块算法(在shared memory和register上)实现了双精度的稠密矩阵乘法(Fermi架构)
u 优化内容:软件流水线,向量存储操作,指令调度
u 性能:在最新的CUBLAST基础上,提升了20%,达到362Gflop/s
n 内容:
u 介绍
l 稠密线性代数库的简要介绍,提出这些库没有解决的两个问题:
n 性能提升的背后使用了什么技术细节?
n 还有多少性能提升的空间?
l 主要的工作
n 制订了一个用于选择(Fermi架构上)分块矩阵乘算法的性能模型
n 讨论三种优化策略以发掘Fermi的计算潜能
n 展现了在Fermi架构上对DGEMM进行代码调优的试验性经验
u 背景
l Fermi GPU架构
n 在DRAM和流处理器之间加多了一层缓存(L1,L2),与shared memory不同,有助于减缓非级联访存带来的影响
n 每个SM有两个warp 调度器,意味着每个时钟周期可以发射两个warp,但是双精度指令无法与其他类型指令同时发射
n 内存操作的位宽增大:64bit/128bit,但是宽数据的转换会造成更高的延迟
l 基于GPU的DGEMM
n 两个层次的分块(数据重用):shared memory blocking,register blocking,对应的注意事项:bank conflict,平衡每个线程的register个数
u 实现方式选择和分析
l 实现:理论分析需要的存储带宽为RB(DF*WB*1/S,最小化后为:DF*WB*1/bn)
n DF:每秒钟的浮点数操作个数,WB:每个数据的大小(字节为单位)
n Shared memory blocking
u 由于DF=515,BW=144,故bn>28,而实验室测试后发现bn应该为16的倍数,加上CUDA建议512B对齐,故选择bn为64
u 有效的带宽在每组16个线程都使用相同的128bit对其地址时,达到最大值;由于128bit的浮点数操作会导致bank conflict,所以,采用的版本为64bit的浮点数操作
n Register blocking
u 一方面:共享存储器带宽有限,计算寄存器块的大小,RB=(DF*WB)*(1/rx),故rx>=4
u 一方面:寄存器个数有限,对于每个线程,2(rx*ry+rx+ry)<63,由于rx=ry,故最多的时候rx=4
u 从而:bm=bn=64,rx=ry=4,由于共享存储器为48K,每个SM保证运行2个线程块,选择bk使其满足:2*2*64*bk*8B<48KB,故bk=16
u 分析:为了是每个SM支持多于2个线程块,每个线程块大小需要被限制在32768/(48*2)=341以内,这里选择了256=64*4,所以,对于每一个bm*bk大小的块,每个线程需要读取4个元素
l 分析
n 在上面的算法中:
u 算法1因为global memory的访问没有很好地被缓冲,导致占用了大部分的时间;
u 算法2使用软件预取改进后,在每次计算当前元素的同时,将下一次需要加载到shared memory中的数据先从global memory加载到寄存器(新增了一倍)中,然后再进行运算,运算完毕之后,将寄存器中的值放入shared memory中(整个过程跟《实战》中的手法一样),这样,计算和访问global memory的过程就重叠在一起,可以使用计算来掩藏global memory的latency,性能有所提升;但是,算法2种增加了寄存器后,由于寄存器不足,导致溢出到local memory中,同样造成了性能的下降,所以,性能的总体提升并不明显
u 总结的经验是:软件预取的时候尽可能少地增加寄存器(这也为后面的双缓存实现提供了指导)
n 拇指原则:
u 在一个应用中浮点数的吞吐量取决于浮点指令的百分比,所占的百分比越高,我们能够达到的吞吐量上限就越高
u 在算法1种,浮点数的百分比为64%,而运行结果已经达到了58%,很难提升
u 注意到在CUDA3.2中,Fermi已经支持128bit的浮点数读取和写入操作,这样,可以是的读取和写入部分所占的比例降低,从而提升浮点指令的百分比到78%
u 由于128bit的浮点数读取和写入会造成bank conflict,所以,需要寻找一种方法使得这种由于128bit浮点数读写引起的延迟能够被隐藏
u 优化
l 本节讲述如何使用软件预取而无需用到额外的寄存器,并且加入128bit读写指令,采用了三种优化技术:数据布局,线程映射,双缓存
l 数据线程映射
n 每个线程原来只读取一个double大小,使用了128bit的指令后可以读取2个double大小,这样,线程块的映射由原来的64*4变成了32*8,同样的读取次数,可以读取的数据多了一倍,在图7的例子中,同样是读取64*16个数据,前者需要4个指令,后者只需要2个指令,降低了非浮点数操作的指令
l 双缓存
n 将同一个block中的数据计算分成两个阶段来计算,而在每个计算阶段的同时,也为下一个计算阶段需要的数据做准备(直接从global memory读入shared memory),这样能够使得计算和准备的数据由于独立性而同时进行,让计算掩藏了数据的读取延迟,达到数据预取的目的而又不增加寄存器的数量
l 指令调度
n 通过对不同的寄存器进行计算stall time,从而调整指令的顺序(超出本文范围),在本文中,算法3被转换成了汇编指令,内部循环完全展开,然后对这些汇编指令进行重新排序,通过分析汇编程序,将具有读取延迟的指令插入到合适的点,最小化stall time(寄存器的等待时间)
u 试验结果和分析
l 性能
n 最终的版本使用了双缓存和指令调度连个技术来节省寄存器并且隐藏延迟,性能比CUDA3.2高出了20%,但是提高性能的前提是我们需要手动调整汇编代码。
n 主要的调整策略:将读取数据的存储指令插入到shared memory中,将从global memory中读取数据然后存储起来的存储指令插入shared memory中。
n 四个版本:
u 版本1:基于算法3,使用128bit的存储操作,不使用双缓存技术,每个循环中有一个同步
u 版本2:基于算法3,直接转化为汇编代码,没有任何的指令的调度优化
u 版本3:基于版本2,内部的for循环指令全部重新调整顺序,其他不变
u 版本4:基于版本3,使用指令调度来优化global memory访问的延迟,也就是最终版本
n 其他:对于Fermi,寄存器溢出的情况并不严重(Fermi使用了cache来缓存local memory中的数据),所以,双缓存策略在Fermi上对性能提升的效果并不明显;global memory的访问一直都是一个主要的性能优化因素,几个版本的提升在此处的改进最为明显;虽然Fermi可以进行双warp发射,但是,double类型的数据无法同时操作,所以,估算的时候仍然只用一倍的比例
l 讨论和一般化优化
n 128bit的内存指令的使用能够提高浮点数的运算潜能(需要与后面两种策略相结合才行)
n 双缓存策略可以减少寄存器的使用,隐藏长的存储延迟
n 最有效的优化策略是指令调度,小部分的指令经常占据了大部分的运行时间,所以,对于指令的调整通常能够很有效地提高性能
l 架构上的影响(对GPU架构的建议)
n 增加每个线程的寄存器
n 增加shared memory的带宽
n 增加指令的吞吐量
n 更有效的指令调度
u 相关工作
u 总结
l 本文的DGEMM是单Fermi GPU上最快的,使用128bit的指令会减少读写内存指令次数,但是会增加延迟,使用这个特性会导致需要花费更多的精力在指令的调度上
n 关键点:
u 分析出最佳的shared memory block和register block大小
u 优化过程:128bit读写内存操作的使用,数据线程映射,双缓存技术的使用,汇编指令的调度
n 能否扩展:
u 对指令调度方面的自适应实现?
u 对两个层次的block大小的自适应实现?
n 疑问:
u 仍然没有多warp divergence,bank conflict和texture & constant memory的描述