开发者
下载
[object Object]

【优先级】高

【描述】SIMT编程模式下,一个Warp内的相邻线程(thread)通常同时发起GM访问请求。若这些线程访问的GM地址连续,硬件可以将访问请求合并,从而提高GM带宽利用率;若相邻线程访问的GM地址跨行或离散分布,则难以形成访存合并,算子执行耗时会明显增加。对于转置、重排、gather/scatter等算子,需要结合数据排布和线程映射关系进行分析,尽量让同一个Warp内相邻线程在同一条访存指令上访问连续地址。

图 1 访存合并对比示意图

[object Object]

访存合并优化适用于两类场景:第一类是数据本身在GM上连续,只是相邻thread被分配处理的数据不连续,导致同一个Warp内相邻线程访问了不连续地址,此时优先调整数据切分方式,使Warp内相邻thread访问连续数据;第二类是算子功能包含转置、重排等布局变换,单纯调整切分方式很难同时保证GM读写都连续,此时考虑引入UB内存空间作为中转,利用UB完成不连续的数据重排过程,从而尽量保证GM侧访问连续。

  • 优化数据切分实现访存合并

    【样例介绍】以Gather算子为例,算子按行从输入中取数,计算公式可以表示为 [object Object]。输出矩阵每一行内部的列方向数据在GM上连续,优化切分时应让同一个Warp内相邻线程尽量访问同一行内的相邻列,而不是只保证单个线程内部连续。

    【反例】按thread切分数据,每个thread处理连续的一行数据。

    [object Object]

    上述实现把输出行分配给不同thread,每个thread在 [object Object] 循环内连续处理一整行数据。从单个thread视角看,[object Object][object Object] 都是连续递增的;但SIMT访存合并的关键是同一个Warp内相邻thread在同一条访存指令上访问的地址连续。在该实现中,同一个Warp内的相邻thread分别处理不同的 [object Object],虽然单个thread内部访问连续,但Warp内访问不连续,GM访存难以合并。

    以输入shape为20001 x 1023,index长度为核数乘以2048为例,线程块总数与物理核数相等,每个线程块启动2048个线程,反例算子执行该用例的性能数据如下:

    [object Object]undefined

    【正例】按ThreadBlock切分输出数据,Warp内相邻thread访问连续地址。

    [object Object]

    上述实现将输出矩阵按一维线性地址组织,并让每个ThreadBlock覆盖一段连续的输出元素。对于同一次循环迭代,ThreadBlock内thread的 [object Object] 连续递增,同一个Warp内相邻thread会访问连续的 [object Object]。当这些 [object Object] 位于同一个输出行内时,[object Object] 相同,[object Object] 连续递增, [object Object] ,因此 [object Object]也相同,输入访问 [object Object] 的地址也是连续的,输入和输出都可以实现访存合并。

    在正例的代码中,每个thread在整个过程中并非处理一段连续数据,而是以 [object Object] 为步长处理多个元素。也就是说,单个thread多次迭代访问的数据是跳跃的;但在每一次迭代中,Warp内相邻thread访问的是连续地址。SIMT访存合并的收益来自Warp维度的连续访问,因此这种切分方式通常比“每个thread处理一整段连续数据”更能发挥GM带宽优势。

    配套的ThreadBlock切分也从按 [object Object] 切分调整为按 [object Object] 切分,使线程数量覆盖真实输出元素规模:

    [object Object]

    正例算子实现访存合并,执行该用例的性能数据如下:

    [object Object]undefined

    从Task Duration看,访存合并后的执行耗时为866.103us,相比优化前的4886.076us,下降约82.3%。这说明优化前的主要瓶颈来自Warp内相邻thread访问的GM地址不连续,调整数据切分后,相邻thread在同一次访存指令上访问相邻元素,GM访存效率得到明显改善。

  • 引入UB中转实现访存合并

    【样例介绍】以矩阵转置为例,输入矩阵shape为1024 x 1024,数据类型为 [object Object]。矩阵转置先将矩阵划分为多个固定大小的子矩阵块进行处理,这里的子矩阵块称为tile。本样例采用32 x 32的tile,每个ThreadBlock负责处理一个tile。Grid配置为 [object Object],ThreadBlock配置为 [object Object],每个thread处理tile中的1个元素。该切分方式下,[object Object]方向的32个thread对应tile内同一行的32个连续元素,适合形成连续GM读访问。

    【反例】直接按照转置公式写回GM。

    [object Object]

    上述实现中,同一个Warp内的thread从输入矩阵的一行读取数据,[object Object]连续,因此GM读访问可以合并。但写回GM时,[object Object]相邻的thread对应不同的 [object Object][object Object]会让相邻thread写到输出矩阵的不同行,地址间隔为 [object Object]个元素。此时写回GM的地址不连续,难以形成高效合并访存。

    在1024 x 1024矩阵转置样例中,直接索引转置的算子性能数据如下:

    [object Object]undefined

    【正例】使用UB中转,将非连续GM写转换为UB内转置读。

    [object Object]

    上述实现先让每个thread按原始布局从GM读取数据,并写入UB中的 [object Object]。同步后,交换ThreadBlock坐标定位到转置后的输出tile,再从UB中读取 [object Object]并写回GM。这样处理后,同一个Warp在GM侧读取输入的一行,也写回输出的一行,GM读写都保持连续;转置造成的非连续访问被转移到UB内部完成。

    在1024 x 1024矩阵转置样例中,使用UB中转后的算子性能数据如下:

    [object Object]undefined

    从Task Duration看,使用UB中转后的执行耗时为35.945us,相比直接索引转置的60.477us,下降约40.6%。这说明该转置场景的主要收益来自将原本不连续的GM写访问转换为UB内转置读,使GM读写两侧都尽量保持连续访问;虽然引入UB会带来一次中转和线程同步开销,但在该用例中,GM写访问合并带来的收益超过了这些额外开销。

【总结】SIMT算子进行访存优化时,应优先分析同一个Warp内相邻thread在同一条访存指令下访问的GM地址是否连续。常用优化方法包括:一是调整数据切分方式,将连续的数据区域分配给ThreadBlock和Warp,使每次迭代中Warp内相邻thread访问连续地址,单个thread可通过stride处理后续数据以覆盖完整输出;二是引入UB进行数据中转和布局重排,将原本不连续的GM访问转换为UB内的重排访问,从而尽量保证GM读写都沿连续方向进行。