网站备案 前置审批号,郑州市有做网站的吗,家电网站首页制作,青岛做网站推广摘要本文主要记录了使用Nsight Compute排查CUDA矩阵乘法性能瓶颈的过程。本文首先简单介绍了Nsight Compute这一工具#xff0c;然后使用一个实际案例演示了如何使用该工具精确排查是哪一行代码造成的Bank Conflict#xff0c;并展示了该问题解决后的结果。前情提要本文是CUD…摘要本文主要记录了使用Nsight Compute排查CUDA矩阵乘法性能瓶颈的过程。本文首先简单介绍了Nsight Compute这一工具然后使用一个实际案例演示了如何使用该工具精确排查是哪一行代码造成的Bank Conflict并展示了该问题解决后的结果。前情提要本文是CUDA矩阵乘法系列文章的一个副产物主要是记录一下使用NVIDIA的Nsight Compute工具进行性能瓶颈定位以及修复的全过程。本文的故事还得从我在实现Kernel 6时说起相关上下文见本公众号内的系列文章上篇参考文章作者在Kernel 6里用了两个技巧一个是向量化加载另一个是把部分数据读取到REGS里减少SMEM的读取次数于是我在实现了向量化加载后测了一波数据又在实现了加载REGS测了一波数据发现两个优化叠加之后性能反而不如前者。当时初步排查了一下确认不是REGS空间不够导致的问题于是这个问题就成为了一个遗留问题。再到后面实现Kernel 10时遇到的问题更棘手了参考文章作者并没有解释清楚Kernel 10的性能优化来源我在按照自己的理解实现了一个版本后发现性能反而不如Kernel 6了很奇怪但是运行参考文章作者的Kernel 10却又很大的性能提升。在这几个问题解决之前是肯定不能开始动笔写下篇的所以我就开始了各种尝试包括怀疑是求坐标的取模运算导致的性能损耗尝试换掉取模运算等但是最终问题都没能解决于是就这样被卡了两周。这里问题的关键在于我们不知道自己的实现和参考文章作者相比慢在了哪里现在已知的信息就只有一个端到端耗时。这时候就需要一个Profiling工具来拆解内核执行时每个阶段的耗时数据从而来协助我们定位具体的问题所在。Nsight Compute正是这样一个工具这一工具能够为开发者带来非常详尽的性能数据这一数据能具体到每条指令的执行次数整体的访存次数甚至Bank Conflict的发生次数都能看到。这里真得夸一下NVIDIA这是我见过的数据最详细的性能分析工具而且GUI的可视化也做的非常精美比如像下面这张内存访问示意图这确实是很惊艳了。image本文会简单介绍一下这一工具的使用方法然后会以本次矩阵乘法内核性能优化为例子展示一下使用这一工具来实现CUDA Kernel性能优化的全过程。Nsight Compute简介Nsight Compute是NVIDIA官方开发的一个CUDA性能分析套件可以进行指令级别的性能数据分析。逻辑上讲Nsight Compute这个工具可以被分为采集端和分析端采集端会实际执行Kernel代码采集数据然后把数据保存到一个文件中分析端则是读取数据然后对数据进行可视化展示。Nsight Compute在全平台上都能安装但是在没有NVIDIA显卡的平台例如Mac平台就只能使用分析端软件如果是安装在有NVIDIA显卡的平台则可以使用ncu指令来进行数据采集。如果本机没有NVIDIA显卡例如本文所面临的情况显卡在云服务器上可以使用这样一个工作流首先在服务器上使用ncu指令完成数据采集然后把数据文件下载到本机使用本机的Nsight Compute GUI打开数据文件进行分析。ncu指令的使用方法ncu指令支持我们通过--set参数指定采集的数据类型这里我们的Kernel比较小运行时间不长可以选择使用--set full全部采集。直接使用sudo ncu --set full -o my_kernel_prof ./cuda_perf即可开始对cuda_perf进行数据采集。在程序运行结束后下载当前目录下的my_kernel_prof.ncu-rep使用GUI打开即可开始分析。ncu采集不到详细数据如何解决如果遇到下图所示的情况提示No section files found in search paths那就是ncu没能找到配置section的路径image此时使用GUI打开只能看到很少量的数据如下图所示image此时打开Details也是看不到任何信息的image这时候需要手动指定section文件夹这个文件夹一般在Nsight Compute的安装目录下例如我的云服务器上就是/usr/lib/nsight-compute/sections这个目录下有很多.section文件如下图所示image使用指令sudo ncu --set full -o my_kernel_prof -section-folder/usr/lib/nsight-compute/sections ./cuda_perf就能够愉快的开始采集数据啦。ncu指令的其他坑这里再分享几个在使用ncu指令采集数据时遇到的坑比较老的显卡是不支持使用ncu采集数据的比如本文实验用的Quadro K620autodl上的服务器也是不支持使用ncu采集数据的可能是因为容器部署的关系需要使用root权限运行才能够完成数据采集如果手里没有合适的环境最方便的解决方法就是上阿里云或者腾讯云买一个按量付费的GPU服务器成本大概是9块一小时测完数据就关机的话实测开销也不会很高。Nsight Compute GUI的使用方法Nsight Compute GUI能够展示的数据种类非常丰富碍于篇幅原因本文仅介绍和此次内核优化相关的部分。这一小节会简单展示一下工具的操作方法以便于想要复现的朋友上手实操一下。为了保障阅读思路的连贯性具体的数据含义和分析会放到后面的实战部分展开介绍。参考资料目前网上已经有了一些比较完善的Nsight Compute使用教程感兴趣的朋友可以参考一下这些资料《【CUDA进阶】深入理解 Nsight System 和 Nsight Compute》链接https://www.bilibili.com/video/BV13w411o7cu/《CUDA-MODE 第一课课后实战上Nsight Compute》链接https://zhuanlan.zhihu.com/p/707107808《Tools(2): Nsight Compute 使用指南》链接https://zhuanlan.zhihu.com/p/715022552Summary与Details界面打开NVIDIA Nsight Compute把获取到的.ncu-rep文件拖到窗口内即可打开。在一开始的Summary界面可以看到ncu采集到的所有Kernel由于我们只运行了一次矩阵乘法所以只采集到了一个名为MatmulKernelV10的Kernelimage双击这个列表项就能进入到下图所示的Details界面image这里面展示了很多的数据可以点击Tab栏左边的展开符号展开这一列表查看更详细的信息。例如点击Memory Workload Analysis左边的展开按钮后能看到如下所示的图表。这个图表很清晰的展示了每个内存区域的数据转移量以及相应的访存请求数量imageimage甚至图表上方的警告处还会贴心的告诉你当前访存方式可能存在哪些问题以及该如何优化image展开Warp State Statistics还能看到参考文章作者所展示的Warp State图imageSource界面注如果需要关联指令和源代码则需要在使用nvcc编译时添加-lineinfo参数这个界面就是对某一条具体的指令进行性能分析的地方了初始的Source界面如下图所示image左边是展示源代码的地方点击Resolve选择源代码文件后就能完成加载imageResolve之后点击源代码的某一行其右边对应的汇编代码就会高亮。可以发现界面上除了指令还有一系列数据可以看到现在的数据都是以百分比展示的点击这个按钮可以切换展示方式image以Instructions Executed这个指标为例鼠标指针移动到这个Tab栏上能够看到这个指标的定义image一行代码会被翻译成多条汇编指令源代码窗口里Instructions Executed的值就是这些汇编指令的值的和所以左右两个窗口可以认为是等价的因此我们可以直接只看左边源代码窗口里的数据image例如这里的2.15B表示这行代码对应的指令被执行了2.15 Billion次。实战演练接下来我们会通过一个实际的例子来展示Nsight Compute这一强大工具的用法。问题描述这里再更具体地描述一下我们需要解决的问题我们自己实现了一个矩阵乘法内核记为V10然后还有一个参考文章作者实现的一个矩阵乘法内核Author_V10。Author_V10的性能是要显著高于V10的现在需要弄清楚V10究竟是哪里慢了并尝试修复这些性能瓶颈让两者的性能一致。具体而言V10版本的性能是470 GFLOPS而Author_V10的性能是582 GFLOPSV10版本慢了接近20% 初步定位首先使用ncu在服务器上分别对V10和Author_V10进行数据采集然后在本地打开采集到的数据文件。由于我们已经知道CUDA矩阵乘法慢大概率是访存没做好导致的所以我们首先就打开Details界面的Memory Load Analysis对比两个版本的数据果然发现了端倪在Shared Memory那一块V10的Load Bank Conflicts数量有2亿多image而Author_V10的是惊人的0image定位Bank Conflict定位方法那么怎么确定具体是哪条指令导致了Bank Conflict呢在网上搜索时发现了NVIDIA开发者论坛里的这两个帖子https://forums.developer.nvidia.com/t/problems-about-profiling-shared-memory-bank-conflicts-using-nsight-compute/201393https://forums.developer.nvidia.com/t/shared-memory-bank-conflicts-and-nsight-metric/115731大致的意思是可以看Source面板里的L1 Wavefronts Shared和L1 Wavefronts Shared Ideal的差值如果差别特别大那大概率就是发生了Bank Conflict。那么这两个指标的含义是什么呢这里贴一段官方的解释memory_I1_wavefronts_sharedNumber of wavefronts in L1 from shared memory instructions.wavefronts: Unique work package generated at the end of the processing stage for requests.All work items of a wavefront are processed in parallel, while work items of different wavefronts are serialized and processed on different cycles.At least one wavefront is generated for each request.这里涉及到两个陌生概念一个是L1另一个是Wavefront我们来分别看看这两个分别是什么。L1 CacheL1就是处理器的L1 Cache这个可以和CPU类比我们在计算机组成原理课程里学过CPU如果要对内存里的两个数字进行运算就需要按顺序把数据从内存加载到L2 CacheL1 CacheRegs中然后才能调用ALU等单元对Regs里的数据进行计算操作。GPU里同样有这样的分级缓存机制和CPU稍微有些不同的是GPU里的Shared Memory是可以直接和L1 Cache进行数据交换的。所以这里的L1 Wavefronts Shared其实就是指的L1 Cache和Shared Memory之间数据交换产生的Wavefronts。Wavefront那么什么是Wavefront呢Wavefront可以理解为在硬件视角下实际要执行的操作数量。GPU会对一些特定的内存访问做优化例如上一篇文章里提到的Memory Coalescing即如果一个Warp里访问的内存是连续的32个字节那么处理器只会做一次访存操作此时就只会产生一个Wavefront但是如果不是连续的32个字节那么最多就会产生32个Wavefront。再例如如果一个Warp里的32个线程都访问同一个内存区域那么硬件在执行时也只会做一次访存操作此时我们看到的Wavefront也是只有1个。在理解了L1 Wavefronts Shared的含义之后L1 Wavefronts Shared Ideal的含义也就明确了前者是实际产生的Wavefront数量后者是理想情况下的Wavefront数量。对于这个“理想情况”官方的描述是Ideal number of wavefronts in L1 from shared memory instructions, assuming each not predicated-off thread performed the operation.这里的描述只提到了对于if-else谓词的假设并没有提到内存访问模式相关的假设AI给出的解释是这里是以完全没有Bank Conflict为前提进行的计算。所以L1 Wavefronts Shared Ideal可以理解为没有Bank Conflict的情况下的Wavefront数量。所以L1 Wavefronts Shared和L1 Wavefronts Shared Ideal的差值就代表Bank Conflict发生的数量差值越大的地方Bank Conflict就越严重。问题显露那么接下来的任务就很简单了只需要打开Source界面查看每行代码对应的L1 Wavefronts Shared和Ideal。注意到在加载Bs到Regs时实际的Wavefront比理想情况多了一倍说明这里存在Bank Conflict。image相对应的参考文章作者在加载Bs的时候并没有遇到Bank Conflict说明确实有方法可以避免这一情况。image紧接着还有一个更关键的问题把这里的Bank Conflict优化掉了能提升多少性能呢这里确实很难根据现有信息得出一个结论但是可以猜测优化掉了这个Bank Conflict之后会有较大的性能提升。这个猜测主要是基于Warp State图做出的如下图所示image这里Stall MIO Throttle的周期非常大这个Stall在本场景下大概率就是SMEM访问引起的。此外在Details - Memory Workload Analysis里面可以发现V10版本的SMEM访存Wavefronts一共有670M如果解决掉这个Bank Conflict可以直接变到400M左右这个优化带来的性能提升应该是比较大的。当然真实的性能提升数据只有实际运行之后才能知道了所以接下来我们就着手解决这一问题。解决Bank Conflict内存访问模式分析对V10的详细解析会在后面的文章里展开这里只需要知道以下两个设定就能够不影响接下来的阅读V10会把一个Warp里的32个线程按4 x 8的方式排列thread_x_in_block表示这个线程在这个4 x 8矩阵里的行数thread_y_in_block表示列数即可常量 TILE_ROW_SIZETILE_COL_SIZE8我们先简单分析一下V10的访存模式关键的SMEM访问代码如下所示for (uint32_t tile_row 0; tile_row TILE_ROW_SIZE; tile_row 4) {reinterpret_castfloat4 *(a_reg[tile_row])[0] reinterpret_castfloat4 *(As[asIdxTranspose(i, thread_x_in_block * TILE_ROW_SIZE tile_row)])[0];}for (uint32_t tile_col 0; tile_col TILE_COL_SIZE; tile_col 4) {reinterpret_castfloat4 *(b_reg[tile_col])[0] reinterpret_castfloat4 *(Bs[bsIdx(i, thread_y_in_block * TILE_COL_SIZE tile_col)])[0];}在访问As时以tile_row0i0为例由于thread_x_in_block只有0,1,2,3这四个取值因为Warp线程布局是4*8的只有4行所以访问As时实际上只会有4次访存分别为As的第0,8,16,24号元素。注实际上这里每次访存会访问4个元素是因为代码里使用了float4这个数据类型这会导致访存时使用128位的向量化加载但是容易验证这对最终的结论没有影响为了不影响阅读这里就暂时忽略这个float4这个设定了。可以注意到上述内存访问完全没有Bank Conflict发生这也能和Nsight Compute里展示的数据相对应。但是在访问Bs时情况就有所不同了还是以tile_col0i0为例由于Warp的布局是4*8所以thread_y_in_block会有8种取值。这就导致访问Bs时会有8次访存分别访问Bs的0,8,16,24,32,40,48,56号元素其中0和32产生了Bank Conflict8和40产生了Bank Conflict......。总共有4对Bank Conflict发生最理想的情况下也需要进行分两批才能完成。所以Nsight Compute里显示的Wavefronts比理想情况多了一倍这也是能和这里的理论分析相对应上的。解决方案这里最佳的解决方案是把TILE_COL_SIZE改成4这样的话访问Bs时访问的元素就是0,4,8,12,16,20,24,28完全没有了Bank Conflict。这也是我认为Author_V10的性能得到提升的根本原因个人认为参考文章作者在文章里提到的Warp Tiling本质上就是为了把TILE_COL_SIZE改成4从而避免访问Bs时的Bank Conflict这一点会在后面的文章中分析Kernel 10时展开阐述。最终效果最终的性能数据如下图所示可以看出修改后的V10性能虽然有了较大的提升。由此可见Bank Conflict带来的性能损耗有多严重。image总结Nsight Compute是一个强大的性能分析工具其在分析Bank Conflict时可以精确到具体的代码行数非常实用。Bank Conflict往往会在不经意间带来不可忽视的性能损失。