UnitFlag 与 L0C Double Buffer 的适用边界分析 —— 昇腾 950PR(DAV_3510)


0. 问题重述与先验共识

昇腾 950PR 上,Matmul 的标准数据流是:

GM ──MTE2──► L1 ──MTE1──► L0A/L0B ──MMAD──► L0C ──Fixpipe──► GM(C)

其中 L0C 的输出(Fixpipe 搬出)与 Cube 的计算(MMAD)之间存在「谁先谁后」的问题。用户观察到的两条掩盖路径:

  1. L0C Double Buffer(dbL0C=2):L0C 分两份,当前一份在 MMAD 计算,另一份在 Fixpipe 搬出,用「计算-搬出」重叠掩盖 Fixpipe。
  2. UnitFlag(细粒度同步):MMAD 每算完 512B 就立即触发 Fixpipe 搬出该 512B,用「指令级流水」掩盖 Fixpipe。

问题:既然 UnitFlag 能这么细粒度地掩盖,L0C 还需要开 Double Buffer 吗? 两者的适用边界在哪?


1. 两条掩盖路径的原理性拆解

1.1 L0C Double Buffer 掩盖的是什么

L0C DB 的目标是:当前 base 块的 Fixpipe 搬出 与 下一个 base 块的 MMAD 计算 并行。它的掩盖对象是「base 块与 base 块之间的 Fixpipe 串行等待」。

L0C 块0:  MMAD ──► Fixpipe 搬出
L0C 块1:           MMAD ──► Fixpipe 搬出
L0C 块2:                     MMAD ──► Fixpipe 搬出
时间轴:  ────────┬───────┬───────┬──►
                 块0搬出与块1计算重叠   块1搬出与块2计算重叠

1.2 UnitFlag 掩盖的是什么

UnitFlag 的目标是:单个 base 块内部,MMAD 计算 与 Fixpipe 搬出 的细粒度并行。它的掩盖对象是「单个 base 块内部的 MMAD-Fixpipe 串行等待」。

未开 UnitFlag:  MMAD(整个base块) ──────► Fixpipe(整个base块)
开 UnitFlag:    MMAD[512B] ─► Fixpipe[512B]
                  MMAD[512B] ─► Fixpipe[512B]   ...
                (512B 粒度交错,计算与搬出重叠)

1.3 两者的关系:掩盖对象不同,可叠加

维度L0C Double BufferUnitFlag
掩盖对象base 块之间的 Fixpipe 串行base 块内部的 MMAD-Fixpipe 串行
粒度base 块级(一份 L0C 搬出时另一份在算)512B 指令级
触发方式Host tiling 决策(dbL0C=1/2)Device kernel 里 enUnitFlag=true
资源成本L0C 容量翻倍占用无额外 Buffer,但受模板/格式约束

关键结论:两者不互为替代,而是掩盖不同层级的串行。一个 base 块的完整耗时可以拆解为:

T_base_block ≈ max(T_MMAD_base, T_MTE1_base, T_MTE2_base) + T_Fixpipe_base_tail

开 L0C DB 的效果:下一个 base 块的 MMAD 可以并行启动,把「当前块 Fixpipe」的时间藏进下一个块的 MMAD 里。但如果当前块的 MMAD 本身就比 Fixpipe 短(计算快、搬出慢),下一个块的 MMAD 很快算完,又要等当前块的 Fixpipe → L0C DB 掩盖不住「计算远快于搬出」的场景

开 UnitFlag 的效果:把当前块内部的 MMAD 与 Fixpipe 交错,Fixpipe 不再等整个 base 块算完,而是边算边搬。即使当前块 MMAD 很快,Fixpipe 也能跟着 MMAD 的进度流水走,把大部分搬出时间藏进 MMAD 里。

所以:L0C DB 掩盖「块间」串行,UnitFlag 掩盖「块内」串行;在「单块 MMAD 短、Fixpipe 长」的场景,L0C DB 掩盖不住,必须靠 UnitFlag;在「单块 MMAD 长、Fixpipe 短」的场景,L0C DB 已足够,UnitFlag 收益小。


2. mat_mul_v3 中两者的实际使用场景

2.1 L0C Double Buffer 的使用场景(Host tiling 决策)

mat_mul_v3 的 L0C DB 由 Host tiling 层计算 runInfo.dbL0C,写入 tiling data 的 dbL0C/l0cDB 字段,Device 侧消费。源码中 dbL0C 的设置点:

设置点文件条件结果
① 初始默认matmul_v3_tiling_helper.cpp:134(ResetBaseDefault)dbL0C = DB_OFF_SIZE = 1(默认关)
② 主搜索后matmul_v3_tiling_helper.cpp:490(GetRebalanceBlock 末尾)baseM·baseN·4B·2 ≤ L0C(256KB)满足则 dbL0C=2,否则 1
③ A 全载matmul_v3_basic_aswt_tiling.cpp:203(DoAL1FullLoad)同上同上
④ B 全载matmul_v3_basic_aswt_tiling.cpp:274(DoBL1FullLoad)同上同上

核心条件统一为:

dbL0C = (baseM · baseN · DATA_SIZE_FP32 · DB_SIZE ≤ L0C容量) ? 2 : 1

代入 950PR 数值(L0C=256KB=262144B,DATA_SIZE_FP32=4B,DB_SIZE=2):

baseM·baseN·4·2 ≤ 262144  →  baseM·baseN ≤ 32768 = 2^15

baseM=baseN=256 时:256·256 = 65536 > 32768dbL0C=1(关)

baseM=256、baseN=128 时:256·128 = 32768dbL0C=2(开)

物理含义:baseM=baseN=256 的 FP32 累加块恰好填满整个 L0C(256KB),开不了 DB;只有把 baseN(或 baseM)砍到一半,L0C 才能容纳两份,才能开 DB。这与「baseM=baseN=256 是重复读最少的最优解」存在张力:base 块取 256×256 时,L0C 被占满,无法用 DB 掩盖 Fixpipe

使用场景总结:mat_mul_v3 中,L0C DB 在绝大多数分支都可能开启(普通 ASWT、A/B 全载、StreamK 复用同一引擎),但前提都是 baseM·baseN ≤ 32768。当最优 base 块是 256×256 时,dbL0C 强制为 1。

2.2 UnitFlag 的使用场景(Device kernel 开启)

mat_mul_v3 中 UnitFlag 由 kernel 侧的 MatmulConfig.enUnitFlag 或自定义搬出回调的 fixpipeParams.unitFlag 控制。检索结果:

if (params->enUnitFlag) {
    fixpipeParams.unitFlag = MM_FIX_PIPE_UNIT_FLAG; // 使能unitflag的参数 3U
}

这里 params->enUnitFlag 来自上层传入的搬出参数,unitFlag = 3U 直接写进 FixpipeParamsC310

使用场景总结:mat_mul_v3 中 UnitFlag 的显式、确定开启只发生在 StreamK 分支的自定义搬出回调里(StreamK 的部分和需要从 L0C 搬到 GM workspace,再由 AIV 累加,搬出路径特殊)。普通 ASWT/A/B 全载分支是否开 UnitFlag,取决于 GetMDLConfig 的默认值(官方口径:MDL 模板默认不使能),源码没有显式强制开启。


3. 适用条件边界:什么时候用哪个

3.1 L0C Double Buffer 的适用边界

条件说明
baseM·baseN ≤ 32768(FP32 累加)硬约束:L0C 必须能容纳两份 base 块
存在「块间 Fixpipe 串行」即 base 块数 > 1(多 base 块循环)
Fixpipe 耗时与 MMAD 相当或更短若 Fixpipe 远快于 MMAD,开不开 DB 都掩盖得住;若 Fixpipe 远慢,DB 掩盖不住(需 UnitFlag)
无 L0C 累加冲突若 base 块需要在 L0C 上做 K 方向累加(split-K 等),DB 语义复杂

典型受益场景:baseM=256、baseN=128 的矩形 base 块,多 base 块循环,MMAD 与 Fixpipe 耗时相当。

3.2 UnitFlag 的适用边界(官方文档明确约束)

约束内容
模板限制仅支持 Norm、IBShare、MDL 三个模板(MatmulConfig.enUnitFlag 字段说明)
流水互斥使能 UnitFlag 时,不支持同时存在 CO1(L0C)→GM 和 A1(L1)→GM 两种搬出流水
累加限制使能 UnitFlag + L0C 累加时,不支持多次 Iterate 计算、一次 GetTensorC 输出
收益前提仅当 MMAD 与 FIXPIPE 串行且未被其他流水掩盖时收益大;若已被 MTE2 Bound 掩盖,收益很小
默认状态Norm/IBShare 默认使能,MDL 默认不使能(需显式 enUnitFlag=true)

典型受益场景:单个 base 块内 MMAD 很快、Fixpipe 慢(如小 K、大 MN),且模板是 Norm/IBShare/MDL、无 L1→GM 混合搬出。

3.3 两者的叠加与互斥判断

从官方约束与源码实现看:


4. 对用户问题的直接回答

存在。Host 层 dbL0CbaseM·baseN·4B·2 ≤ L0C(256KB)(即 baseM·baseN ≤ 32768)时置 2,普通 ASWT、A/B 全载、StreamK(复用同一引擎)都会走到。但当最优 base 块是 256×256 时,dbL0C=1(开不了)。

存在,且显式开启点唯一:StreamK 分支的自定义搬出回调 CustomDataCopyOutmat_mul_stream_k_kernel.h),在 params->enUnitFlag 为真时设 fixpipeParams.unitFlag = 3U。普通 ASWT 分支的 UnitFlag 状态取决于 MM_CFG_NO_PRELOADGetMDLConfig)的默认值——官方口径 MDL 默认不使能,因此普通 ASWT 路径上 UnitFlag 大概率是关的,需对照 GetMDLConfig 实际参数确认。

不是。两者掩盖对象不同:L0C DB 掩盖块间、UnitFlag 掩盖块内。在「单块 MMAD 长、多块循环」的场景,L0C DB 已能很好掩盖,UnitFlag 收益小;在「单块 MMAD 短、Fixpipe 长」的场景,L0C DB 掩盖不住(下一块 MMAD 很快算完又要等当前块搬出),必须靠 UnitFlag。两者是正交互补,不是替代关系。


5. 对 mat_mul_v3 的观察与后续建议

  1. UnitFlag 在普通 ASWT 路径的默认状态需确认MM_CFG_NO_PRELOAD = GetMDLConfig(..., true) 的最后一个参数是否真是 enUnitFlag、MDL 默认值是否生效,建议对照 GetMDLConfig API 文档核实。若普通 ASWT 默认未开 UnitFlag,而 StreamK 开了,两者在 Fixpipe 掩盖上的行为不一致,值得在性能模型里区分。
  2. dbL0C 与 baseM/baseN 的耦合dbL0C 直接由 baseM·baseN 决定,但 baseM/baseN 的搜索(GetRebalanceBlock)目标是 CubeBound + 负载均衡,没有把「dbL0C 能否开」纳入目标函数。当 256×256(开不了 DB)与 256×128(能开 DB)性能接近时,模型可能选前者而牺牲 DB 收益——这是一个潜在的 tiling 改进点。
  3. 性能模型需区分两种掩盖机制:后续 Phase 1 的 T_total = max(...) 模型里,Fixpipe 项应区分「块间掩盖(DB)」和「块内掩盖(UnitFlag)」,否则对 Fixpipe Bound 场景会高估或低估。

附录:关键证据索引

证据位置
UnitFlag 定义与约束昇腾NPU知识库/CANN社区版9.2.0-beta.1/01_AscendC算子开发/292_..._Matmul高阶API开启UnitFlag.md、商用版 9.0.0 203_..._使能UnitFlag.md
enUnitFlag 字段与默认值昇腾NPU知识库/CANN商用版9.0.0/02_API参考/AscendC_API/626_..._MatmulConfig.md
dbL0C 设置条件ops-nn/matmul/mat_mul_v3/op_host/op_tiling/arch35/matmul_v3_tiling_helper.cpp:490
dbL0C 在全载分支.../matmul_v3_basic_aswt_tiling.cpp:203,274
UnitFlag 显式开启ops-nn/matmul/mat_mul_v3/op_kernel/arch35/mat_mul_stream_k_kernel.h:36-37
MM_CFG_NO_PRELOAD 定义ops-nn/matmul/mat_mul_v3/op_kernel/mat_mul_v3_common.h:72