Matmul 基础API最佳实践样例概述本样例基于静态Tensor编程范式通过 L1/L0 双缓冲机制、大包搬运、细粒度流水同步、UnitFlag、L2Cache等多种优化手段实现高性能矩阵乘法运算。本样例基于基础API实现与高阶API版本所使用的优化方法相同样例目标为基于静态Tensor编程范式展示调优实现细节。本样例支持的产品及CANN软件版本产品CANN软件版本Ascend 950PR/Ascend 950DT CANN 9.1.0Atlas A3 训练系列产品/Atlas A3 推理系列产品 CANN 9.0.0Atlas A2 训练系列产品/Atlas A2 推理系列产品 CANN 9.0.0目录结构介绍├── matmul_basic_api_high_performance │ ├── scripts │ │ ├── gen_data.py // 输入数据和真值数据生成脚本文件 │ │ └── verify_result.py // 真值对比文件 │ ├── CMakeLists.txt // 编译工程文件 │ ├── data_utils.h // 数据读入写出函数 │ ├── mmad.asc // Ascend C样例实现 │ └── README.md // 样例说明文档样例描述Matmul计算公式 $$ C A * B $$A、B为源操作数A为左矩阵形状为[M, K]B为右矩阵形状为[K, N]C为目的操作数存放矩阵乘结果的矩阵形状为[M, N]样例规格样例类型(OpType)Matmul样例输入nameshapedata typeformatisTransA[M, K]halfNDfalseB[K, N]halfNDtrue样例输出C[M, N]halfND-核函数名mmad_custom样例实现性能指标说明指标说明Task Duration(μs)整个任务执行的总时间算子执行时间以该参数为准Block Num使用的核数Block数量aicore_time(μs)AI Core 的平均执行时间aic_mac_time(μs)Cube 计算单元的执行时间aic_mac_ratioCube 计算单元的时间占比反映计算单元利用率aic_mte1_time(μs)MTE1L1 到 L0A/L0B 搬运的执行时间aic_mte1_ratioMTE1 的时间占比反映 L1 到 L0 的数据搬运压力aic_mte2_time(μs)MTE2GM 到 L1 搬运的执行时间aic_mte2_ratioMTE2 的时间占比反映 GM 到 L1 的数据加载压力aic_fixpipe_time(μs)FixPipeL0C 到 GM 搬运的执行时间aic_fixpipe_ratioFixPipe 的时间占比反映结果写回的访存压力数据流路径GM ──(MTE2, DataCopy)── L1 ──(MTE1, LoadData)── L0A/L0B ──(Cube, Mmad)── L0C ──(Fixpipe)── GM DataCopyInA/B DataLoadA/B Compute CopyOut核心特性1. L1/L0 双缓冲 Ping-Pong 布局L1 和 L0 均采用 Ping-Pong 双缓冲使得 DataCopyInGM→L1、DataLoadL1→L0与 Compute 三个阶段形成三级流水每个阶段处理不同缓冲区的数据互不阻塞。时间 ────────────────────────────────────────────────────────────── MTE2: |─ A1 Ping ──|─ A1 Pong ──|─ A1 Ping ──| ... MTE1: |─ A2 Ping ──|─ A2 Pong ──|─ A2 Ping ──| ... Cube: |─ Mmad ─────|─ Mmad ─────| ... Fixpipe: |─ CopyOut ──|(unitflag)L1 双缓冲布局A1 占 L1 前半0~256KBB1 占 L1 后半256~512KB各自再分为 Ping/Pong 两块L1 (512KB): ├── A1 Ping: [0, 128KB) ├── A1 Pong: [128KB, 256KB) ├── B1 Ping: [256KB, 384KB) └── B1 Pong: [384KB, 512KB)L0 双缓冲布局A2/B2 各自独立64KB空间各自再分为 Ping/PongL0A/L0B (64KB): ├── A2 Ping: [0, 16KB) ├── A2 Pong: [32KB, 48KB) ├── B2 Ping: [0, 32KB) └── B2 Pong: [32KB, 64KB)// A1: L1 Ping/Pong AscendC::LocalTensorhalf a1LocalPing(AscendC::TPosition::A1, 0, a1PingpongSize); AscendC::LocalTensorhalf a1LocalPong(AscendC::TPosition::A1, a1PingpongSize * sizeof(half), a1PingpongSize); // A2: L0 Ping/Pong AscendC::LocalTensorhalf a2LocalPing(AscendC::TPosition::A2, 0, a2PingpongSize); AscendC::LocalTensorhalf a2LocalPong(AscendC::TPosition::A2, L0_PINGPONG_BYTES, a2PingpongSize);2. 大包搬运通过stepKa/stepKb参数将多个基本块打包为一次 DataCopyIn 操作称为大包减少 MTE2 搬运次数。例如stepKa8表示一次将 8 个 baseM * baseK 块从 GM 搬入 L1。// DataCopyInA: 一次搬入 stepKa 个 baseK 块 AscendC::Nd2NzParams nd2nzParams; nd2nzParams.nValue curM; nd2nzParams.dValue baseK * stepKa; // 大包包含 stepKa 个 baseM * baseK3. 细粒度流水同步使用四类硬件事件标志实现精确的流水线同步分为正向同步数据就绪通知和反向同步缓冲区释放通知事件类型方向用途flag 编号MTE2_MTE1正向L1 数据就绪通知DataCopyIn 通知 DataLoad 可读取0/1: A1 Ping/Pong; 2/3: B1 Ping/PongMTE1_MTE2反向L1 缓冲区释放通知DataLoad 通知 DataCopyIn 可写入同上MTE1_M正向L0 数据就绪通知DataLoad 通知 Compute 可计算mte1DBFlag (0/1 交替)M_MTE1反向L0 缓冲区释放通知Compute 通知 DataLoad 可写入mte1DBFlag (0/1 交替)反向同步需预置由于反向同步是消费方 SetFlag → 生产方 WaitFlag首次使用前必须预置 SetFlag否则首次 WaitFlag 会死锁// 初始化预置反向同步 flag防止首次 WaitFlag 死锁 AscendC::SetFlagAscendC::HardEvent::MTE1_MTE2(EVENT_ID0); // A1 Ping 可写 AscendC::SetFlagAscendC::HardEvent::MTE1_MTE2(EVENT_ID1); // A1 Pong 可写 AscendC::SetFlagAscendC::HardEvent::MTE1_MTE2(EVENT_ID2); // B1 Ping 可写 AscendC::SetFlagAscendC::HardEvent::MTE1_MTE2(EVENT_ID3); // B1 Pong 可写 AscendC::SetFlagAscendC::HardEvent::M_MTE1(EVENT_ID0); // L0 Ping 可写 AscendC::SetFlagAscendC::HardEvent::M_MTE1(EVENT_ID1); // L0 Pong 可写大包粒度的正向同步DataLoad 从 L1 大包中按K方向逐个base块读取数据只需在读取大包首个base块时等待数据就绪后续base块与首个在同一个大包内无需重复等待// 正向同步仅大包首元素需等待数据就绪 if (kOffsetInChunkA 0) { AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE1(a1ReadIdx); } // 反向同步仅大包末元素消费完后通知 DataCopyIn 可覆盖 if ((kOffsetInChunkA 1) stepKa) { AscendC::SetFlagAscendC::HardEvent::MTE1_MTE2(a1ReadIdx); }4. LoadData3D 替代 LoadData2D——减少指令队列占用在 Atlas A2/A3 架构上本样例使用LoadData3DParamsV2即 LoadData3D替代LoadData2DParams即 LoadData2D完成 L1→L0 的数据搬运。这是一个关键的指令队列优化。问题背景MTE1 指令队列深度为 32。使用 LoadData2D 时由于单条 LoadData2D 指令搬运粒度有限搬运一个 baseM×baseK 的切片需要用 for 循环发射多条 LoadData2D 指令。例如 baseM128、baseK64 时最少需要发射baseK/16 4条 LoadData2D 指令。代码实现使用 LoadData2D 时需要按CUBE_BLOCK粒度循环发射多条搬运指令改为 LoadData3D 后可用一条指令描述完整的 base 切片。优化前baseK64时循环次数为 4AscendC::LoadData2DParams loadDataParams; for (int i 0; i AscendC::DivCeil(baseK, CUBE_BLOCK); i) { AscendC::LoadData(b2Local[i * dstOffset], b1Local[srcAddr i * srcOffset], loadDataParams); }优化后使用LoadData3DParamsV2一次性描述baseM * baseK切片AscendC::LoadData3DParamsV2half loadDataParams; loadDataParams.l1H 1; loadDataParams.l1W baseM; loadDataParams.channelSize baseK; loadDataParams.kExtension baseK; loadDataParams.mExtension curMAlign; loadDataParams.mStartPt 0; loadDataParams.kStartPt 0; AscendC::LoadData(a2Local, a1Local[srcAddr], loadDataParams);这样每个 base 切片的 MTE1 指令数从多条LoadData2D降为 1 条LoadData3D可以降低 MTE1 队列占用。B 矩阵非转置输入时同样通过LoadData3DParamsV2配置enTranspose true完成转置搬运。注意Atlas A5 芯片提供了LoadData2DParamsV2接口单条指令即可完成搬运无需 LoadData3D。因此本样例通过__NPU_ARCH__条件编译区分两种架构的 LoadData 实现。5. 多核并行切分按 M/N 方向均匀切分矩阵到多核并行计算4×6 切分策略M 方向 4 块、N 方向 6 块共 24 核满足地址 512B 对齐并减少同地址访问冲突constexpr uint32_t mIter AscendC::DivCeil(M, singleCoreM); uint32_t mIterIdx AscendC::GetBlockIdx() % mIter; uint32_t nIterIdx AscendC::GetBlockIdx() / mIter;6. 常量 Tiling所有 Tiling 参数baseM/baseK/baseN、singleCoreM/K/N、stepKa/stepKb通过模板参数在编译期确定运行时无需 Scalar 动态计算减少 Scalar 开销template uint32_t M, uint32_t K, uint32_t N, uint32_t baseM, uint32_t baseK, uint32_t baseN, uint32_t singleCoreM, uint32_t singleCoreK, uint32_t singleCoreN, uint32_t stepKa, uint32_t stepKb class KernelMmad { ... };7. UnitFlag 优化开启 UnitFlag 后MMAD 和 FIXPIPE 实现细粒度512B流水并行而非指令级同步。每当 Cube 完成一个 512B 数据结果的计算FIXPIPE 立即搬出该数据Cube 计算与结果写回流水重叠mmadParams.unitFlag (kBlockIdx ! kLoopCount - 1) ? 2 : 3; // 开启 UnitFlagunitFlag 2中间 K 块MMAD 计算结果不立即写回而是等待下一个 512B 完成后流水搬出unitFlag 3最后一个 K 块通知 FIXPIPE 将所有剩余结果写回 GM8. DataCopyIn 优先搬运 B 矩阵当stepKa stepKb时B 矩阵每stepKb个 baseK 就需要切换到下一个 L1 缓冲区Pong而 A 矩阵需要stepKa个 baseK 才切换。因此 B 的数据需求更紧迫。如果先搬运 AMTE2 流水被 A 占用B 的搬运要等 A 搬完才能开始导致需要 B 数据时 B 尚未就绪。本样例在 Compute 之后触发 DataCopyIn 时先 B 后 A优先搬运更紧迫的 B 数据k0: Compute → DataCopyIn(B1 Pong) → DataCopyIn(A1 Pong) ↑ MTE2 先搬 B ↑ 再搬 A kstepKb: 需要 B1 Pong → 已就绪 ✓B 已有 stepKb 轮时间搬运 kstepKa: 需要 A1 Pong → 已就绪 ✓A 有 stepKa 轮时间搬运更充裕9. L2Cache 优化L2Cache 是 AI Core 共享的外部缓存纯读带宽约为 GM 的 3 到 4 倍。若数据无法命中 L2Cache需要访问 GM带宽利用效率较低导致 MTE2 成为性能瓶颈。L2Cache 切分的具体实现与高阶 API Matmul 样例中的 Case 6 一致核心思路均为将 A 矩阵 M 轴切分使 B 矩阵跨轮驻留 L2Cache。本样例通过ProcessL2Cache()方法实现双重外层循环按outerMIdx分轮调度每轮内 24 核并行计算各自的子块// ProcessL2Cache: 按 M 方向分轮每轮 24 核覆盖 mIterPerRound 份 M 子块 constexpr uint32_t mIterPerRound AscendC::DivCeil(M, singleCoreM * 2); constexpr uint32_t outerMLoopCount AscendC::DivCeil(mIterTotal, mIterPerRound); for (uint32_t outerMIdx 0; outerMIdx outerMLoopCount; outerMIdx) { uint32_t mIterIdx AscendC::GetBlockIdx() % mIterPerRound outerMIdx * mIterPerRound; uint32_t nIterIdx AscendC::GetBlockIdx() / mIterPerRound; if (mIterIdx mIterTotal || nIterIdx nIterTotal) continue; InitComputeParamsL2Cache(mIterIdx, nIterIdx); ProcessLoop(...); }10. K 方向主循环完整流程以 stepKa8、stepKb4 为例展示一个完整 (mBlockIdx, nBlockIdx) 子块内 K 方向循环的详细执行流程预处理 SetFlag(MTE1_MTE2, 0/1/2/3) // 预置反向同步L1 Ping/Pong 均可写 SetFlag(M_MTE1, 0/1) // 预置反向同步L0 Ping/Pong 均可写 DataCopyIn(A1 Ping, k0) // 搬入首个 A 大包到 Ping DataCopyIn(B1 Ping, k0) // 搬入首个 B 大包到 Ping SetFlag(MTE2_MTE1, 0/2) // 通知 A1/B1 Ping 数据就绪 K循环 kBlockIdx 0, 1, ..., kLoopCount-1: ┌─ a1ReadIdx (kBlockIdx / stepKa) % 2 // 当前读 L1 A 的 Ping/Pong │ b1ReadIdx (kBlockIdx / stepKb) % 2 // 当前读 L1 B 的 Ping/Pong │ kOffsetInChunkA kBlockIdx % stepKa // 当前 baseK 在 A 大包内的偏移 │ kOffsetInChunkB kBlockIdx % stepKb // 当前 baseK 在 B 大包内的偏移 │ │ WaitFlag(M_MTE1, mte1DBFlag) // 等待上一轮 Compute 释放 L0 │ if (kOffsetInChunkA 0) │ WaitFlag(MTE2_MTE1, a1ReadIdx) // 等待 L1 A 大包数据就绪仅首元素 │ if (kOffsetInChunkB 0) │ WaitFlag(MTE2_MTE1, b1ReadIdx 2) // 等待 L1 B 大包数据就绪仅首元素 │ │ DataLoadA(A1 → A2) // L1 → L0 │ DataLoadB(B1 → B2) // L1 → L0 │ │ if (kOffsetInChunkA 1 stepKa) │ SetFlag(MTE1_MTE2, a1ReadIdx) // A 大包末元素通知 L1 A 可写 │ if (kOffsetInChunkB 1 stepKb) │ SetFlag(MTE1_MTE2, b1ReadIdx 2) // B 大包末元素通知 L1 B 可写 │ │ Compute(Mmad) // Cube 计算M 指令 │ SetFlag(M_MTE1, mte1DBFlag) // 通知 L0 可覆盖 │ mte1DBFlag ^ 1 // 切换 L0 Ping/Pong │ │ // DataCopyIn 放在 Compute 之后先 B 后 A │ if (B大包末元素 还有B数据): │ WaitFlag(MTE1_MTE2, b1WriteIdx 2) // 等待 L1 B 缓冲区可写 │ DataCopyInB(下一个 B 大包) // GM → L1MTE2 指令 │ SetFlag(MTE2_MTE1, b1WriteIdx 2) // 通知 L1 B 数据就绪 │ if (A大包末元素 还有A数据): │ WaitFlag(MTE1_MTE2, a1WriteIdx) // 等待 L1 A 缓冲区可写 │ DataCopyInA(下一个 A 大包) // GM → L1MTE2 指令 │ SetFlag(MTE2_MTE1, a1WriteIdx) // 通知 L1 A 数据就绪 └─时序示意stepKa8, stepKb4预取: DataCopyIn(A1 Ping) DataCopyIn(B1 Ping) ← K循环前 k0: WaitFlag(A1Ping, B1Ping) → DataLoad → Compute → DataCopyIn(B1Pong) → DataCopyIn(A1Pong) k1: DataLoad → Compute k2: DataLoad → Compute k3: DataLoad(释放B1Ping) → Compute → DataCopyIn(搬运B1Ping) k4: WaitFlag(B1Pong) → DataLoad(B1Pong) → Compute k5: DataLoad → Compute k6: DataLoad → Compute k7: DataLoad(释放A1Ping B1Pong) → Compute → DataCopyIn(B1Pong) → DataCopyIn(A1Ping) k8: WaitFlag(A1Pong已就绪✓) → WaitFlag(B1Ping已就绪✓) → DataLoad → Compute ...性能数据分析Atlas A2训练系列芯片性能数据Scenario 1不使能 L2Cache 切分singleCoreM2048, singleCoreN153624 核一轮全覆盖Scenario 2使能 L2Cache 切分singleCoreM1024, singleCoreN153624 核分 2 轮计算ScenarioTask Duration(μs)Block 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_ratioScenario 14121.16243670.73081.6640.84337.3430.0922538.3480.6923552.2480.968160.4050.044Scenario 24081.64243636.853082.1580.847345.1390.0952553.0640.7023487.0680.959161.8120.044除去启动开销已达成该芯片 84.7% 的峰值算力。使能 L2Cache 切分后aic_mte2_time 从 3552.248μs 降低到 3487.068μs降低了 1.84%。当前切分策略较简单用户可进一步优化 L2Cache 切分策略以提高 MTE2 带宽。Ascend 950PR芯片性能数据Scenario 1不使能 L2Cache 切分singleCoreM2048, singleCoreN102432 核一轮全覆盖Scenario 2使能 L2Cache 切分singleCoreM1024, singleCoreN102432 核分 2 轮计算ScenarioTask Duration(μs)Block 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_ratioScenario 12572.047322571.442564.8130.997144.6040.056828.0010.3221874.2670.729221.9970.086Scenario 22574.492322573.392564.1470.996104.8450.041819.2070.3181892.7420.736223.1290.087已达成该芯片 99.7% 的峰值算力。使能 L2Cache 切分后在 Ascend 950PR 芯片上没有明显效果原因如下L2Cache 优化的目标是缓解 MTE2 bound但当前瓶颈为 Cube 计算而非数据搬运因此减少 MTE2 耗时无法提升整体性能此外 L2Cache 切分将计算分为 2 轮调度引入了额外的 Scalar 开销和调度开销导致 Scenario 2 的 Task Duration 略高于 Scenario 1。同时Scenario 2 的 aic_mte2_time1892.742μs反而略高于 Scenario 11874.267μs这是因为当样例处于 Cube bound 时MTE2 流水线被 Cube 计算阻塞profiler 采集的 aic_mte2_time 包含了流水线等待时间而非纯粹的数据搬运时间L2Cache 优化虽然减少了实际数据访问延迟但被 Cube 计算瓶颈掩盖无法在 MTE2 指标上体现。理论性能分析Cube 计算性能分析Atlas A2 训练系列芯片样例参数 MNK8192baseM128baseN256baseK64该计算芯片主频为 1.85GHz每 cycle 处理 16×16×16 次乘加运算。$$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$$Cube 计算耗时误差$$误差 \frac{aic_mac_time - cube_time}{cube_time} \frac{3082.158 - 3022.92}{3022.92} 1.96%$$Ascend 950PR 芯片样例参数 MNK8192baseM256baseN256baseK64该处理器主频为 1.65GHz每 cycle 处理 16×16×16 次乘加运算。$$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.00\mu s$$Cube 计算耗时误差$$误差 \frac{aic_mac_time - cube_time}{cube_time} \frac{2564.813 - 2542.00}{2542.00} 0.90%$$MTE2 带宽分析读入数据总量Atlas A2 训练系列芯片baseM128baseN256$$读入数据总量 \left(\frac{N}{baseN} \times M \times K \frac{M}{baseM} \times K \times N\right) \times sizeof(half) (32 \times 8192 \times 8192 64 \times 8192 \times 8192) \times 2B 12GB$$Ascend 950PR 芯片baseM256baseN256$$读入数据总量 \left(\frac{N}{baseN} \times M \times K \frac{M}{baseM} \times K \times N\right) \times sizeof(half) (32 \times 8192 \times 8192 32 \times 8192 \times 8192) \times 2B 8GB$$MTE2 理论耗时Atlas A2 训练系列芯片L2Cache 峰值带宽约 5TB/sHBM 带宽约 1.8TB/s。首次从 HBM 读入数据后续从 L2Cache 读取。$$第一次从HBM读入的数据总量 M \times K \times sizeof(half) K \times N \times sizeof(half) 256MB$$$$MTE2理论耗时 \frac{HBM读入数据总量}{1.8TB/s} \frac{L2Cache读入数据总量}{5TB/s}$$MTE2 耗时误差$$MTE2耗时误差 \frac{3487.068 - 2672.44}{2672.44} 30.48%$$当前MTE2耗时与理论值相差较大因为实际芯片L2Cache大小为192MB当前L2Cache切分策略较简单另一方面当MTE2搬运场景为ND2NZGM数据Layout为ND搬运到L1时需做ND→NZ格式转换时L2Cache带宽会降低。用户可进一步优化L2Cache切分策略以提高MTE2带宽。Ascend 950PR 芯片L2Cache 峰值带宽约 5TB/sHBM 带宽约 1.6TB/s。$$MTE2理论耗时 \frac{HBM读入数据总量}{1.6TB/s} \frac{L2Cache读入数据总量}{5TB/s}$$MTE2 耗时误差$$MTE2耗时误差 \frac{1874.267 - 1832.10}{1832.10} 2.30%$$相比于 Atlas A2 训练系列芯片Ascend 950PR 芯片数据搬运更为高效MTE2 带宽利用率更高。编译运行在本样例根目录下执行如下步骤编译并执行样例。切换Case在 cmake 编译时通过-DSCENARIO_NUMN指定要编译的场景各场景说明1: 不使能L2Cache切分2: 使能L2Cache切分配置环境变量请根据当前环境上CANN开发套件包的安装方式配置环境变量。source ${install_path}/cann/set_env.sh说明${install_path}为CANN包安装目录未指定安装目录时默认安装至/usr/local/Ascend下。样例执行在本样例目录下执行如下命令。SCENARIO_NUM1 mkdir -p build cd build; # 创建并进入build目录 cmake .. -DCMAKE_ASC_ARCHITECTURESdav-2201 -DSCENARIO_NUM$SCENARIO_NUM;make -j; # 编译工程默认npu模式 python3 ../scripts/gen_data.py # 生成测试输入数据 ./demo # 执行编译生成的可执行程序执行样例 python3 ../scripts/verify_result.py output/output.bin output/golden.bin # 验证输出结果是否正确确认算法逻辑正确使用NPU仿真模式时添加-DCMAKE_ASC_RUN_MODEsim参数即可。示例如cmake .. -DCMAKE_ASC_RUN_MODEsim -DCMAKE_ASC_ARCHITECTURESdav-2201 -DSCENARIO_NUM$SCENARIO_NUM;make -j; # NPU仿真模式注意切换编译模式或Scenario前需清理 cmake 缓存可在 build 目录下执行rm CMakeCache.txt后重新 cmake。编译选项说明选项可选值说明CMAKE_ASC_RUN_MODEnpu默认、sim运行模式NPU运行、NPU仿真CMAKE_ASC_ARCHITECTURESdav-2201、dav-3510NPU硬件架构dav-2201对应A2/A3dav-3510对应Ascend 950PRSCENARIO_NUM1、2场景编号1不使能L2Cache切分2使能L2Cache切分执行结果执行结果如下说明精度对比成功。test pass!性能分析使用msprof工具获取详细性能数据msprof ./demo # 分析样例性能当前目录下会生成 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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考