昇腾 NPU 算子性能优化实战:使用 unit_flag 指令级标志位实现 MMAD 计算与 FixPipe 搬出流水并行
【免费下载链接】cann-samplesCANN高性能实战演进样例与体系化调优知识库项目地址: https://gitcode.com/cann/cann-samples
本文基于 CANN 开源仓库 cann-samples 中的unit_flag样例,系统讲解昇腾 NPU 上指令级优化特性 unit_flag 的原理与工程实践:如何通过配置计算指令(MMAD)与搬出指令(FixPipe)的参数,将原本串行的"计算-搬出"流程改造为以 512B 数据块为粒度的流水线并行,从而隐藏数据搬移延迟、提升算子吞吐量。读者读完本文后,将掌握 unit_flag 标志位的取值含义、在 MatMul 等算子中的改造方法、编译运行与 Profiling 验证手段,以及如何结合源码判断该特性的适用场景。
1. 背景:搬运瓶颈与 N-Buffer 的局限
在算子性能调优过程中,带宽搬运瓶颈引起的计算断流是最需要优先优化的对象。业界通常从两个方面入手:
- 减少片外内存到片内内存的重复搬运,从数据量上做减法;
- 提高搬运效率,让搬运与计算在时间上重叠,从流水并行上做加法。
针对第二点,仓库中提供了 N-Buffer 特性:通过引入多份缓冲区(如双缓冲 Ping/Pong、四缓冲等),实现数据加载(MTE 流水)与矩阵计算(Cube 流水)的重叠执行,原理详见 N-Buffer特性介绍。
然而 N-Buffer 存在一个天然的局限:为了降低重复搬运的数据量,传入计算单元的计算基本块会被设计得尽量大,导致L0C 缓存块被占满,无法开启缓存机制。这样一来,计算流水(MMAD)与搬出流水(FixPipe)就无法并行——数据必须等一整块全部计算完毕,才能开始搬出,搬出期间计算单元只能空闲等待。
针对这一痛点,昇腾 NPU 硬件提供了指令级的优化手段:通过对计算和搬出指令的参数进行配置,开启unit_flag标志位,使计算(MMAD)与搬出(FixPipe)以更小的数据块粒度重叠执行,搬运效率因此得到进一步提升。
2. unit_flag 原理:计算与搬出的流水线并行
2.1 传统计算方式 vs unitflag
两种模式的核心差异在于数据搬出的粒度与并行度:
- 传统计算方式:计算单元需等待数据完全计算完成后才开始搬运,搬移与计算串行执行,导致计算单元空闲等待;
- unitflag:通过合理配置 MMAD 和 FixPipe 参数,系统实现计算与搬运的流水线并行。每当计算单元完成一个512b 数据块的计算,FixPipe 便立即将其搬出,无需等待全部数据搬运完成即可开始下一块数据的计算,从而实现计算与数据搬移的完全重叠执行。
下图直观对比了两种模式:传统方式下 PIPE_M 与 PIPE_FIX 各处理一个 2048B 的大块,串行推进;开启 unit_flag 后,同一个 2048B 的数据块被拆分为 4 个 512B 的小块,PIPE_M 每算完一块,PIPE_FIX 就立即搬出一块,两条流水紧密咬合:
2.2 硬件级流水线调度
UnitFlag 通过硬件级流水线调度,实现数据搬出与计算单元操作的并行执行。其关键机制是:
当设置 UnitFlag 为特定值时,计算指令(如 MMAD)无需等待前序搬运完全完成,即可基于缓冲区中已就绪的数据块立即启动计算,后续数据则通过流水线持续供给。
这要求软件层面做好两件事:
- 合理切分数据块(Tiling):只有将计算基本块拆成硬件可识别的小块,FixPipe 才能实现"边算边搬";
- 协调数据搬运与 MMAD 计算之间的流水同步:通过事件标志或互斥锁(Mutex)控制流水阶段间的依赖关系,最大化隐藏搬运延迟。
2.3 预期效果
- 吞吐量提升:单位时间内完成的有效计算量增加,整体算子性能得到优化;
- 资源利用率优化:计算单元与搬移单元并行工作,硬件资源得到更充分利用。
3. 实践:在 MatMul 算子中使能 unit_flag
3.1 改造前:基线实现
在未使能 unit_flag 的 MatMul 实现中,MMAD 计算与 FixPipe 搬出之间通过硬同步事件M_FIX严格串行——计算完成后才能搬出,搬出完成后下一轮计算才能开始:
// 执行 M-MAD 操作 MmadParams para; para.cmatrixInitVal = (iter1 == 0 && iter0 == 0); para.m = actualCurM; para.n = actualCurN; para.k = curKL0; AscendC::Te::Mad( MmadAtom<MmadTraits<MmadOperation, MmadTraitDefault>>{}, tensorL0C, tensorAL0, tensorBL0, para); // 流水同步 MMAD计算和Fixpipe搬出指令 AscendC::SetFlag<AscendC::HardEvent::M_FIX>(ZERO_FLAG); AscendC::WaitFlag<AscendC::HardEvent::M_FIX>(ZERO_FLAG); // 拷贝 L0c 到 GM(默认配置) auto copyL0C2GM = AscendC::Te::MakeCopy(AscendC::Te::CopyL0C2GM{}); AscendC::Te::Copy(copyL0C2GM, gmBlockC_, tensorL0C);这里SetFlag/WaitFlag<HardEvent::M_FIX>的作用是:等待 MMAD 计算完成、触发 FixPipe 搬出数据,并等待搬出完成。它保证了数据一致性,但也把两条流水死死绑在了一起。
3.2 改造后:unit_flag 使能版本
改造后主要改动点是配置 mmad 入参和fixpipe 重新配置,共三处关键改动:
// 新增:单元标志控制开关 constexpr uint32_t UNITFLAG_DISABLE = 0; // 不开启unit_flag constexpr uint32_t NO_FINAL_ACCUMULATION = 2; // 非尾轮标志 constexpr uint32_t FINAL_ACCUMULATION = 3; // 尾轮标志 // 执行 M-MAD 操作 MmadParams para; para.cmatrixInitVal = (iter1 == 0 && iter0 == 0); para.m = actualCurM; para.n = actualCurN; para.k = curKL0; // 关键改动1:根据迭代位置动态配置 unitFlag if constexpr (EN_UNIT_FLAG == true) { if (iter1 == (kL0IterNum - 1) && iter0 == (kL1TileNum - 1)) { para.unitFlag = FINAL_ACCUMULATION; } else { para.unitFlag = NO_FINAL_ACCUMULATION; } } AscendC::Te::Mad( MmadAtom<MmadTraits<MmadOperation, MmadTraitDefault>>{}, tensorL0C, tensorAL0, tensorBL0, para); // 关键改动2:删除 MMAD和Fixpipe之间的流水同步 // 关键改动3:使用自定义 FixpipeUnitFlagTrait(内置 unitFlag=3) auto copyL0C2GM = AscendC::Te::MakeCopy(AscendC::Te::CopyL0C2GM{}); AscendC::Te::Copy(copyL0C2GM, gmBlockC_, tensorL0C, AscendC::Te::FixpipeParams{UNITFLAG_EN_OUTER_LAST});三处关键改动逐一解读:
- 关键改动 1(MMAD 侧):根据迭代位置动态配置
para.unitFlag。K 维(L0 迭代)与 L1 迭代都处于最后一轮时(即整个数据块的尾块),配置为FINAL_ACCUMULATION(=3,表示这是最终累加轮,数据可以搬出);其余中间块配置为NO_FINAL_ACCUMULATION(=2,表示本轮计算后数据仍需在 L0C 中累加,不触发搬出)。 - 关键改动 2:删除 MMAD 与 FixPipe 之间的
M_FIX流水同步,让两条流水解绑; - 关键改动 3(FixPipe 侧):搬出指令显式携带 FixPipe 参数,内置
unitFlag=3(尾轮搬出标志),使 FixPipe 以小数据块为单位随算随搬。
3.3 源码级印证:main.asc 中的完整实现
仓库中该样例的核心内核实现位于 main.asc。从源码结构看,它在一个基于双缓冲(GM→L1→L0)与多核并行的 MatMul 内核上叠加了 unit_flag 配置:
标志位常量定义(main.asc):
// Unit flag values for cube unit pipelining constexpr uint32_t UNITFLAG_DISABLE = 0; // Disable unit flag constexpr uint32_t NO_FINAL_ACCUMULATION = 2; // Enable unit flag (inner loops) constexpr uint32_t FINAL_ACCUMULATION = 3; // Enable unit flag for outer last iterationTiling 参数(main.asc):baseM = 256、baseN = 256、baseK = 128 / sizeof(T)(即 bf16 下为 64)、kL1 = 512 / sizeof(T)。内核按mTileNum × nTileNum切分输出 Tile,K 维再分两层:外层iter0遍历 L1 Tile(kL1TileNum),内层iter1遍历 L0 Tile(kL0IterNum)。
unitFlag 动态赋值(main.asc):
AscendC::Te::MmadParams para; para.cmatrixInitVal = (iter1 == 0 && iter0 == 0); para.m = curM; para.n = curN; para.k = curKL0; if (iter1 == (kL0IterNum - 1) && iter0 == (kL1TileNum - 1)) { para.unitFlag = tool::FINAL_ACCUMULATION; } else { para.unitFlag = tool::NO_FINAL_ACCUMULATION; }可以确认:只有 K 维内层循环(iter1)到达最后一个 L0 Tile且K 维外层循环(iter0)到达最后一个 L1 Tile 时,才使用FINAL_ACCUMULATION(=3),其余迭代全部使用NO_FINAL_ACCUMULATION(=2)。这与 README 中"最后一块尾块的 UnitFlag 参数配置与中间块不同"的说明完全一致。注意样例源码中该赋值是无条件执行的(未用EN_UNIT_FLAG编译开关包裹),即默认开启该优化。
FixPipe 侧配置(main.asc):
auto copyL0C2GM = AscendC::Te::MakeCopy(AscendC::Te::CopyL0C2GM{}); AscendC::Te::Copy(copyL0C2GM.with(AscendC::Te::FixpipeParams{tool::FINAL_ACCUMULATION}), tensorCGmBlock, tensorL0C);在样例源码中,搬出指令通过.with(FixpipeParams{FINAL_ACCUMULATION})显式携带 unitFlag=3,与 README 代码片段中的UNITFLAG_EN_OUTER_LAST语义一致。
此外,从 main.asc 可以看到,该实现已用基于 Mutex 的流水同步(AscendC::Mutex::Lock/Unlock<PIPE_MTE2/PIPE_MTE1/PIPE_M>)取代了 README 基线代码中的SetFlag/WaitFlag事件同步,分别保护 L1 与 L0 双缓冲区的读写依赖;内核以KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIC_ONLY)声明为 AI Core 专属任务,并以 bfloat16 精度完成C = A × B的矩阵乘计算。
3.4 修改注意点
- mmad 计算参数配置:需注意循环边界处理,最后一块尾块的 UnitFlag 参数配置与中间块不同。若所有块都配置为"非尾轮"(=2),L0C 中的累加结果永远不会被标记为可搬出;若都配置为"尾轮"(=3),则中间块的中间结果会被提前搬出,破坏累加语义。必须精确地在"最后一轮 K 迭代 × 最后一轮 L1 迭代"处切换为
FINAL_ACCUMULATION。 - fixpipe 参数配置:需要深入理解 UnitFlag 值的含义与作用顺序,根据实际场景按需配置。FixPipe 侧的 unitFlag 必须与 MMAD 侧的尾轮判定保持一致,二者共同决定"何时允许将 L0C 结果搬回 GM"。
4. 性能结果对比
4.1 测试条件与 Profiling 观察
以基础 MatMul 算子为例,在相同输入规模M=1024, K=2048, N=4096下进行性能测试,通过 Profiling 工具采集硬件流水线执行状态。未开启 unit_flag 优化时,FixPipe 数据搬移与 MMAD 计算交替执行,流水线中存在明显的等待空洞:
开启 unit_flag 后,FixPipe 数据搬移流水线与 MMAD 计算流水线实现深度并行,有效隐藏了数据搬移延迟。值得注意的是,优化后的时序图中fixpipe 流水段长度显著增加。其根本原因在于 fixpipe 与 mmad 的流水线解绑:解绑后,fixpipe 的指令下发时机提前,但其对应的数据搬运操作并未同步启动,而是延迟至尾轮计算完成、数据累加结束后才进行实际的数据搬移。下一轮 mmad 计算必须等待 fixpipe 完成数据搬运后方可开始,导致该轮 mmad 的计算等待时间延长,整体流水线出现展宽:
4.2 Profile 数据明细
通过python3 profile_matmul.py 1024 2048 4096采集到的指标(kernel / mac / scalar / mte1 / mte2 / fixpipe 时间,单位 us,以及 icache miss 率)如下:
| candidate | kernel(us) | mac(us) | scalar(us) | mte1(us) | mte2(us) | fixpipe(us) | icache_miss(%) |
|---|---|---|---|---|---|---|---|
| unit_flag | 85.907 | 42.257 | 2.677 | 11.139 | 35.639 | 22.802 | 1.100 |
| matmul | 86.870 | 43.804 | 1.850 | 12.997 | 51.857 | 2.970 | 2.200 |
对比两份数据可以得出:
- mte2(数据搬入)时间从 51.857us 降至 35.639us,降幅约 31%——流水并行后搬入效率显著提升,这正是"搬运效率提升"的直接体现;
- mac(MMAD 计算)时间从 43.804us 降至 42.257us,计算流水本身也更饱满;
- fixpipe 时间从 2.970us 增至 22.802us,与前述"fixpipe 流水段展宽"的分析吻合——流水解绑后 fixpipe 承担了更多与计算重叠的搬出任务,其自身耗时变长,但不再阻塞计算;
- 整体 kernel 时间从 86.870us 降至 85.907us,icache miss 率从 2.2% 降至 1.1%,算子整体性能得到提升。
可见,FixPipe 数据搬移流水线与 MMAD 计算流水线并行执行,是整体性能提升的核心来源。
5. 结论与适用场景
unit_flag 通过硬件流水线实现搬运与计算并行,在数据切分合理的大规模矩阵运算中可显著提升性能,是优化 NPU 计算效率的关键特性。其典型适用场景包括:
- 双缓冲机制下,FixPipe 数据搬出与计算单元串行执行,影响后续核心的输出效率。为避免因缓存资源争用导致的计算流水线停顿,可通过开启 unit_flag 实现搬出与计算的流水并行;
- 硬件指令支持以小数据块为单位批量搬出数据,从而降低搬出延迟,提升流水线整体效率。
在决定是否启用该特性前,建议先结合 Profiling 数据分析当前瓶颈:若mte2/fixpipe等搬运时间占比过高、计算与搬运存在明显串行等待,可优先尝试 unit_flag(以及 N-Buffer特性介绍 中的双缓冲优化);同时需要结合 Tiling 策略,确保切分后的 Tile 大小在开启缓存机制后不超出硬件资源。
6. 编译、运行与性能测试
6.1 编译样例
从项目根目录启动构建,环境准备与整体构建方式参考项目 README.md。在仓库根目录下完成编译和安装后,进入当前样例目录:
cmake -S . -B build -DNPU_ARCH=dav-3510 cmake --build build --parallel cmake --install build --prefix ./build_out cd ./build_out/1_Features/instruction_optimization/unit_flag/如需单独编译当前样例,可使用以下指令:
cmake --build build --target unit_flag cp ./Samples/1_Features/instruction_optimization/unit_flag/scripts/* ./build/Samples/1_Features/instruction_optimization/unit_flag/ cd ./build/Samples/1_Features/instruction_optimization/unit_flag/从 CMakeLists.txt 可以看到,该样例通过cann_sample_check_arch(dav-3510)约束了目标架构,以add_executable(unit_flag main.asc)构建可执行文件,链接platform、tiling_api、cann_samples::tensor_api等库,并通过 POST_BUILD 与 install 规则将scripts/目录(数据生成、精度校验、性能测试脚本)随产物一同分发。
6.2 运行样例
使用可执行文件直接执行算子用例,需要指定矩阵乘维度,并随机生成输入数据:
./unit_flag 1024 2048 4096运行成功后,终端将打印类似如下信息(主程序会依次调用gen_data.py生成输入与 CPU 参考结果、执行内核、调用verify_result.py校验 NPU 与 CPU 结果一致性):
Data generated successfully! [verify] shape(1024, 4096), elements=4194304 - summary (large matrix, full tensors omitted) abs_err: max=2.560000e+02, mean=7.385254e-03, rmse=1.375000e+00 rel_err: max=6.410256e-03 count(|abs_err| > 0.001): 121 / 4194304 cpu golden (top-left 4x4): tensor([[42496., 42496., 41728., 41728.], [41728., 41984., 41216., 40960.], [41984., 41984., 41216., 41216.], [41216., 41472., 40960., 40960.]], dtype=torch.bfloat16) npu out (top-left 4x4): tensor([[42496., 42496., 41728., 41728.], [41728., 41984., 41216., 40960.], [41984., 41984., 41216., 41216.], [41216., 41472., 40960., 40960.]], dtype=torch.bfloat16) max abs diff: 256.0 point error count(>0.1): 0/4194304 ratio error count(>0.001): 121/4194304, error ratio: 0.000029 [PASS] NPU results are consistent with CPU.如果存在精度问题,则会打印错误数据,并显示如下结果:
[ERROR] NPU results differ from CPU.精度判定逻辑位于 verify_result.py:要求单点相对误差(abs_diff/abs(golden))不超过 1e-1,且绝对误差超过 1e-3 的点占比不超过 1e-3。数据生成脚本 gen_data.py 使用torch.bfloat16生成输入并以torch.matmul计算 CPU golden。
6.3 测试性能
运行性能测试脚本,指定矩阵乘法的维度后执行:
python3 profile_matmul.py 1024 2048 4096打印如下执行结果,证明样例性能测试成功:
[Profile Breakdowm] +-----------+------------+---------+------------+----------+----------+-------------+----------------+ | candidate | kernel(us) | mac(us) | scalar(us) | mte1(us) | mte2(us) | fixpipe(us) | icache_miss(%) | +===========+============+=========+============+==========+==========+=============+================+ | unit_flag | 85.907 | 42.257 | 2.677 | 11.139 | 35.639 | 22.802 | 1.100 | +-----------+------------+---------+------------+----------+----------+-------------+----------------+与相同输入规模下的基础 matmul 算子相比:
[Profile Breakdowm] +-----------+------------+---------+------------+----------+----------+-------------+----------------+ | candidate | kernel(us) | mac(us) | scalar(us) | mte1(us) | mte2(us) | fixpipe(us) | icache_miss(%) | +===========+============+=========+============+==========+==========+=============+================+ | matmul | 86.870 | 43.804 | 1.850 | 12.997 | 51.857 | 2.970 | 2.200 | +-----------+------------+---------+------------+----------+----------+-------------+----------------+从 profile_matmul.py 的实现看,该脚本会自动发现当前目录下的所有可执行文件作为候选(因此只要把基线matmul与优化版unit_flag两个可执行文件放到同一目录即可对比),逐个用msprof采集 Profiling 数据,解析op_summary_*.csv中的Task Duration(us)、aic_mac_time(us)、aic_mte1_time(us)、aic_mte2_time(us)、aic_fixpipe_time(us)、aic_icache_miss_rate等字段,按 kernel 时间升序给出推荐算法排序与完整指标表。
可以看到,FixPipe 数据搬移流水线与 MMAD 计算流水线并行执行,提升了整体性能。
7. 支持架构
当前样例支持NPU ARCH 3510(对应构建参数-DNPU_ARCH=dav-3510),其他架构暂未适配。若需在其他昇腾硬件上使用该特性,建议先确认目标芯片是否支持 FixPipe 的小数据块批量搬出指令级能力。更多指令优化类样例(如 N-Buffer、MTE2 预取、weightnz 等)可参考 instruction_optimization 目录总览。
【免费下载链接】cann-samplesCANN高性能实战演进样例与体系化调优知识库项目地址: https://gitcode.com/cann/cann-samples
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考