How_to_optimize_in_GPU_GEMM_(二)_评论分析

你好想问一下看起来并没有用异步的指令为什么可以实现数据预取呢

pipeline 双缓冲 pingpong操作,一个事情,都是为了实现计算和访存错开。在计算当前迭代的数据时,启动DMA 数据搬运操作,将下一次迭代的数据搬运过来。这样的话,每次启动计算的时候,数据是ready的,理论上就可以让计算单元被打满。
可以再体会一下,尤其是看看sass码,明显地可以看到对于同一个寄存器,FFMA指令和load指令中间间隔的指令多了。-> 也就是对于一个寄存器来说,FFMA指令之后就被线程调度去取共享内存中的数据了


大佬您好,希望百忙之中稍微在回复下。就是文中所说的“最后完成寄存器的预取,并将最后一个小迭代完成”。这里为什么会出现“最后一个小迭代?”是不是为了处理前七次迭代没有处理过的最后的一部分矩阵想乘? 如果是这样的话,为什么在此之前还需要“最后完成寄存器的预取”?我觉得没有必要预取了啊,因为第七次小迭代结束后不是已经预取好了数据嘛,最后一次直接拿寄存器中的数来算不就好了吗?

最后一个小迭代”是为了处理前七次迭代没有处理过的最后的一部分矩阵相乘。“最后完成寄存器的预取”这一部分是为下一次大迭代的第一次小迭代进行预取,不是为了本次大迭代的最后一次小迭代预取。


想问一下在代码块// load A from global memory to shared memory 部分,计算A的偏移量OFFSET的时候,为什么col是A_TILE_COL 而不是THREAD_PER_BLOCK_X * bx + TILE_COL_A

因为在上边的for循环中也指明了,当前的预取是在block块里边发生的,同时有N个block块中的线程同时进行处理。


大佬您好,我想请问下,这里每个线程都是先load下一个tile,再计算现在的tile,加载和计算之间看起来并没有并行关系。但我理解的double buffer是一些线程加载的同时另一些线程计算,最后能够pipeline起来。请问这是如何做到的?或者是我对double buffer的理解有问题吗?感谢!

因为SIMT架构的原因
我的理解是硬件资源的并行?线程只是用来调度加载和计算的寄存器处理,当然SIMT架构的原因是必然的。


不懂为什么要转置的同学可以看看这个zhuanlan.zhihu.com/p/44

随后看,目前我认为转置的原因是之后计算时索引计算更方便?期待看完打脸。待做+1


请问题主有没有写过双精度版本,我按照同样的思路但是只能达到80%左右的峰值效率,有点困惑?

双精度的话,我还没有测试过,不是太清楚。有兴趣的话可以基于我的代码改一改,看看单精度和双精度的性能差距?
双精度提上日程


请问一下,你在代码里是一次性读写4个float,这种是不是要求一些参数比如K,或者是THREAD_SIZD_X这些是4的倍数

边界需要单独处理一下
主要是在大矩阵的边界,小矩阵已经按4倍数取了


感谢大佬的分享,有个问题想请教一下:基于现在新的安培架构,是不是不再需要ldg_a_reg和ldg_b_reg这个两个寄存器了呢,是不是直接从global mem搬到shared mem就可以了

是的呢
比较重要的点,也就是从全局内存到共享内存不需要指明寄存器了,但是需要转置A的话我认为还是需要指明的。


想请教一下双缓冲的方案中frag_a和frag_b是同一个线程进行数据搬移和运算,和普通版的区别是double buffer在运算之前会将下一组数据提前搬运进来,然后对上一组数据进行运算。此处我有个疑问,我理解double buffer和普通版的差别就是在第一次运算r_c之前进行了两次数据搬运,剩下的运算和普通版好像没区别,请问double buffer这样做为啥会提升性能呢?我理解,double buffer是两个线程要相互协作,线程1搬运数据到缓存1的时候线程2取缓存0的数据运算,然后线程2从缓存1取数据的时候线程1向线程0搬运数据,然后就依此交错。 不知道是不是我理解的有误,麻烦大佬指点迷津

主要是减少了数据依赖,指令发射更通畅。可以看看汇编代码的差别。load数据和ffma指令的间距可以看出来。
其实问题非常好,这是操作系统的生产者消费者模型,但是在这里我还是同意是SIMT架构的原因,同时减少了数据依赖,指令发射更通畅。


for ( int i = 0 ; i < BLOCK_SIZE_K; i += B_TILE_ROW_STRIDE) {
 int ldg_index = i / A_TILE_ROW_STRIDE * 4;
   FETCH_FLOAT4(ldg_b_reg[ldg_index]) = FETCH_FLOAT4(B[OFFSET(
   tile_idx + B_TILE_ROW_START + i, // row
   B_TILE_COL + BLOCK_SIZE_N * bx, // col
    N )]);
}

for (int thread_x = 0; thread_x < THREAD_SIZE_X; ++thread_x) {
 accum[thread_y][thread_x] += frag_a[j%2][thread_y] * frag_b[j%2][thread_x];
}

意思是编译后这两个for循环可以并行执行?同时下轮访存和本轮计算?

我的理解应该是不可以的,理由同上。


老哥您好,有个问题向请教一下:在(github.com/Liu-xiandong)的第163~169行,这里总共从As、Bs中拿了8个元素,这似乎意味着核函数模板中的THREAD_SIZE_X和THREAD_SIZE_Y只能是8,而不能随意改变。请问核函数模板中的这两个参数是只能设置为8吗,谢谢老哥

v3那个gemm版本是已经定好参数的,所以不能改的


求教,为什么pre-fetch的时候,global mem要先放到寄存器中再挪到shared mem中呢[抱抱]

因为硬件的限制,在安培架构之前,global mem和shared mem没有直连,所以搬运逻辑就是先搬寄存器,再搬共享内存。


想请教一下A转置的目的是什么呢,可以提高性能吗?

为了访存连续,提高带宽
在别的地方看到的。当同一个warp中的所有线程都执行同一条指令访问全局存储器中连续的单元时,就获得最有利的访问模式。硬件检测到同一个warp中的这些线程访问全局存储器中连续的存储单元,并将这些单元结合成一个合并的访问。这里是从global中取数据,应该也是为了迎合这条规律->就是合并访存了


博主还想请教一个问题,请问最后的output写回global memory是在小迭代结束后还是在大迭代结束后呀?谢谢博主

在大迭代结束之后再写回
因为每一个线程都有它的寄存器可以保留全部所负责的计算元素


博主你好,从您的文章中学习到了很多。有一个问题想请教一下:像bm、bn、bk、rm、rn这些参数值是如何进行取值的呢?是针对不同硬件架构进行取值的吗?那对于同种硬件架构,这几个参数值就是固定的吗?谢谢博主~

这些取值需要针对不同的硬件架构和不同的输入矩阵规模。对于同样的硬件架构,输入的矩阵shape不一样,最优的参数配置也可能不一样。而且这只是最简单的几个参数配置,往深了做,有很多参数都是需要调整的
也就是cutlass cublas这些库都在玄学的原因


大佬,还有个问题打扰一下,就是咱现在的代码版本支持非2的整数次幂的尺寸的矩阵的运算吗

现在需要满足是8的倍数,暂时不打算支持其他的了[捂脸]


大佬,有个小问题一直想问一下,就是这个共享内存1288是如何取得的?根据经验判断吗?还有寄存器88选取的依据是啥啊?

这个问题涉及的东西非常多,主要考虑的有这么几个方面,一是让共享内存和寄存器资源分配合理,因为共享内存和寄存器数量的使用会限制活跃的warp数量,如果warp数量太少,会导致访存的latency难以被覆盖,二是让指令cache的使用更加合理,避免用了太多寄存器后需要更多的FFMA指令,有可能导致指令cache装不下。这个参数在不同的硬件架构不同的硬件型号里面都需要调整。


感谢博主,写的很好!想问一个问题,工业界或者类似于cublas这种库在做gemm运算的时候,都是基本像你这样只维护一个支持整数倍大小矩阵运算的核,当遇到其他大小的矩阵先做padding再通往核做运算的么?还是说他们使用的是其他优化手段?

官方库必须支持所有的可能情况,包括非整数倍大小。具体cublas怎么做的,不开源的也不太情况,但是一般而言,会在内部做padding处理。


楼主好,我目前正在学习本科的并行计算课程,非常感谢你的介绍!有一个小问题想请教您,就是我课上学到线程私有的数组是会被编译器放到off-chip DRAM上的,您是怎么保证他们被分配到registers的呢?

只要寄存器不超过使用的阈值,都是会放在片上的,这里每个线程用了200个左右的寄存器,是放得下的
学到了