Files
matmul-analysis/Matmul/UnitFlag能否取代L0C_DoubleBuffer.md

20 KiB
Raw Blame History

UnitFlag 能否完全取代 L0C Double Buffer —— 昇腾 950PRDAV_3510时序与数据流分析

续问:既然 UnitFlag 能在单个 base 块内部就把 max(T_MMAD, T_Fixpipe) 做满,那 base 块之间是不是就不用考虑 Fixpipe 这一级流水了两者能否叠加UnitFlag 能否完全取代 L0C 双缓冲? 分析方法:以 NPU 数据通路 + 流水线时序图为主线,结合 mat_mul_v3 源码证据dbL0C 决策、enUnitFlag 使能点)与官方约束,逐层论证。 配套同仓《UnitFlag与L0C_DoubleBuffer分析.md》基础概念与边界、《3.1_mat_mul_v3源码解析.md》分支与 tiling


0. 先修正并锁定一个前提事实

上一轮文档曾把「普通 ASWT 的 UnitFlag 状态」标记为"待确认"。本轮查实 GetMDLConfig 的函数签名后定死

// 官方 GetMDLConfig 签名(第 7 个位置参数即 enUnitFlag
GetMDLConfig(intrinsicsLimit, batchLoop, doMTE2Preload, isVecND2NZ,
             isPerTensor, hasAntiQuantOffset, enUnitFlag=false, ...)

// mat_mul_v3_common.h
constexpr MatmulConfig MM_CFG_NO_PRELOAD =
    GetMDLConfig(false, false, 0, false, false, false, /*enUnitFlag=*/true);

MM_CFG_NO_PRELOAD 的第 7 个实参是 trueenUnitFlag 被显式开启。而 mat_mul_v3 的普通 ASWT、A/B 全载、SplitK 等模板 kernel 的默认模板参数就是 MM_CFG = MM_CFG_NO_PRELOADStreamK 又在自定义搬出回调 CustomDataCopyOut 里再显式 fixpipeParams.unitFlag = MM_FIX_PIPE_UNIT_FLAG(=3)

结论mat_mul_v3 在 950PR 的主路径上,UnitFlag 默认是开启的(普通模板经 MM_CFG_NO_PRELOADStreamK 经回调)。这与"MDL 模板默认不使能"并不矛盾——MDL 默认关,但 mat_mul_v3 用 GetMDLConfig 把这一位显式置真了。


1. 两条掩盖路径的时序本质(先把"流水"画清楚)

设 MTE1L1→L0与 MTE2GM→L1已由 L1/L0 的 Double Buffer 掩盖(这是 tiling 的常规动作,与本文讨论的 L0C 无关),则单个 base 块的耗时只需关注 MMAD 计算Fixpipe 搬出 两级:

t_mm = baseM·baseN·baseK / R_cube        MMAD 计算时间R_cube=8192 FLOP/cycle
t_fp = baseM·baseN·dtypeC / BW_fp        Fixpipe 搬出时间dtypeC=输出dtype字节数

1.1 无 DB 无 UnitFlag —— 完全串行

块n:   [==== MMAD ====]··等待··[---- Fixpipe ----]
块n+1:                          [==== MMAD ====]··等待··[---- Fixpipe ----]
周期 T = t_mm + t_fp

L0C 只有一份Fixpipe 搬出期间 Cube 不能写 → 块内串行;且当前块 Fixpipe 不结束,下一块 MMAD 不能开始 → 块间也串行。这是最差形态。

1.2 只开 L0C Double Buffer —— 块间掩盖

L0C 分两份A/B块 n 在 L0C.A 搬出的同时,块 n+1 在 L0C.B 上算:

L0C.A(块n):   [==== MMAD ====]········[---- Fixpipe ----]
L0C.B(块n+1):            [==== MMAD ====]········[---- Fixpipe ----]
L0C.A(块n+2):                                   [==== MMAD ====]···
周期 T = max(t_mm, t_fp)

关键DB 让"块 n 的 Fixpipe"与"块 n+1 的 MMAD"重叠,把块间等待消掉。但注意两点:

  • 掩盖成立的前提一baseM·baseN·4B·2 ≤ L0C256KBbaseM·baseN ≤ 32768,否则 L0C 装不下两份DB 开不了。baseM=baseN=256 时 256·256=65536 > 32768恰恰开不了 DB
  • 掩盖成立的前提二t_mm ≥ t_fp。若 t_mm < t_fp(计算快、搬出慢),块 n+1 的 MMAD 很快算完,仍要空等块 n 的 Fixpipe 收尾 → 周期退化为 T = t_fp,瓶颈转移到 FixpipeDB 掩盖不充分。

1.3 只开 UnitFlag —— 块内 512B 细粒度流水

UnitFlag 把单个 base 块按 512B 切成若干 micro-tileMMAD 每算完一个 512B 就立即触发对应 Fixpipe两者在同一块内交错:

块n(L0C): [M][M][M]...[M]      ← MMAD 按 512B 推进
           [F][F][F]...[F]    ← Fixpipe 跟随 512B逐段搬出
周期 T ≈ max(t_mm, t_fp) + tail
        tail = 最后一个 512B micro-tile 的 Fixpipe 尾开销,可忽略)

关键差异UnitFlag 不依赖"下一块的 MMAD"来掩盖,它在当前块内部就让 Fixpipe 跟着 MMAD 的进度流水走。因此:

  • 不要求 L0C 装两份(不消耗 L0C 容量);
  • 不要求 t_mm ≥ t_fp:无论谁长谁短,都能逼近 max(t_mm, t_fp)。即使 t_mm < t_fpFixpipe 也是边算边搬Cube 不停(是 Fixpipe 流水成为瓶颈,而非 Cube 空等)。

2. 块内能 max 了,块间还需不需要考虑?—— 叠加性论证

这是你追问的核心。我们严格区分两种情形。

2.1 情形 AK 不在单块内累加(一次 Iterate 算完一整块 K

这是 mat_mul_v3 最常见的形态:一个 base 块的 singleCoreK 完整累加进 L0C算完一次性 Fixpipe 搬出到 GM。

  • 开 UnitFlag 后,单块周期已经是 ≈ max(t_mm, t_fp)。块 n 的 Fixpipe 段与块 n 的 MMAD 段在时间上几乎完全重叠(只差一个 tail
  • 此时若再开 L0C DB想做的是"块 n 的 Fixpipe 与块 n+1 的 MMAD 重叠"——但块 n 的 Fixpipe 已经被块 n 自己的 MMAD 占满了时间窗,块 n+1 的 MMAD 本可以在块 n 的 Fixpipe 尾段就开始,可 UnitFlag 已经把块 n 的 Fixpipe"摊平"到整个块 n 的计算区间里,块 n+1 能利用的"纯 Fixpipe 空窗"只剩最后一个 tail。

所以在这个情形下DB 的块间掩盖与 UnitFlag 的块内掩盖争夺的是同一段时间(同一块的 Fixpipe被 UnitFlag 掩盖后DB 几乎无的放矢 → 两者收益不叠加。 你的判断"开了 UnitFlag上一块的 cube 计算已经和上一块的 fixpipe 重叠了,就没法再和下一块的 cube 重叠"在 K 内一次累加场景下成立

2.2 情形 BK 在单块内分片累加(多次 Iterate 累加进同一 L0C

singleCoreK 很大、被切成多个 baseK 分片时,同一 base 块要做多轮 MMAD→部分结果留在 L0CFixpipe 只在最后一次累加完成后才整体搬出

  • 这里 UnitFlag 的约束就暴露了:官方明确「使能 UnitFlag + L0C 累加时,不支持多次 Iterate 计算、一次 GetTensorC 输出」。也就是说,L0C 上做多分片 K 累加的场景UnitFlag 用不了
  • 此时若 Fixpipe 搬出与后续计算存在串行,只能靠 L0C DB 做块间掩盖(一块在累加,另一块的成品在搬出)。

所以 UnitFlag 的使用边界里明确排除了「L0C K 累加」这一类场景,而这恰恰是多分片大 K 的常见形态 → 这类场景 UnitFlag 无法取代 DB。

2.3 叠加性结论

场景 UnitFlag L0C DB 能否叠加 主导掩盖
K 一次累加t_mm≥t_fp能开DB 块内掩盖 块间掩盖 收益不叠加(同一段 Fixpipe 任一即可UnitFlag 更省 L0C
K 一次累加t_mm<t_fp 块内掩盖Cube不停 掩盖不足(退化 t_fp DB 无效UnitFlag 主导 UnitFlag
K 一次累加baseM·baseN>32768 块内掩盖 开不了 DB 不可用UnitFlag 主导 UnitFlag
K 分片累加(大 K 不支持多次Iterate一次输出 块间掩盖 UnitFlag 不可用DB 主导 L0C DB

3. UnitFlag 能否完全取代 L0C DB

不能完全取代。 结合上面论证与 NPU 架构约束,分三层说清:

3.1 从掩盖能力看UnitFlag 掩盖域 ⊇ DB在它能用的场景内

在「K 一次累加、C 输出 GM、Norm/IBShare/MDL 模板」这个 UnitFlag 的合法域内:

  • DB 达到 max(t_mm,t_fp) 需要两个前提L0C 装得下两份 + t_mm≥t_fp
  • UnitFlag 达到 ≈max(t_mm,t_fp) 几乎无条件(块内 512B 流水),还省下 L0C 容量(这份容量可用来把 baseM/baseN 做大,减少重复读)。

所以在 UnitFlag 合法域内DB 相对它没有独立价值,可以被取代。你的直觉在这一层是对的。

3.2 从合法域看UnitFlag 有硬约束DB 是兜底

UnitFlag 的官方约束(缺一不可):

  1. 模板限制:仅 Norm / IBShare / MDL 三模板;
  2. 流水互斥:使能后不允许同时存在 CO1(L0C)→GM 与 A1(L1)→GM 两种搬出流水
  3. 累加限制:使能 + L0C 累加时,不支持多次 Iterate、一次 GetTensorC 输出
  4. 收益前提:仅当 MMAD 与 Fixpipe 串行且未被 MTE2 等其他流水掩盖时才有收益。

凡是落在这些约束之外的场景(最典型就是 §2.2 的多分片 K 在 L0C 累加UnitFlag 直接不可用,只能退回到 L0C DB 做块间掩盖。

3.3 从 mat_mul_v3 实际看:默认已开 UnitFlagDB 是「补位」而非「主选」

  • mat_mul_v3 主路径 MM_CFG_NO_PRELOAD 已把 enUnitFlag 置真StreamK 又在回调里显式 unitFlag=3UnitFlag 是该算子的默认掩盖手段
  • dbL0C 的判据 baseM·baseN ≤ 32768 与"最优 base=256×256"天然冲突256×256 开不了 DB——这说明在 950PR 上,设计者的倾向就是靠 UnitFlag 做块内掩盖DB 只在 base 块被迫做小(如矩形 256×128或落在 UnitFlag 约束之外时补位

最终判断UnitFlag 在其合法域内可以取代 L0C DB且更优但因「模板/流水互斥/累加」三重硬约束,它无法覆盖全部场景L0C DB 作为不依赖这些约束的块间掩盖手段,是 UnitFlag 失效场景下的必要补充,两者是主从互补关系,而非简单的"新替旧"。


3.4 对官方三条约束的逐条考据(答疑)

针对三条容易被误读的官方约束,逐一给出原文出处、准确含义、以及在 mat_mul_v3 / 950PR 上的实际适用性。

3.4.1 「UnitFlag + L0C 累加时,不支持多次 Iterate 计算、一次 GetTensorC 输出」——具体是什么约束?

官方原文出处(两处,措辞略异,商用版更完整):

  • 昇腾社区官网 CANN 社区版 → 算子实践参考 → 优秀实践 → Matmul 性能调优案例 → 《Matmul 高阶 API 开启 UnitFlag》,约束条件第 3 条:

    开启 UnitFlag 功能时,若同时开启 L0C 累加 功能,不支持多次计算、一次输出。 (注:该页面在线渲染时 "L0C 累加" 一词在部分版本缺失,仅显示"若同时开启功能",以下方商用版为准。)

  • 昇腾社区官网 CANN 商用版 → 同名章节 《Matmul 高阶 API 使能 UnitFlag》,约束条件第 3 条(完整表述):

    使能 UnitFlag 功能时,若同时使能 L0C 累加功能,不支持多次 Iterate 计算、一次 GetTensorC 输出

准确含义拆解:这条约束针对的是 「同一个 base 块K 方向分多片,在 L0C 上驻留并多次累加,最后才一次性 GetTensorC 搬出」 的编程形态。即:

// 被禁止的形态K 在 L0C 内分片累加,最后一次才搬出
for (kSlice = 0; kSlice < kSliceNum; kSlice++) {   // 多次 Iterate
    mm.Iterate();          // 每片 K 的部分结果累加进同一块 L0CL0C 累加)
}
mm.GetTensorC(c);          // 只在最后一次性搬出(一次输出)

为什么 UnitFlag 与这种形态冲突UnitFlag 的硬件机制是「MMAD 每产出 512B 成品,就立即触发 Fixpipe 把这段搬走」。但 L0C 累加形态下,前面若干次 Iterate 的结果是部分和,还必须留在 L0C 里等后续 K 片继续累加,不能被搬走。一旦开 UnitFlagMMAD 算出 512B 就被 Fixpipe 立即搬出L0C 里就保不住部分和、后续累加无从谈起——两者在"这 512B 到底是驻留还是立即搬走"上直接矛盾,故硬件/API 不允许叠加。

mat_mul_v3 是否踩这条约束?——不踩(常规路径)。看 mat_mul_asw_kernel.h:140-141

mm_.Iterate();
mm_.GetTensorC(cGlobal_[...], enAtomic || kIndex != 0);   // 每次 Iterate 紧跟一次 GetTensorC

mat_mul_v3 的普通 ASWT 是 「一次 Iterate → 一次 GetTensorC」一一配对(即使在 splitKRound 循环里,也是 Iterate 与 GetTensorC 成对出现,用 enAtomic || kIndex!=0 在 GM 侧做原子加,而非在 L0C 里驻留累加)。它不是"多次 Iterate、一次 GetTensorC"因此常规路径不触碰这条约束UnitFlag 可用。

真正会踩这条的场景:把 split-K 的核内部分和放在 L0C 里驻留累加L0C 累加),或 intraBlockPartSum(两 AIV 结果在 L0C 累加)这类显式 L0C 累加特性。StreamK 之所以要绕道 workspace 由 AIV 累加、而不是在 L0C 里累加,正是为了避开这条。

3.4.2 一次 Iterate 最大支持的 K 是多少?

先澄清概念:官方约束里没有任何一条把 "一次 Iterate 的 K 上限" 与 UnitFlag 挂钩。UnitFlag 约束的不是 K 的大小,而是"累加形态"(见上一条)。一次 Iterate 能算多大的 KL0C 之外的资源L1/L0A/L0B 容量、stepKa/stepKb 流水深度、baseK 决定,与 UnitFlag 无关:

  • 单次 Iterate 的 K 由 singleCoreK 决定,而 singleCoreK 在 L1 内按 stepKa·baseKA 侧)/ stepKb·baseKB 侧)分多片流水喂给 Cube每片的中间结果在同一 L0C 上原地累加(这是 MMAD 的固有累加语义,不是"多次 Iterate 一次输出",也不违反 UnitFlag——因为整个 singleCoreK 是一次 Iterate 内部完成的)。
  • 换句话说:一次 Iterate 内部Cube 会在 L0C 上做 singleCoreK/baseK 次累加,这在 UnitFlag 下是允许的UnitFlag 禁止的是"跨多次 Iterate 的累加 + 只在最后一次输出"。

结论没有"一次 Iterate 最大 K 受 UnitFlag 限制"这回事。一次 Iterate 的 K 上限由 L1/L0 容量与 stepK 流水决定(例如 950PR 上 maxStepK ≤ 8baseK ≈ 128B/dtypeUnitFlag 只管"算出的 512B 何时搬出",不管 K 多大。把这条约束理解成"K 不能超过某值"是误读——它限的是"累加跨几次 Iterate",不是"K 的绝对大小"。

3.4.3 「不允许同时存在 CO1(L0C)→GM 与 A1(L1)→GM 两种搬出流水」——是什么意思L1 能直接写回 GM 吗?

官方原文出处同上两篇《Matmul 高阶 API使能/开启UnitFlag》约束条件第 2 条:

使能 UnitFlag 功能时,不支持算子内同时存在 CO1(L0C) 搬出到 Global MemoryA1(L1) 搬出到 Global Memory 的两种流水。

这条的字面意思UnitFlag 开启时,同一个算子里不能同时出现两类搬出流水——一类是 L0C→GM结果搬出走 Fixpipe另一类是 L1(A1)→GM从 L1 直接把数据写回 GM。即搬出通路的"源头"必须统一为 L0C不能 L0C 和 L1 混着当搬出源。

「L1 能直接写回 GM 吗?」——分架构看,这正是关键点

  • 在 950PR351x 架构)上:不能。 950 白皮书明确351x 相对上代删除了 L1→GM 的回写通路(同时也删除了 GM→L0A/L0B 的直达通路),一切矩阵数据的搬出都必须经 L0C→Fixpipe→GM。所以在 950PR 上根本不存在"L1→GM"这条物理通路,这条约束对 950PR 而言自然满足、不会触发——它更像是从支持 L1→GM 的老架构(如部分 A2/A3 代际)沿用过来的兼容性约束。
  • mat_mul_v3 在 950PR 上的搬出源:普通 ASWT 的结果一律走 L0C→Fixpipe→GM(C)mm_.GetTensorC(cGlobal...)),没有任何 L1→GM 的直接搬出 → 符合该约束。

小结:这条约束的语义是"搬出源必须唯一(只能是 L0C",防止 UnitFlag 的细粒度同步在"两种不同源头的搬出"上产生时序混乱。在 950PR 上由于 L1→GM 通路已被硬件删除,这条约束自动成立,不会成为限制 mat_mul_v3 开 UnitFlag 的因素。

3.4.4 三条约束在 mat_mul_v3 / 950PR 上的实际生效性汇总

官方约束 准确含义 950PR 上是否实质限制 mat_mul_v3
仅 Norm/IBShare/MDL 模板 UnitFlag 只在三个高阶模板下有效 mat_mul_v3 用 MDLGetMDLConfig满足
不能同时有 CO1→GM 与 A1→GM 两种搬出 搬出源必须唯一为 L0C 351x 已删 L1→GM 通路 → 自动满足,无实质限制
L0C 累加时不支持多次 Iterate 一次输出 禁止跨 Iterate 的 L0C 驻留累加 + 末次才搬出 mat_mul_v3 常规路径是 Iterate/GetTensorC 一一配对 → 不触碰UnitFlag 可用;仅显式 L0C 累加(核内 split-K 驻留 / intraBlockPartSum才触碰
MTE2 Bound 已掩盖时收益小 Fixpipe 已被搬运流水掩盖时无需再开 这是收益问题不是正确性问题,不影响可用性

由此可得更精确的判断mat_mul_v3 在 950PR 上常规路径(含普通 ASWT、A/B 全载、以及把 K 分片放到 GM workspace 由 AIV 累加的 StreamK都满足 UnitFlag 的可用条件且默认已开启;真正被 UnitFlag 排除的是"在 L0C 里做跨 Iterate 驻留累加"这一类形态——而这种形态在 mat_mul_v3 里被刻意用"GM 原子加 / workspace + AIV 累加"规避了。这进一步支撑了 §3.3 的结论:在 950PR 上 UnitFlag 的合法域覆盖了 mat_mul_v3 的全部主路径L0C DB 的补位空间被压缩得很小


4. 对本项目性能建模与最优方案的直接推论

  1. 建模时 Fixpipe 项要分两种掩盖:在性能模型 T_total = max(T_MMAD, T_MTE2, T_MTE1, T_FIXPIPE) 中,T_FIXPIPE 的实际暴露量应写成
    • 开 UnitFlag合法域内T_FIXPIPE_exposed ≈ tail512B 尾开销)→ 基本可以忽略Fixpipe 通常不构成独立瓶颈;
    • 不开 UnitFlag或不可用T_FIXPIPE_exposed = max(0, t_fp - t_mm)DB 掩盖后的残余),且仅当 baseM·baseN ≤ 32768 时 DB 才生效,否则 T_FIXPIPE_exposed = t_fp(全暴露)。
  2. 最优 tiling 的取舍被简化:既然 UnitFlag 默认开baseM/baseN 的搜索不必再为"能否开 L0C DB"让步,可以更激进地逼近 256×256重复读最少。原先"为开 DB 而把 baseN 砍半"的权衡在 UnitFlag 合法域内不成立。这是对 §3.2 GetRebalanceBlock 的一处实质修正点。
  3. 真正需要单独建模的 K 累加形态:需要警惕的不是 mat_mul_v3 的主路径,而是"在 L0C 里做跨 Iterate 驻留累加"这一类形态(核内 split-K 驻留 / intraBlockPartSum 等)。这类形态被 UnitFlag 约束排除,只能靠 L0C DB 做块间掩盖。mat_mul_v3 的 StreamK 用 workspace + AIV 累加规避了它,因此主路径不受影响;但若未来引入 L0C 驻留累加的新模板,性能模型需为其单列 Fixpipe 掩盖方式(回到 DB 主导)。

附录:关键证据索引

证据 位置
UnitFlag 三条约束官方原文(在线) 昇腾社区官网 → CANN → 算子实践参考 → 优秀实践 → Matmul 性能调优案例 → 《Matmul 高阶 API 开启 UnitFlag》社区版/ 《Matmul 高阶 API 使能 UnitFlag》商用版。社区版直达链接见任务输入所给 UnitFlag.md 页面
UnitFlag 三条约束官方原文(本地) 昇腾NPU知识库/CANN社区版9.2.0-beta.1/01_AscendC算子开发/292_..._开启UnitFlag.md第19/21行昇腾NPU知识库/CANN商用版9.0.0/01_AscendC算子开发/203_..._使能UnitFlag.md第21/23行表述最完整
GetMDLConfig 签名第7参=enUnitFlag 昇腾NPU知识库/CANN商用版9.0.0/02_API参考/AscendC_API/631_..._GetMDLConfig.md
enUnitFlag 默认值与约束 昇腾NPU知识库/CANN商用版9.0.0/02_API参考/AscendC_API/626_..._MatmulConfig.md
UnitFlag 512B 细粒度同步定义 昇腾NPU知识库/CANN社区版9.2.0-beta.1/01_AscendC算子开发/292_..._开启UnitFlag.md
351x 删除 L1→GM / GM→L0 直达通路 昇腾NPU知识库/00_硬件/昇腾950_NPU架构白皮书.txtCV融合/数据通路章节);同仓 昇腾950PR/昇腾950PR架构解读.md §5.1
MM_CFG_NO_PRELOAD 显式开 enUnitFlag ops-nn/matmul/mat_mul_v3/op_kernel/mat_mul_v3_common.h:72
普通 ASWT Iterate/GetTensorC 一一配对 ops-nn/matmul/mat_mul_v3/op_kernel/arch35/mat_mul_asw_kernel.h:140-141
StreamK 回调显式 unitFlag=3 ops-nn/matmul/mat_mul_v3/op_kernel/arch35/mat_mul_stream_k_kernel.h:36-37
StreamK K 分片走 workspace + AIV 累加 ops-nn/matmul/mat_mul_v3/op_kernel/arch35/mat_mul_stream_k_kernel.h:188-259mat_mul_stream_k_block.h
dbL0C 判据 baseM·baseN≤32768 ops-nn/matmul/mat_mul_v3/op_host/op_tiling/arch35/matmul_v3_tiling_helper.cpp:490