CANN/asc-devkit Matmul最佳实践

Matmul最佳实践样例

【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C++标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。 【免费下载链接】asc-devkit 项目地址: https://gitcode.com/cann/asc-devkit

概述

本样例基于Matmul 高阶API实现矩阵乘法运算,通过9个递进式的优化case展示从基础实现到高性能优化的完整调优路径,包括单核基础版本、Tiling优化、多核并行切分、MDL模式、L1Cache/L2Cache优化、常量Tiling、UnitFlag优化等多种优化手段。

本样例支持的产品及CANN软件版本

产品CANN软件版本
Ascend 950PR/Ascend 950DT>= CANN 9.1.0
Atlas A3 训练系列产品/Atlas A3 推理系列产品>= CANN 9.0.0
Atlas A2 训练系列产品/Atlas A2 推理系列产品>= CANN 9.0.0

目录结构介绍

├── matmul_high_performance
│   ├── scripts
│   │   ├── gen_data.py         // 输入数据和真值数据生成脚本文件
│   │   └── verify_result.py    // 真值对比文件
│   ├── CMakeLists.txt          // 编译工程文件
│   ├── data_utils.h            // 数据读入写出函数
│   ├── figures                 // 图示
│   ├── matmul.h                // 所有优化case的头文件定义
│   ├── matmul.asc              // Ascend C样例实现
│   └── README.md               // 样例说明文档

样例描述

  • 计算公式:C = A * B

    • A、B为源操作数,A为左矩阵,形状为[M, K];B为右矩阵,形状为[K, N]
    • C为目的操作数,存放矩阵乘结果的矩阵,形状为[M, N]
  • 样例规格:

样例类型(OpType)Matmul
样例输入nameshapedata typeformatisTrans
A[M, K]halfNDfalse
B[K, N]halfNDtrue
样例输出C[M, N]halfND-
核函数名matmul_custom

样例实现

类实现说明

本样例通过三个独立的类实现不同的优化策略,每个类对应特定的Case版本。

类名对应Case实现特点使用的核函数优化特性
MatmulKernelCase 0-5基础实现,运行时Tilingmatmul_custom (isMdl=false)
matmul_custom_mdl (isMdl=true)
- Case 0: 单核基础版本
- Case 1: 单核Tiling优化
- Case 2: 多核切分 2x12
- Case 3: 多核切分 4x6
- Case 4: MDL模式
- Case 5: MDL模式下L1Cache优化
MatmulKernelL2CacheCase 6L2Cache优化,运行时Tilingmatmul_custom_mdl_l2cache- MDL模式
- L1Cache优化
- L2Cache优化 (A矩阵M轴切分)
MatmulKernelMdlL2CacheConstantCase 7-8常量Tiling,编译期计算matmul_custom_mdl_l2cache_constant (useUnitFlag=false, Case7)
matmul_custom_mdl_l2cache_constant_unitflag (useUnitFlag=true, Case8)
- MDL模式
- L1Cache优化
- L2Cache优化
- 常量Tiling (编译期计算)
- Case 8: UnitFlag优化
1. Tiling机制特点
  • MatmulKernelMatmulKernelL2Cache使用TCubeTiling类型,需要从GM内存拷贝完整的Tiling数据结构,Tiling参数在运行时由Scalar单元计算
  • MatmulKernelMdlL2CacheConstant使用自定义的MatmulProblemShape结构体,仅包含shape信息(M, N, K, singleCoreM等),Tiling参数已在编译期通过CONSTANT_CFG计算完成,运行时无需Scalar计算
2. Process方法计算流程特点
  • 计算流程MatmulKernel单次迭代,MatmulKernelL2CacheMatmulKernelMdlL2CacheConstant循环2次(L2Cache优化,A矩阵M轴切分)

性能指标说明

指标说明
Task Duration(μs)整个任务执行的总时间,算子执行时间以该参数为准
Block Num使用的核数(Block数量)
aicore_time(μs)AI Core的平均执行时间
aic_mac_time(μs)Cube计算单元的执行时间
aic_mac_ratioCube计算单元的时间占比,反映计算单元利用率
aic_scalar_time(μs)Scalar标量计算单元的执行时间
aic_scalar_ratioScalar标量计算单元的时间占比
aic_mte1_time(μs)MTE1(L1到L0A/L0B搬运)的执行时间
aic_mte1_ratioMTE1的时间占比,反映L1到L0的数据搬运压力
aic_mte2_time(μs)MTE2(GM到L1搬运)的执行时间
aic_mte2_ratioMTE2的时间占比,反映GM到L1的数据加载压力
aic_fixpipe_time(μs)FixPipe(L0C到GM搬运)的执行时间
aic_fixpipe_ratioFixPipe的时间占比,反映结果写回的访存压力

说明:以下各Case的性能变化分析以A2芯片(Ascend 910B1)性能数据为例。Ascend 950PR的性能调优数据请参考下文

Case 0: 单核基础版本 (SINGLE_CORE_BASIC)

样例目标:实现基础的Matmul功能,确保计算正确性

核心实现

  • 使用单核计算,numBlocks=1
  • 基础Tiling配置,baseM=baseN=baseK=64,base块为参与一次矩阵乘指令的基本块,baseM、baseN、baseK分别代表Matmul计算时L0上M、N、K轴长度,以元素为单位,
  • 搬运策略:GM上输入的A、B矩阵按baseM×baseK、baseK×baseN分块依次搬运到L1中,再从L1搬运到L0A/L0B,Cube单元对每次搬运的base块执行一次baseM×baseN×baseK大小的Matmul计算

关键代码

tilingApi.SetShape(M, N, K);
tilingApi.SetFixSplit(64, 64, 64);
tilingData.set_baseK(64);

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 759363.98 | 1 | 759363.46 | 141699.459 | 0.187 | 508787.509 | 0.67 | 162104.184 | 0.213 | 582515.828 | 0.767 | 34303.063 | 0.045 |

分析

  • 单核运算时间759363.98μs
  • 计算单元利用率aic_mac_ratio仅18.7%,该场景仅作为Matmul运算性能对比样例,不建议用户使用

Case 1: 单核Tiling优化 (SINGLE_CORE_TILING)

优化目标:优化Tiling中base块参数,提升单核计算效率

核心实现

  • 使用单核计算,numBlocks=1
  • 采用优化的Tiling配置,baseM=128, baseN=256, baseK=64
  • 搬运策略:GM上输入的A、B矩阵按baseM×baseK、baseK×baseN分块依次搬运到L1中,再从L1搬运到L0A/L0B,Cube单元对每次搬运的base块执行一次baseM×baseN×baseK大小的Matmul计算

关键代码

tilingApi.SetShape(M, N, K);
tilingApi.SetFixSplit(128, 256, 64);

优化手段

  • base块选择原则:case0中Tiling中设置的base块为 [baseM, baseN, baseK] = [64, 64, 64],访存计算比(即计算每cycle需要的数据量)高。针对当前shape较大的场景,基本块的选择原则为访存计算比最小,即在Cube计算量相同的情况下,需要访存的数据量最小。

  • 访存计算比分析:在输入为fp16类型的情况下,Cube执行单元1 cycle能完成16×16×16次乘加运算。设置base块为[baseM, baseN, baseK] = [128, 256, 64]时,可以在满足搬出时GM地址512Byte对齐的同时,达到最小的访存计算比。Cube计算cycle数为(128 × 64 × 256) / (16 × 16 × 16) = 512cycle,访存计算比为(128 × 64 × 2 + 256 × 64 × 2) / 512cycle = 96(byte / cycle);设置base块为[baseM, baseN, baseK] = [64, 64, 64]时,Cube计算cycle数为(64 × 64 × 64) / (16 × 16 × 16) = 64cycle,访存计算比为(64 × 64 × 2 + 64 × 64 * 2) / 64cycle = 256(byte / cycle)。[128, 256, 64]的base块方案的访存计算比更低,同样的计算量,需要的数据量更小,所需带宽压力也就越低

  • 💡推荐base块设置:A2/A3 芯片L0A大小与L0B大小一致,为64KB,L0C大小为128KB,[baseM, baseN, baseK] = [128, 256, 64]时,能最大限度利用内存空间。输入数据类型b16时,base块推荐[baseM, baseN, baseK] = [128, 256, 64],输入数据类型b8时,base块推荐[baseM, baseN, baseK] = [128, 256, 128]。

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 249467.08 | 1 | 249466.54 | 81477.188 | 0.327 | 63608.818 | 0.255 | 52003.703 | 0.208 | 195613.432 | 0.784 | 11313.746 | 0.045 |

分析

  • 相比Case 0,Task Duration从759363.98μs降低到249467.08μs,降低了509896.90μs,性能提升3.04
  • MTE2数据搬运耗时从582515.828μs降低到195613.432μs,降低66.42%
  • 后续优化方向:引入多核并行计算,充分利用多核资源提升整体吞吐量

Case 2: 多核切分 2x12 (MULTI_CORE_SPLIT_2_12)

优化目标:引入多核并行计算,提升整体吞吐量

核心实现

  • 多核并行计算,将8192×8192的矩阵乘法切分到24个核上并行执行
  • 切分策略:M方向2块,N方向12块,singleM=4096,singleN=683,尾块tailN=679
  • 搬运策略:GM上输入的A、B矩阵按baseM×baseK、baseK×baseN分块依次搬运到L1中,再从L1搬运到L0A/L0B,Cube单元对每次搬运的base块执行一次baseM×baseN×baseK大小的Matmul计算

关键代码

tilingApi.SetDim(24);
tilingApi.SetSingleShape(4096, 683, 8192);
tilingApi.SetFixSplit(128, 256, 64);
SetL1(tilingData);

优化手段

  • 均衡负载分配,尽量让每个核的计算量相近

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 12541.22 | 24 | 12537.56 | 3462.802 | 0.276 | 2987.47 | 0.238 | 2260.551 | 0.18 | 10177.798 | 0.812 | 875.439 | 0.07 |

分析

  • 相比Case 1,Task Duration从249467.08μs降低到12541.22μs,性能提升19.89倍,多核并行计算显著提升了整体吞吐量
  • aic_mte2_time耗时10177.798μs,占比81.2%,成为性能提升瓶颈
  • 后续优化方向:当前多核切分策略没有满足地址512B对齐,且对M、N没有均匀分核,后续将优化切分策略,提升地址对齐,提高访存效率

Case 3: 多核切分 4x6 (MULTI_CORE_SPLIT_4_6)

优化目标:优化多核切分策略

核心实现

  • 多核并行计算,将8192×8192的矩阵乘法切分到24个核上并行执行
  • 切分策略:对M、N均匀切分,M方向4块,N方向6块
  • 搬运策略:GM上输入的A、B矩阵按baseM×baseK、baseK×baseN分块依次搬运到L1中,再从L1搬运到L0A/L0B,Cube单元对每次搬运的base块执行一次baseM×baseN×baseK大小的Matmul计算

关键代码

tilingApi.SetDim(24);
tilingApi.SetSingleShape(2048, 1536, 8192);
tilingApi.SetFixSplit(128, 256, 64);
SetL1(tilingData);

优化手段

  • 地址满足512B对齐,设置singleM=2048, singleN=1536,尾块tailN=512,提高有效带宽利用率
  • 避免同地址访问:同地址访问指多个核同时读取A矩阵的同一行数据时,需要访问相同的内存地址,硬件对同一地址的多次访问需要串行化。同地址访问核数越多,串行导致的性能劣化越严重,相比于case2中2×12的分核策略,case3的4×6分核同地址冲突延迟更小
  • 在多核切分时,应在满足地址512B对齐的情况下,对M、N进行均匀切分。

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 12283.84 | 24 | 10870.96 | 3394.884 | 0.312 | 2656.33 | 0.244 | 2166.824 | 0.199 | 8616.671 | 0.793 | 488.829 | 0.045 |

分析

  • 相比Case 2,Task Duration从12541.22μs降低到12283.84μs,降低了 2.05%。
  • aic_mte2_time从10177.798μs降低到8616.671μs,降低15.33%,降幅1561.127μs
  • 后续优化方向:当前aicore耗时仍主要为MTE2,后续将启用L1多块缓存功能,隐藏数据搬运延迟,提升MTE2流水线效率

Case 4: 多核使用MDL模板 (MULTI_CORE_MDL)

优化目标:使用MDL模板,启用L1多块缓存功能,开启"大包"搬运,减少MTE2循环搬运次数

核心实现

  • 使用24核并行计算,切分策略同上(4x6)
  • 使用MDL模板,启用"大包"搬运功能

关键代码

AscendC::Matmul<AscendC::MatmulType<AscendC::TPosition::GM, CubeFormat::ND, AType>,
                AscendC::MatmulType<AscendC::TPosition::GM, CubeFormat::ND, BType>,
                AscendC::MatmulType<AscendC::TPosition::GM, CubeFormat::ND, CType>,
                AscendC::MatmulType<AscendC::TPosition::GM, CubeFormat::ND, BiasType>, CFG_MDL>
    matmulObj;
// MDL模板自动优化,启用大包搬运功能

优化手段

  • 启用MDL模式,支持"大包"搬运
  • 大包搬运:MTE2从GM到L1的搬运不再是每次只搬运一个基本块,而是在L1中缓存多个基本块,可以显著减少GM到L1的搬运次数。本例场景中depthA1=4且开启L1搬运的double buffer,表示L1中缓存4份baseM × baseK的数据块,搬运时ping、pong缓冲区各搬入两块。
  • 💡L1多块缓存调优参数应满足:
    • dbL0A / dbL0B=2
    • depthA1 / (stepM * stepKa)=2,
    • depthB1 / (stepN * stepKb)=2
    • 参数含义:
      • dbL0A、dbL0B:分别表示 A 矩阵、 B 矩阵的 MTE1 是否开启 double buffer(值为 1 或 2,2 表示开启双缓冲)
      • depthA1、depthB1:分别表示 L1 中全载基本块的份数
      • stepM、stepKa:分别表示A矩阵在 L1 中缓存的数据块 M 方向上 baseM 的倍数、Ka方向即A矩阵K方向上 baseK 的倍数
      • stepN、stepKb:分别表示B矩阵在 L1 中缓存的数据块 N 方向上 baseN 的倍数、Kb方向即B矩阵K方向上 baseK 的倍数

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 5039.86 | 24 | 4487.89 | 3116.472 | 0.694 | 2073.97 | 0.462 | 2332.914 | 0.52 | 4051.458 | 0.903 | 44.749 | 0.01 |

分析

  • 相比Case 3,Task Duration从12283.84μs降低到5039.86μs,耗时降低58.98%
  • aic_mte2_time从8616.671μs降低到4051.458μs,MTE2搬运时间降低52.97%,降幅4565.213μs
  • aic_mac_ratio从31.2%提升到69.4%
  • 后续优化方向:MDL模板自动调优计算的Tiling没有完全利用L1空间,可手动调整Tiling参数,进一步提高MTE2的搬运力度

Case 5: 多核MDL + L1Cache优化 (MULTI_CORE_MDL_L1CACHE)

优化目标:提高MTE2的搬运力度,充分利用L1缓存空间

核心实现

  • 使用24核并行计算,切分策略同上(4x6)
  • 使用MDL模板,启用"大包"搬运功能
  • 手动优化Tiling参数为depthA1=16, stepKa=8,充分利用L1缓存空间

关键代码

AscendC::Matmul<AscendC::MatmulType<AscendC::TPosition::GM, CubeFormat::ND, AType>,
                AscendC::MatmulType<AscendC::TPosition::GM, CubeFormat::ND, BType>,
                AscendC::MatmulType<AscendC::TPosition::GM, CubeFormat::ND, CType>,
                AscendC::MatmulType<AscendC::TPosition::GM, CubeFormat::ND, BiasType>, CFG_MDL>
    matmulObj;
tilingData.set_depthA1(16);  // 增大depthA1,增加L1缓存中A矩阵的块数
tilingData.set_stepKa(8);

优化手段

  • L1 Cache Tiling参数优化:手动调整depthA1和stepKa参数,充分利用L1缓存
    • 当前配置:depthA1=16, depthB1=8, baseM=128, baseN=256, baseK=64

    • L1 缓存占用计算:

      A 矩阵: depthA1 × baseM × baseK × sizeof(half) = 16 × 128 × 64 × 2B = 262,144B = 256 KB

      B 矩阵: depthB1 × baseN × baseK × sizeof(half) = 8 × 256 × 64 × 2B = 262,144B = 256 KB

      总计256 KB + 256 KB = 512 KB

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 4156.76 | 24 | 3925.61 | 3352.087 | 0.854 | 1464.492 | 0.373 | 2750.454 | 0.701 | 3778.555 | 0.963 | 47.853 | 0.012 |

分析

  • 相比Case 4,Task Duration从5039.86μs降低到4156.76μs,耗时降低17.52%
  • aic_mte2_time从4051.458μs降低到3778.555μs,MTE2搬运时间降低了6.47%
  • aic_mac_ratio从69.4%提85.4%,cube利用率提升了23.05%
  • 后续优化方向:aic_mte2_ratio达到96.3%,达到MTE2 bound,成为性能提升瓶颈,后续将优化L2Cache以缓解MTE2 bound

Case 6: 多核MDL + L1Cache + L2Cache (MULTI_CORE_MDL_L1CACHE_L2CACHE)

优化目标:启用L2Cache优化,缓解MTE2 bound

核心实现

  • 使用24核并行计算,切分策略同上(4x6)
  • 使用MDL模板并优化L1 Cache参数同上
  • 启用L2Cache,对A矩阵M轴进行切分

关键代码

for (int i = 0; i < 2; i++) {
    matmulObj.SetTensorA(aGlobal[offsetA + i * (M >> 1) * K], IS_TRANS_A);
    matmulObj.SetTensorB(bGlobal[offsetB], IS_TRANS_B);
    if (shapes.isBias) {
        matmulObj.SetBias(biasGlobal);
    }
    matmulObj.IterateAll(cGlobal[offsetC + i * (M >> 1) * N]);
}

优化手段

  • 启用L2Cache优化
    • L2Cache特点:L2Cache是AI Core共享的外部缓存,L2Cache的纯读带宽约为GM的3到4倍,在搬入或搬出相同数据量的情况下,访问L2Cache内的数据比GM更快
    • 缓存命中优化:若数据无法命中L2Cache,导致需要访问GM进行读写,带宽利用效率较低,导致MTE2可能成为样例整个运行过程中的性能瓶颈
    • 分块计算适配L2Cache:当前L2Cache大小为192MB,矩阵计算所需数据总量为384MB(A矩阵128MB + B矩阵128MB + C矩阵128MB),由于L2Cache容量小于矩阵计算数据总量,因此可以将A矩阵在M轴切分成2份,其中区域1与B矩阵完成完整运算,区域2再与B矩阵完成完整运算,通过切分提高L2cache的命中率

分块计算适配L2Cache示意图:

计算过程:
Step 1: C1 = A1 × B  (A1从GM加载到L2Cache,B从GM加载到L2Cache并驻留)
Step 2: C2 = A2 × B  (A2从GM加载到L2Cache,B已在L2Cache中,无需重新加载)

L2Cache利用:
- B矩阵(128MB)在Step 1加载后驻留在L2Cache
- Step 2中B矩阵直接从L2Cache读取,避免GM访问
- 单次L2Cache访问带宽约为GM的3到4倍,显著提升访存效率

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 4088.36 | 24 | 3786.26 | 3254.31 | 0.86 | 1753.463 | 0.463 | 2680.849 | 0.708 | 3625.42 | 0.958 | 47.398 | 0.013 |

分析

  • 相比Case 5,Task Duration从4156.76μs降低到4088.36μs,降低了68.40μs
  • aic_mte2_time从3778.555μs降低到3625.42μs,MTE2搬运时间降低了4.05%
  • aic_mac_ratio从85.4%提升到86%
  • 后续优化方向:当前制约样例性能的仍为MTE2 bound,开发者可通过优化L2Cache的切分策略对MTE2进一步优化。本样例后续将展示对Scalar和Fixpipe流水的优化

Case 7: 多核MDL + L1Cache + L2Cache + Constants Tiling (MULTI_CORE_MDL_L1CACHE_L2CACHE_CONSTANTS)

优化目标:使用常量Tiling,减少运行时Scalar计算开销

核心实现

  • 使用24核并行计算,切分策略同上(4x6)
  • 使用MDL模板并优化L1 Cache参数同上
  • L2 Cache策略同上
  • 使能Tiling全量常量化

关键代码

constexpr MatmulShapeParams shapeParams = {SINGLE_M_L2CACHE, SINGLE_N, SINGLE_K, BASE_M, BASE_N, BASE_K};

constexpr static auto CONSTANT_CFG = GetCustomConstantCFG<A_TYPE, B_TYPE, C_TYPE, BIAS_TYPE, true, true>();
AscendC::Matmul<A_TYPE, B_TYPE, C_TYPE, BIAS_TYPE, CONSTANT_CFG> matmulObj;
// CONSTANT_CFG在编译期计算,减少运行时Scalar计算

优化手段

  • Tiling常量化
    • AI Core的硬件优势在于向量/矩阵并行计算,标量运算无法发挥硬件能力。Matmul API在初始化和迭代过程中有大量Scalar计算,Matmul初始化时的Scalar计算影响指令头开销,Matmul迭代间的Scalar计算可能阻塞MTE2流水
    • 静态Tiling减少运行时开销:使用MatmulApiStaticTiling参数替代TCubeTiling变量参数,将Scalar计算提前到编译期进行,运行时不需要通过Scalar单元动态计算,减少Scalar性能开销

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 4053.44 | 24 | 3665.12 | 3163.682 | 0.863 | 968.616 | 0.264 | 2609.617 | 0.712 | 3513.806 | 0.959 | 45.76 | 0.012 |

分析

  • 相比Case 6,Task Duration从4088.36μs降低到4053.44μs
  • aic_scalar_time从1753.463μs降低到968.616μs,降低44.76%
  • 常量Tiling有效减少了Scalar单元的性能开销。当样例因Scalar阻塞性能较差时,用户可通过该方式降低Scalar时间占比
  • 后续优化方向:启用UnitFlag优化,演示并行化计算与搬运流水

Case 8: 多核MDL + L1Cache + L2Cache + Constants Tiling + UnitFlag (MULTI_CORE_MDL_L1CACHE_L2CACHE_CONSTANTS_UNITFLAG)

优化目标:启用UnitFlag优化,并行化计算与搬运流水

核心实现

  • 使用24核并行计算,切分策略同上(4x6)
  • 使用MDL模板并优化L1 Cache参数同上
  • L2 Cache策略同上
  • 使能Tiling全量常量化同上
  • 启用UnitFlag,优化计算与搬运的并行度

关键代码

// 使用constexpr定义编译期常量,并启用UnitFlag
MatmulConfig mmCFG = GetMMConfig<MatmulConfigMode::CONFIG_MDL>(shapeParams);
mmCFG.enUnitFlag = true;

优化手段

  • 启用UnitFlag优化
    • 优化计算与搬运的并行度:未开启UnitFlag功能时,AIC核的MMAD计算指令和FIXPIPE数据搬运指令是指令级别的同步,FIXPIPE指令需要等MMAD指令执行完才进行结果搬出,MMAD和FIXPIPE之间流水串行;开启UnitFlag功能后,MMAD和FIXPIPE是512B大小的细粒度同步,在一条MMAD指令执行过程中,每当完成一个512B数据结果的计算,FIXPIPE立即搬出该512B的数据,从而实现Cube计算单元和FIXPIPE搬运单元流水并行

UnitFlag功能示意图:

性能数据: | Task Duration(μs) | Block Num | aicore_time(μs) | aic_mac_time(μs) | aic_mac_ratio | aic_scalar_time(μs) | aic_scalar_ratio | aic_mte1_time(μs) | aic_mte1_ratio | aic_mte2_time(μs) | aic_mte2_ratio | aic_fixpipe_time(μs) | aic_fixpipe_ratio | |------------------|-----------|----------------|-----------------|---------------|-------------------|-----------------|------------------|----------------|------------------|----------------|--------------------|-------------------| | 4012.44 | 24 | 3559.06 | 3076.396 | 0.864 | 1026.069 | 0.288 | 2533.512 | 0.712 | 3435.584 | 0.965 | 272.576 | 0.077 |

分析

  • 相比Case 7,Task Duration从4053.44μs降低到4012.44μs
  • aic_fixpipe_time从45.76μs提高到272.576μs,原因为开启Unitflag后,aic_fixpipe_time会包含FIXPIPE指令的等待时间,并不是真正流水的耗时,用户可关注端到端性能是否提升
  • 当前性能收益较小的原因为制约性能的仍为MTE2 bound,算子的MMAD、FIXPIPE流水被MTE2 bound掩盖。当样例因FIXPIPE阻塞性能较差时,用户可启用UnitFlag功能

性能对比总结

Atlas A2训练系列芯片性能数据

综合优化效果

  • 本样例cube利用率提升了67.7%(18.7% → 86.4%),已达到芯片峰值算力的86.4%
  • 通过Case 0到Case 8的递进优化,样例耗时降幅达99.47%(759363.98μs → 4012.44μs)
Case versionTask Duration(μs)端到端耗时相对Case 0Block Numaicore_time(μs)aic_mac_time(μs)aic_mac_ratioaic_scalar_time(μs)aic_scalar_ratioaic_mte1_time(μs)aic_mte1_ratioaic_mte2_time(μs)aic_mte2_ratioaic_fixpipe_time(μs)aic_fixpipe_ratio
Case 0759363.981x1759363.46141699.4590.187508787.5090.67162104.1840.213582515.8280.76734303.0630.045
Case 1249467.083.04x1249466.5481477.1880.32763608.8180.25552003.7030.208195613.4320.78411313.7460.045
Case 212541.2260.55x2412537.563462.8020.2762987.470.2382260.5510.1810177.7980.812875.4390.07
Case 312283.8461.82x2410870.963394.8840.3122656.330.2442166.8240.1998616.6710.793488.8290.045
Case 45039.86150.67x244487.893116.4720.6942073.970.4622332.9140.524051.4580.90344.7490.01
Case 54156.76182.68x243925.613352.0870.8541464.4920.3732750.4540.7013778.5550.96347.8530.012
Case 64088.36185.74x243786.263254.310.861753.4630.4632680.8490.7083625.420.95847.3980.013
Case 74053.44187.34x243665.123163.6820.863968.6160.2642609.6170.7123513.8060.95945.760.012
Case 84012.44189.25x243559.063076.3960.8641026.0690.2882533.5120.7123435.5840.965272.5760.077

理论性能对比

Matmul理论性能评估的关键参数为:Cube计算性能和MTE2带宽。

Cube计算性能分析

样例参数:M=N=K=8192,baseM=128,baseN=256,baseK=64。本样例性能数据在Atlas A2训练系列产品上测试,该计算芯片主频为1.85GHz,每cycle处理16×16×16次乘加运算。

Cube理论计算时间: $$cube_time = \frac{M \times N \times K}{16 \times 16 \times 16 \times core_num \times cube_freq} = \frac{8192 \times 8192 \times 8192}{16 \times 16 \times 16 \times 24 \times 1850} = 3022.92\mu s$$

Case 8 Cube计算耗时误差: $$误差 = \frac{aic_mac_time - cube_time}{cube_time} = \frac{{3076.396\mu s} - {3022.92\mu s}}{{3022.92\mu s}} = 1.77%$$

除去启动开销,已达成该芯片86%的峰值算力

MTE2带宽分析

读入数据总量: $$读入数据总量 = \left[\frac{N}{baseN} \times M \times K\right] + \left[\frac{M}{baseM} \times K \times N\right] \times dataType = (32 \times 8192 \times 8192) + (64 \times 8192 \times 8192) \times 2B = 12GB$$

理想情况下假设L2Cache容量足够大,首次从HBM中载入数据,后续数据均从L2Cache中读取,L2Cache峰值带宽约为5TB/s,HBM带宽约为1.8TB/s。 $$第一次从HBM读入的数据总量 = M \times K \times dataType + K \times N \times dataType = 256MB$$

MTE2理论耗时: $$MTE2理论耗时 =\frac{HBM读入数据总量}{1.8TB/s} +\frac{L2Cache读入数据总量}{5TB/s} = 2672.44\mu s$$

Case 8 MTE2耗时误差: $$MTE2耗时误差 = \frac{{3435.584\mu s} - {2672.44\mu s}}{{2672.44\mu s}} = 28.56%$$

当前MTE2耗时与理论值相差较大,因为实际芯片L2Cache大小为192MB,当前L2Cache切分策略较简单;另一方面当MTE2搬运场景为ND2NZ(GM数据Layout为ND,搬运到L1时需做ND→NZ格式转换)时,L2Cache带宽会降低。用户可进一步优化L2Cache切分策略以提高MTE2带宽。

Ascend 950PR芯片性能数据

Case versionTask Duration(μs)端到端耗时相对Case 0Block Numaicore_time(μs)aic_mac_time(μs)aic_mac_ratioaic_scalar_time(μs)aic_scalar_ratioaic_mte1_time(μs)aic_mte1_ratioaic_mte2_time(μs)aic_mte2_ratioaic_fixpipe_time(μs)aic_fixpipe_ratio
Case 01096626.4311x11096625.66198351.650.181583195.2220.532115705.1320.106960571.9930.87628615.9880.026
Case 1130560.4758.40x1130559.5688685.1420.67936462.0670.27922489.1560.172106793.3420.8184200.3850.032
Case 24294.619255.35x324293.92788.7810.6491149.240.268707.5920.1653540.6450.825141.8760.033
Case 34332.557253.11x324331.822774.2130.641143.2760.264703.8960.1623582.2460.827144.5250.033
Case 42668.224410.99x322667.492571.0740.9641377.7360.516799.3780.32531.9120.94933.8640.013
Case 52591.366423.18x322590.512547.0460.983612.9560.237834.3110.3221926.3580.74435.440.014
Case 62589.888423.43x322589.182547.5180.984765.1250.296826.4290.3191879.0290.72633.2610.013
Case 72589.09423.55x322588.382547.0490.984426.3980.165827.9390.321895.1650.73233.6480.013
Case 82558.155428.68x322557.492549.6570.997412.290.161835.5790.3271900.3220.743213.7890.084

理论性能对比

Cube计算性能分析

样例参数:M=N=K=8192,baseM=256,baseN=256,baseK=64。本样例性能数据在Ascend 950PR芯片上测试,该处理器主频为1.65GHz,每cycle处理16×16×16次乘加运算。

Cube理论计算时间: $$cube_time = \frac{M \times N \times K}{16 \times 16 \times 16 \times core_num \times cube_freq} = \frac{8192 \times 8192 \times 8192}{16 \times 16 \times 16 \times 32 \times 1650} = 2542\mu s$$

Case 8 Cube计算耗时误差: $$误差 = \frac{aic_mac_time - cube_time}{cube_time} = \frac{{2549.657\mu s} - {2542\mu s}}{{2542\mu s}} = 0.30%$$

已达成该芯片99.7%的峰值算力

MTE2带宽分析

读入数据总量: $$读入数据总量 = \left[\frac{N}{baseN} \times M \times K\right] + \left[\frac{M}{baseM} \times K \times N\right] \times dataType = (32 \times 8192 \times 8192) + (32 \times 8192 \times 8192) \times 2B = 8GB$$

理想情况下假设L2Cache容量足够大,首次从HBM中载入数据,后续数据均从L2Cache中读取,L2Cache峰值带宽约为5TB/s,HBM带宽约为1.6TB/s。 $$第一次从HBM读入的数据总量 = M \times K \times dataType + K \times N \times dataType = 256MB$$

MTE2理论耗时: $$MTE2理论耗时 =\frac{HBM读入数据总量}{1.6TB/s} +\frac{L2Cache读入数据总量}{5TB/s} = 1832.1\mu s$$

Case 8 MTE2耗时误差: $$MTE2耗时误差 = \frac{{1900.322\mu s} - {1832.1\mu s}}{{1832.1\mu s}} = 3.72%$$ 相比于Atlas A2训练系列芯片,Ascend 950PR芯片升级,数据搬运更为高效

调优建议

  1. 从小规模开始:先使用单核基础版本验证功能正确性
  2. 逐步优化:按照case顺序逐步引入优化手段,观察性能提升
  3. 多核切分策略:合理设置多核切分策略,避免同地址访问
  4. 利用MDL模式:MDL模式提供了高度优化的实现,优先使用
  5. L2Cache:L2Cache能进一步缓解MTE2 bound,对于需重复读取的数据推荐使用
  6. 常量Tiling:对于固定shape的场景,使用常量Tiling减少Scalar运行时开销
  7. UnitFlag优化:启用UnitFlag可以并行化计算与搬运流水

编译运行

在本样例根目录下执行如下步骤,编译并执行样例。

  • 配置环境变量 请根据当前环境上CANN开发套件包的安装方式,配置环境变量。

    source ${install_path}/cann/set_env.sh
    

    说明: ${install_path} 为CANN包安装目录,未指定安装目录时默认安装至 /usr/local/Ascend 下。

  • 样例执行

    在本样例目录下执行如下命令。

    SCENARIO_NUM=0                       # 选择执行场景
    mkdir -p build && cd build;      # 创建并进入build目录
    cmake -DSCENARIO_NUM=$SCENARIO_NUM -DCMAKE_ASC_ARCHITECTURES=dav-2201 ..;make -j;  # 编译工程(默认npu模式)
    python3 ../scripts/gen_data.py   # 生成测试输入数据
    ./demo                           # 执行
    python3 ../scripts/verify_result.py output/output.bin output/golden.bin   # 验证输出结果是否正确,确认算法逻辑正确
    

    使用 NPU仿真 模式时,添加 -DCMAKE_ASC_RUN_MODE=sim 参数即可。

    示例:

    cmake -DCMAKE_ASC_RUN_MODE=sim -DCMAKE_ASC_ARCHITECTURES=dav-2201 ..;make -j;   # NPU 仿真模式
    

    注意: 切换编译模式前需清理 cmake 缓存,可在 build 目录下执行 rm CMakeCache.txt 后重新 cmake。

  • 编译选项说明

    选项可选值说明
    SCENARIO_NUM0-8样例类型(0-8),默认为0
    CMAKE_ASC_RUN_MODEnpu(默认)、sim运行模式:NPU 运行、NPU仿真
    CMAKE_ASC_ARCHITECTURESdav-2201(默认)、dav-3510NPU 架构:dav-2201 对应 Atlas A2 训练系列产品/Atlas A2 推理系列产品和 Atlas A3 训练系列产品/Atlas A3 推理系列产品,dav-3510 对应 Ascend 950PR/Ascend 950DT

    执行结果如下,说明精度对比成功。

    test pass!
    

性能分析

使用 msprof 工具获取详细性能数据:

msprof ./demo   # 分析case 的性能

当前目录下会生成PROF_前缀的文件夹,mindstudio_profiler_output目录保存Host和各个Device的性能数据汇总,性能数据分析推荐查看该目录下文件

PROF_xxxx_XXXXXX
├── device_{id}
└── host
└── mindstudio_profiler_log
└── mindstudio_profiler_output    # 保存Host和各个Device的性能数据汇总
    ├── msprof_*.json
    ├── xx_*.csv
    └── README.txt

【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C++标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。 【免费下载链接】asc-devkit 项目地址: https://gitcode.com/cann/asc-devkit

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值