单个 - 精确矩阵乘法(SGEMM)几乎是学习CUDA的学生的所有情况。这个经典计算的密集情况可以显示GPU编程中常用的优化技术,以及它们是否可以编写高效效率,这也是对CUDA程序员对GPU体系结构的理解的绝佳测试。这篇文章将介绍CUDA SGEMM IN IN IN CUDA SGEMM IN IN IN CUDA SGEMM IN IN IN IN IN细节。它适合仔细阅读“ CUDA C ++编程指南”。我希望有一定的CUDA编程基金会的学生阅读,我希望激发追求极端表现的学生。
我采访了许多具有CUDA编程经验的学生的学校招募学生。在问问题以在CUDA编写SGEMM内核时,我通常会得到这样的答案:
当然,如此天真的内核不是作者所期望的,因为该内核的性能基本上可以确定即使是Cublas的1/10也不可用,这显然无法满足我们追求高性能的需求。这种天真的实施是吗?
分析代码我们可以看到一次计算一次FMA(乘数)需要读取A并阅读一次。众所周知,阅读全球记忆的成本非常高,通常是数百个周期(时钟周期),并且一次计算一次。fma通常只需要几个周期,并且在访问中花费了很多时间。周到的思想立即认为它们可以转移到共享内存(SM低删除的片上内存,内部线程共享,附加到NVIDIA GPU内存结构图)。这确实是一个好主意,但这只能从中降低数百个周期到数十个周期,这不会改变问题的本质。问题的关键是主周期由两个负载指令和FMA指令组成。计算指令仅解释了整体的三分之一,而计算访问者比率太低,最终导致访问的延迟无法隐藏,这不是理想的。
让我们敞开心mind。如果线程不仅计算一个结果,还计算出4x4结果并使用共享内存优化,那么热环将是什么样的?伪代码如下:
可以从从SMEMA到寄存器A_REG的阅读中获得分析,您需要执行4个访问操作。b相同的原因,然后主体的计算访问比例为16/8。与以前的情况相比,计算指令会计计算。比率大大提高。大型计算访问比率可以提高计算单元的利用率并在隐藏访问中起作用。我们可以进一步增强计算访问对于库存而言,使内核的性能接近理论峰值。
显然,我们不能仅使用一个块来计算超大矩阵,这会导致大量SM(流多处理器)闲置浪费。这需要对矩阵进行块计算,如下图所示:
不同的块大小在以不同形状的矩阵乘法应用中具有其自身的优势和缺点。本文选择128x128子块作为示例。
从上一节中,我们可以看到增强计算访问有很大的好处。计算访问率可以无限改善吗?答案是编号的,因为要改善计算访问,单个线程需要计算一个较大的块,这需要更多的寄存器,但是寄存器的数量有限。以图灵体系结构的GPU为例,总数单个SM寄存器为65536。由于指令编码的限制,单个线程可以使用的最大寄存器数为255,并且不使用寄存器的数量,因此您需要介绍一个概念占用(职业率)。职业是指每个SM.WARP中的活动线程数(Warp)的比率在隐藏延迟中起作用。每个SM中的活动纱数取决于块所使用的资源数量。每个线程使用的寄存器数量都与共享内存一起使用。可以通过CUDA Toolkit中提供的CUDA_OCCUPANCY_CALCULATOR.XLS工具获得Occupany。
考虑一个块块以计算128x128。如果计算每个线程以计算128个结果,则所需的块大小为128,单个线程需要128个寄存器才能存储计算结果,以及所需的GMEM,以示符号,smem to regs等。所需的寄存器至少需要180。计算占领可以知道,目前有活跃的战争数量仅为8,而占领为25%;如果将块大小设置为256,则每个线程只需要计算64个结果,调整寄存器和寄存器,然后调整寄存器和寄存器以及寄存器和共享内存的用法和观察占用,则可以看出,如果使用每个线程只有128个寄存器,块中的共享内存限制为32K,而活动战争的数量可以达到16。这是一个更好的选择:
此时,配置计算访问者比率可以达到64/4(读取向量读数),这足以隐藏访问的延迟。
在正常情况下,选择适当的块资源分配后,使用共享内存来减少访问的延迟并在循环中做得很好,SGEMM内核的性能可以达到良好的水平(80%的Cublas),但这不是我们的旅程。首先,我们可以使用向量读取指令来优化共享内存访问(对应于Float4数据类型),这可以大大减少访问说明的数量并进一步增加对库存的计算访问,因此我们需要将A矩阵保存到SMEMA中一次,然后进行一次。
同时,我们的内核计算为256个线程的128x128的划分。为了能够访问共享内存,我们将将256个线程绘制为两个维度,顺序:
并在共享内存中读取以下方式,如下所示:
最终单线计算2x2 4x4的结果,结果布局如图所示:
通过微观基准,可以检测到图灵(Tesla T4)全球记忆的访问约300周期,共享内存的访问延迟约30个周期需要充分利用Prefetch的想法将全球记忆隐藏在中间存款中,从全局内存到共享内存,并延迟从共享内存中对数据块的访问,以避免由于失速而延长单元的访问。最终伪代码如下:
注意:这里的懒惰假设M,N和K多次为4,如果不是4,则无法使用Float4读取全局内存,并且无法用Float4.Memory Exchange结果回报。MemoryExchange结果扭曲到确保每个扭曲都执行商店指令,以回写连续的内存空间。
在这一点上,我们得到了完整优化的SGEMM内核。此外,Ampere GPU添加了说明。从全局内存到共享内存的过程不需要中间寄存器来进一步优化SGEMM的性能。
为了避免Cublas选择分裂K的内核,我们将K固定为1024,并将M,N = 2048、4096、8192和16384作为测试用例。上述SGEMM内核和CUBLAS的性能(测试GPU为Tesla T4,锁定核心频率为1100):
可以看出,SGEMM内核的实施已达到Cublas 97.5%的平均表现。
在这一点上,可能仍然有一个问题,学生可能仍然有一个问题。我们似乎使用所有可以考虑的优化方法。为什么CUDA C内核写了?Cublas仍然存在一定的差距。部分不是NVCC编写的CUDA内核,而是由NVIDIA GPU(SASS)的汇编语言(SASS)编写的深度敲击版本。尤其是CUDA 11中的NVCC,已编译内核和手工组装和优化版本之间的差距已大大减少,但是登记库冲突的影响并充分利用了寄存器的重复使用(这两个概念将在此引入下面的详细信息),以便仍然存在差距。即使是伪隔离的语言,例如PTX,无法准确控制寄存器的分布,并且面临与Cuda C.的困难。因此,为了完全利用GPU的性能限制,GPU指令和登记册需要准确控制,并且必须由GPU本地组装语言SASS完成。g通过Microbench分类串联论文进行NVIDIA XXX GPU架构。Citadel的In -Depth研究。结论可能不准确,但通常具有很高的参考值。在同一时间,许多开源汇编,例如Kepleras,Maxas(最成熟,最成熟,,最成熟,,最成熟,,最成熟,,遥远的),图林斯(Turingas)和Cuassembler以及其他一系列开源SASS汇编,使使用Sass撰写高性能内核成为可能。
我们知道共享内存有银行冲突,并且寄存器的银行冲突也是一个类似的概念。NVIDIAGPU为每个SM有一个独立的寄存器文件,并且将寄存器文件分为几家银行。以麦克斯韦为例。如果来自同一银行的指令需要超过2个源登记册,则将产生冲突,并且指令威利特等同于重新发布和浪费周期。,并且寄存器是寄存器的归属(例如R0属于银行0,R5属于银行1)。图灵架构已得到改善。寄存器文件分为两家银行。每个银行都有2个端口。如果不是三个源寄存器ID和奇怪的木偶,就不会发生冲突,这大大减轻了注册银行的冲突。
在Maxas的Maxwell SGEMM SASS内核中,为了减轻注册银行冲突,对FFMA计算中涉及的登记册进行了精确分配(请参阅Maxas的SGEMM文档),如下图所示:
C巧妙地排出C后,登记银行冲突大大减少了,但仍无法完全避免(如上图中的黑色框架徽标部分所示,A/B使用的登记册将产生银行冲突)。取消。
登记册的重用是NVIDIA开始在Maxwell引入的机制,以减轻登记银行冲突。NVIDIA的收集器单元读取指令操作的数量添加了寄存器的重复使用缓存。仅读取recuse缓存,以获取操作数是否由缓存的控制代码指定(Maxas的Control Code wiki of Maxas)。使用CuobjDump编译一些内核,以在某些寄存器背后找到一些标志。
但是,使用需要满足某些条件(在重写之前不会设置寄存器),并且将重复使用标志随机设置可能会获得历史值,从而导致计算错误。根据作者的理解,它更像是在重用Cachehold的徽标中寄存器的价值。NVCC编译CUDA内核还使用REUSE CACHE避免了一些登记库冲突,但是由于寄存器分配和指示,重复使用的利用率,不高。要制定统计数据,可以发现重复使用缓存仅达到约20%,而Maxas的Sass内核可以按设计按设计达到49%。
SGEMM内核的性能最终由Sass察觉,可以完全超过Cublas。有兴趣的学生可以在Maxas中编译SGEMM内核来测试Maxwell或Pascal GPU。尽管如此,尽管使用SASS可以完全挖掘GPU的性能,但有三个主要问题:1。第三党NV GPU汇编器取决于逆向研究GPU架构。2.编译内核很难开发,更难调试;3.每一代GPU的NV的ISA(指令集)不同。有必要连续开发相应的组装和组装内核。由于存在这些主要问题,因此使用SASS编写内核是一项耗时的工作。除非需要追求极端性能,否则不建议轻松尝试。
我们都知道,IM2COL可以作为矩阵乘法实现优化卷积计算。对于上述SGEMM内核,您只需要将全局内存数据移至共享内存。IM2COL映射,SGEMM内核变成计算Conv的内核,这是Cudnn卷积的隐式GEMM算法。在IM2COL过程中,如果IM2COL过程,则指针的偏移是直接计算的,将引入大量密集的去除并剩余的计算。作为内核中的参数传输,您可以在需要时从常数内存中读取以避免完整性和去除。实施隐式的预科gemm。有关更多详细信息,请参阅我们以前的文章Megengine Tensorcore卷积实施原则。在此想法的指导下,我们已经基于Cutlass实现了有效的INT8/INT4卷积计算(Megengine Cutlass),并在连续的迭代中获得。
本文详细介绍了如何编写高效率CUDA SGEMM内核,并介绍了使用SASS编程的极端优化性能的手段,并稍微扩展了通过具有硬件性能的隐式Gemm.Students优化卷积计算的想法最终,欢迎所有学生加入Megengine团队,以优化共同学习的培训和推理表现。
作者:Ma Jun |查看Megengine建筑师