本案例中的算子FlashAttentionScoreGrad,用于训练场景下计算注意力的反向输出,即FlashAttentionScore算子的反向计算。
已知注意力的正向计算公式为:
为方便表达,以变量S和P表示计算公式:
则注意力的反向计算公式为:
计算流程图如下:
按照FlashAttention反向计算流程的实现,简介整体计算流程如下。对本算子的算法感兴趣的用户可简单了解,无需重点关注。
本案例的验证平台为
流水优化分析工具包括CAModel和Profiling工具,分别从两个方面分析:第一个是从Profiling工具生成的Profiling数据中分析各项流水的占比,第二个是从CAModel工具生成的打点图分析各流水并行情况。
通过观察分析流水图和Profiling数据,结合优化经验来判断性能瓶颈点。在优化过程中不同阶段可能会出现不同的瓶颈点,需要不断优化以达到最佳性能。
1 2 3 |
pipe->InitBuffer(ubBuffer, 120 * 1024); pipe->InitBuffer(tmpBuffer, 30 * 1024); pipe->InitBuffer(vecClc3, 8 * 1024); |
如上代码所示,InitBuffer接口的第二个参数表示buffer占用的大小,所有buffer大小的和即为占用的总空间。这里120 * 1024 + 30 * 1024 + 8 * 1024 = 158KB < UB Size,没有充分利用UB空间。
在满足UB空间大小够用的情况下,tiling基本块切分的越大越好。如下图为优化前按照(64, 128)切分计算,总共需要循环计算32次。
考虑到UB空间没有用满,基本块调整到(128, 128),如下图优化后只需循环计算16次,切分后算子性能提升一倍。
由于FAG算子中Cube计算比Vector计算快且存在依赖性,同时为了减少CV之间的通信次数,通过缓存机制实现让matmul提前计算多块,这里的缓存机制指的是将mm一次性计算多个基本块缓存到GM上。如下代码中,SetTail设置的singleCoreM和singleCoreN大小分别为BaseM,BaseN的倍数,即matmul一次发起多个基本块的计算,实现matmul结果的缓存,Vector侧分多次取matmul的结果。
1 2 3 4 |
mm3.SetTail(s2CvExtend, -1, preS1Extend); mm3.SetTensorA(mulWorkSpaceGm[pingpongIdx * coreNum * cubeBaseMN + cBlockIdx * cubeBaseMN], true); mm3.SetTensorB(queryGm[mm2aTensorOffsetCv]); mm3.template IterateAll<false>(dkWorkSpaceGm[bTensorOffsetCv], true); |
如上图是实现mm1、mm2和mm3缓存的流水图,并行度提高,CV的间隔减小,提升了算子性能。
基于缓存mm1/mm2/mm3的优化后,在本轮Vector计算等Cube流水的间隔,插入下一轮循环的Vector计算,如上图所示,这样使Vector流水与Cube流水之间的并行度更高,反映到流水图中为Vector计算更密集。原计算过程伪代码与在CV间隔中插入下一轮Vector计算的伪代码,分别如以下两段所示。
1 2 3 4 5 6 7 8 |
// 原计算过程伪代码 // mm1计算; dropout(); Sub(); // mm2计算; Softmax(); AttenMask(); ... |
1 2 3 4 5 6 7 8 9 10 |
// 在Vector等Cube流水的间隔中,插入下一轮循环的Vector计算伪代码 // mm1计算; dropout(); Sub(); dropout(); // 下一轮循环的Vector计算 Sub(); // 下一轮循环的Vector计算 // mm2计算; Softmax(); AttenMask(); ... |
尽量实现每个核的计算量均匀,负载均衡。优化前的分核及每个核的计算量如图11 causal场景优化前每个核计算量所示,按照第一根轴的大小8(行)来分核,平均分到9个核上,每个核计算ceil(8/9)=1行,第1个核只计算1个基本块,但是第8个核计算8个基本块。优化后如图12 causal场景优化后每个核计算量所示,红色块总共36个基本块,均分到每个核上,每个核的计算量为4块,性能提升一倍。
从采集的Profiling数据来看,Cube FixPipe占比高达81%,出现了很严重的bound(达到上限)。CAModel工具打印发现存在很多异常的128B搬运,排查代码,发现workspace地址未512B对齐。
代码实现中使用SetGlobalBuffer接口设置workspace的起始地址,如果起始地址不是按照512B对齐,搬运效率会很低,可以强制GM地址512Byte对齐来避免这个情况。下面代码中ADDR_ALIGN_SIZE即为512。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// init workspace address syncGlobal.SetGlobalBuffer((__gm__ int32_t*)workspace); uint64_t workspaceOffsets = SYNC_GLOBAL_WORKSPACE_SIZE; dqWorkSpaceGm.SetGlobalBuffer((__gm__ float*)workspace + workspaceOffsets / sizeof(T2)); workspaceOffsets = (workspaceOffsets + qPostBlockTotal * sizeof(float) + ADDR_ALIGN_SIZE) / ADDR_ALIGN_SIZE * ADDR_ALIGN_SIZE; dkWorkSpaceGm.SetGlobalBuffer((__gm__ float*)workspace + workspaceOffsets / sizeof(T2)); workspaceOffsets = (workspaceOffsets + kvPostBlockTotal * sizeof(float) + ADDR_ALIGN_SIZE) / ADDR_ALIGN_SIZE * ADDR_ALIGN_SIZE; dvWorkSpaceGm.SetGlobalBuffer((__gm__ float*)workspace + workspaceOffsets / sizeof(T2)); workspaceOffsets = (workspaceOffsets + kvPostBlockTotal * sizeof(float) + ADDR_ALIGN_SIZE) / ADDR_ALIGN_SIZE * ADDR_ALIGN_SIZE; // matmul1 and matmul2 workspace size matmulWorkspaceSize = cubeBaseMN * sizeof(float); mm1WorkspaceGm.SetGlobalBuffer((__gm__ T2*)(workspace + workspaceOffsets + cBlockIdx * matmulWorkspaceSize)); mm2WorkspaceGm.SetGlobalBuffer((__gm__ T2*)(workspace + workspaceOffsets + coreNum * matmulWorkspaceSize + cBlockIdx * matmulWorkspaceSize)); // drop workspace offset workspaceOffsets = (workspaceOffsets + coreNum * cubeBaseMN * sizeof(float) * INPUT_NUMS + ADDR_ALIGN_SIZE) / ADDR_ALIGN_SIZE * ADDR_ALIGN_SIZE; dropWorkSpaceGm.SetGlobalBuffer((__gm__ T1*)workspace + workspaceOffsets / sizeof(T1)); // mul workspace offset workspaceOffsets = (workspaceOffsets + coreNum * cubeBaseMN * sizeof(half) * 2 + ADDR_ALIGN_SIZE) / ADDR_ALIGN_SIZE * ADDR_ALIGN_SIZE; mulWorkSpaceGm.SetGlobalBuffer((__gm__ T1*)workspace + workspaceOffsets / sizeof(T1)); |
修改代码,workspace地址经过512B对齐后,FixPipe时间减半。
结合如下的Profiling数据和流水图,可以看出MTE2 bound,且部分MTE2搬运时间异常。
将输入数据排布格式从BSH更改为BNSD后,数据搬运连续,不需要跳地址读取数据,搬运效率提升一倍,部分异常搬运时长降低了一半。
融合算子场景,可参考此优化。