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

227 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>UnitFlag 能否完全取代 L0C Double Buffer —— 昇腾 950PRDAV_3510时序与数据流分析</title>
<style>
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; max-width: 980px; margin: 40px auto; padding: 0 24px; line-height: 1.75; color: #1f2328; background:#fff; }
h1 { font-size: 28px; border-bottom: 2px solid #d0d7de; padding-bottom: 12px; }
h2 { font-size: 22px; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 36px; }
h3 { font-size: 18px; margin-top: 28px; }
code { background:#f6f8fa; padding: 2px 6px; border-radius: 4px; font-family: "SF Mono", Consolas, monospace; font-size: 0.9em; color:#c7254e; }
pre { background:#f6f8fa; padding: 16px; border-radius: 8px; overflow-x:auto; }
pre code { background:none; padding:0; color:#24292e; }
table { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 14px; }
th, td { border: 1px solid #d0d7de; padding: 8px 12px; text-align: left; vertical-align: top; }
th { background:#f6f8fa; font-weight: 600; }
blockquote { border-left: 4px solid #d0d7de; margin: 16px 0; padding: 4px 16px; color:#57606a; background:#f6f8fa; }
hr { border:none; border-top:1px solid #d0d7de; margin: 24px 0; }
li { margin: 4px 0; }
strong { color:#0a3069; }
</style>
</head><body><h1>UnitFlag 能否完全取代 L0C Double Buffer —— 昇腾 950PRDAV_3510时序与数据流分析</h1>
</blockquote>
<hr/>
<h2>0. 先修正并锁定一个前提事实</h2>
</blockquote>
<pre><code>// 官方 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);</code></pre>
<p><code>MM_CFG_NO_PRELOAD</code> 的第 7 个实参是 <code>true</code><strong>enUnitFlag 被显式开启</strong>。而 mat_mul_v3 的普通 ASWT、A/B 全载、SplitK 等模板 kernel 的默认模板参数就是 <code>MM_CFG = MM_CFG_NO_PRELOAD</code>StreamK 又在自定义搬出回调 <code>CustomDataCopyOut</code> 里再显式 <code>fixpipeParams.unitFlag = MM_FIX_PIPE_UNIT_FLAG(=3)</code></p>
<p><strong>结论</strong>mat_mul_v3 在 950PR 的主路径上,<strong>UnitFlag 默认是开启的</strong>(普通模板经 MM_CFG_NO_PRELOADStreamK 经回调)。这与&quot;MDL 模板默认不使能&quot;并不矛盾——MDL 默认关,但 mat_mul_v3 用 <code>GetMDLConfig</code> 把这一位显式置真了。</p>
<hr/>
<h2>1. 两条掩盖路径的时序本质(先把&quot;流水&quot;画清楚)</h2>
<p>设 MTE1L1→L0与 MTE2GM→L1已由 L1/L0 的 Double Buffer 掩盖(这是 tiling 的常规动作,与本文讨论的 L0C 无关),则单个 base 块的耗时只需关注 <strong>MMAD 计算</strong><strong>Fixpipe 搬出</strong> 两级:</p>
<pre><code>t_mm = baseM·baseN·baseK / R_cube MMAD 计算时间R_cube=8192 FLOP/cycle
t_fp = baseM·baseN·dtypeC / BW_fp Fixpipe 搬出时间dtypeC=输出dtype字节数</code></pre>
<h3>1.1 无 DB 无 UnitFlag —— 完全串行</h3>
<pre><code>块n: [==== MMAD ====]··等待··[---- Fixpipe ----]
块n+1: [==== MMAD ====]··等待··[---- Fixpipe ----]
周期 T = t_mm + t_fp</code></pre>
<p>L0C 只有一份Fixpipe 搬出期间 Cube 不能写 → 块内串行;且当前块 Fixpipe 不结束,下一块 MMAD 不能开始 → 块间也串行。这是最差形态。</p>
<h3>1.2 只开 L0C Double Buffer —— 块间掩盖</h3>
<p>L0C 分两份A/B块 n 在 L0C.A 搬出的同时,块 n+1 在 L0C.B 上算:</p>
<pre><code>L0C.A(块n): [==== MMAD ====]········[---- Fixpipe ----]
L0C.B(块n+1): [==== MMAD ====]········[---- Fixpipe ----]
L0C.A(块n+2): [==== MMAD ====]···
周期 T = max(t_mm, t_fp)</code></pre>
<p><strong>关键</strong>DB 让&quot;块 n 的 Fixpipe&quot;&quot;块 n+1 的 MMAD&quot;重叠,把块间等待消掉。但注意两点:</p>
<ul>
<li><strong>掩盖成立的前提一</strong><code>baseM·baseN·4B·2 ≤ L0C</code>256KB<code>baseM·baseN ≤ 32768</code>,否则 L0C 装不下两份DB 开不了。baseM=baseN=256 时 <code>256·256=65536 &gt; 32768</code><strong>恰恰开不了 DB</strong></li>
<li><strong>掩盖成立的前提二</strong><code>t_mm ≥ t_fp</code>。若 <code>t_mm &lt; t_fp</code>(计算快、搬出慢),块 n+1 的 MMAD 很快算完,仍要空等块 n 的 Fixpipe 收尾 → 周期退化为 <code>T = t_fp</code>,瓶颈转移到 FixpipeDB 掩盖不充分。</li>
</ul>
<h3>1.3 只开 UnitFlag —— 块内 512B 细粒度流水</h3>
<p>UnitFlag 把单个 base 块按 512B 切成若干 micro-tileMMAD 每算完一个 512B 就立即触发对应 Fixpipe两者在<strong>同一块内</strong>交错:</p>
<pre><code>块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 尾开销,可忽略)</code></pre>
<p><strong>关键差异</strong>UnitFlag 不依赖&quot;下一块的 MMAD&quot;来掩盖,它在<strong>当前块内部</strong>就让 Fixpipe 跟着 MMAD 的进度流水走。因此:</p>
<ul>
<li><strong>不要求 L0C 装两份</strong>(不消耗 L0C 容量);</li>
<li><strong>不要求 t_mm ≥ t_fp</strong>:无论谁长谁短,都能逼近 <code>max(t_mm, t_fp)</code>。即使 <code>t_mm &lt; t_fp</code>Fixpipe 也是边算边搬Cube 不停(是 Fixpipe 流水成为瓶颈,而非 Cube 空等)。</li>
</ul>
<hr/>
<h2>2. 块内能 max 了,块间还需不需要考虑?—— 叠加性论证</h2>
<p>这是你追问的核心。我们严格区分两种情形。</p>
<h3>2.1 情形 AK 不在单块内累加(一次 Iterate 算完一整块 K</h3>
<p>这是 mat_mul_v3 最常见的形态:一个 base 块的 <code>singleCoreK</code> 完整累加进 L0C算完一次性 Fixpipe 搬出到 GM。</p>
<ul>
<li><strong>开 UnitFlag 后</strong>,单块周期已经是 <code>≈ max(t_mm, t_fp)</code>。块 n 的 Fixpipe 段与块 n 的 MMAD 段在时间上<strong>几乎完全重叠</strong>(只差一个 tail</li>
<li>此时若再开 L0C DB想做的是&quot;块 n 的 Fixpipe 与块 n+1 的 MMAD 重叠&quot;——但<strong>块 n 的 Fixpipe 已经被块 n 自己的 MMAD 占满了时间窗</strong>,块 n+1 的 MMAD 本可以在块 n 的 Fixpipe 尾段就开始,可 UnitFlag 已经把块 n 的 Fixpipe&quot;摊平&quot;到整个块 n 的计算区间里,块 n+1 能利用的&quot;纯 Fixpipe 空窗&quot;只剩最后一个 tail。</li>
</ul>
<p><strong>所以在这个情形下DB 的块间掩盖与 UnitFlag 的块内掩盖争夺的是同一段时间(同一块的 Fixpipe被 UnitFlag 掩盖后DB 几乎无的放矢 → 两者收益不叠加。</strong> 你的判断&quot;开了 UnitFlag上一块的 cube 计算已经和上一块的 fixpipe 重叠了,就没法再和下一块的 cube 重叠&quot;在 K 内一次累加场景下<strong>成立</strong></p>
<h3>2.2 情形 BK 在单块内分片累加(多次 Iterate 累加进同一 L0C</h3>
<p><code>singleCoreK</code> 很大、被切成多个 <code>baseK</code> 分片时,同一 base 块要做多轮 <code>MMAD→部分结果留在 L0C</code><strong>Fixpipe 只在最后一次累加完成后才整体搬出</strong></p>
<ul>
<li>这里 UnitFlag 的约束就暴露了:<strong>官方明确「使能 UnitFlag + L0C 累加时,不支持多次 Iterate 计算、一次 GetTensorC 输出」</strong>。也就是说,<strong>L0C 上做多分片 K 累加的场景UnitFlag 用不了</strong></li>
<li>此时若 Fixpipe 搬出与后续计算存在串行,<strong>只能靠 L0C DB 做块间掩盖</strong>(一块在累加,另一块的成品在搬出)。</li>
</ul>
<p><strong>所以 UnitFlag 的使用边界里明确排除了「L0C K 累加」这一类场景,而这恰恰是多分片大 K 的常见形态 → 这类场景 UnitFlag 无法取代 DB。</strong></p>
<h3>2.3 叠加性结论</h3>
<table><thead><tr><th>场景</th><th>UnitFlag</th><th>L0C DB</th><th>能否叠加</th><th>主导掩盖</th></tr></thead><tbody>
<tr><td>K 一次累加t_mm≥t_fp能开DB</td><td>块内掩盖</td><td>块间掩盖</td><td><strong>收益不叠加</strong>(同一段 Fixpipe</td><td>任一即可UnitFlag 更省 L0C</td></tr>
<tr><td>K 一次累加t_mm&lt;t_fp</td><td>块内掩盖Cube不停</td><td>掩盖不足(退化 t_fp</td><td>DB 无效UnitFlag 主导</td><td><strong>UnitFlag</strong></td></tr>
<tr><td>K 一次累加baseM·baseN&gt;32768</td><td>块内掩盖</td><td><strong>开不了</strong></td><td>DB 不可用UnitFlag 主导</td><td><strong>UnitFlag</strong></td></tr>
<tr><td>K 分片累加(大 K</td><td><strong>不支持</strong>多次Iterate一次输出</td><td>块间掩盖</td><td>UnitFlag 不可用DB 主导</td><td><strong>L0C DB</strong></td></tr>
</tbody></table>
<hr/>
<h2>3. UnitFlag 能否完全取代 L0C DB</h2>
<p><strong>不能完全取代。</strong> 结合上面论证与 NPU 架构约束,分三层说清:</p>
<h3>3.1 从掩盖能力看UnitFlag 掩盖域 ⊇ DB在它能用的场景内</h3>
<p>在「K 一次累加、C 输出 GM、Norm/IBShare/MDL 模板」这个 UnitFlag 的合法域内:</p>
<ul>
<li>DB 达到 <code>max(t_mm,t_fp)</code> 需要<strong>两个前提</strong>L0C 装得下两份 + t_mm≥t_fp</li>
<li>UnitFlag 达到 <code>≈max(t_mm,t_fp)</code> <strong>几乎无条件</strong>(块内 512B 流水),还省下 L0C 容量(这份容量可用来把 baseM/baseN 做大,减少重复读)。</li>
</ul>
<p>所以<strong>在 UnitFlag 合法域内DB 相对它没有独立价值,可以被取代</strong>。你的直觉在这一层是对的。</p>
<h3>3.2 从合法域看UnitFlag 有约束DB 是兜底</h3>
</blockquote>
<p><strong>高阶 API 层官方约束</strong>(出处:昇腾官网 → CANN → 算子实践参考 → 优秀实践 → Matmul 性能调优案例 → 《Matmul 高阶 API 开启 UnitFlag》社区版 / 《Matmul 高阶 API 使能 UnitFlag》商用版&quot;约束条件&quot;一节,逐字摘录):</p>
<ol>
<li><strong>模板限制</strong>UnitFlag 功能仅支持 <strong>Norm、IBShare、MDL</strong> 三个模板。</li>
<li><strong>流水互斥</strong>:开启 UnitFlag 功能时,不支持算子内<strong>同时存在 L0C BufferCO1搬出到 Global Memory 和 L1 BufferA1搬出到 Global Memory 的两种流水</strong></li>
<li><strong>累加限制</strong>:开启 UnitFlag 功能时,若同时开启 <strong>L0C 累加</strong> 功能,不支持<strong>多次 Iterate 计算、一次 GetTensorC 输出</strong></li>
<li><strong>收益前提</strong>:仅当 MMAD 流水与 FIXPIPE 流水<strong>串行执行且未被其他流水掩盖</strong>(比如 MTE2 Bound开启 UnitFlag 才有收益;否则收益很小。</li>
</ol>
<p>这四条是官方文档明确写出的原文(第 2、3 条在 §3.4 逐条考据)。落在高阶 API 约束之外的场景UnitFlag 不可用,<strong>只能退回 L0C DB</strong> 做块间掩盖。</p>
<h3>3.3 从 mat_mul_v3 实际看:默认已开 UnitFlagDB 是「补位」而非「主选」</h3>
<ul>
<li>mat_mul_v3 主路径 <code>MM_CFG_NO_PRELOAD</code> 已把 enUnitFlag 置真StreamK 又在回调里显式 <code>unitFlag=3</code><strong>UnitFlag 是该算子的默认掩盖手段</strong></li>
<li>dbL0C 的判据 <code>baseM·baseN ≤ 32768</code>&quot;最优 base=256×256&quot;天然冲突256×256 开不了 DB——这说明在 950PR 上,<strong>设计者的倾向就是靠 UnitFlag 做块内掩盖DB 只在 base 块被迫做小(如矩形 256×128或落在 UnitFlag 约束之外时补位</strong></li>
</ul>
<p><strong>最终判断</strong>UnitFlag 在其合法域内可以取代 L0C DB且更优但因「模板/流水互斥/累加」三重硬约束,它<strong>无法覆盖全部场景</strong>L0C DB 作为不依赖这些约束的块间掩盖手段,是 UnitFlag 失效场景下的必要补充,两者是<strong>主从互补</strong>关系,而非简单的&quot;新替旧&quot;</p>
<hr/>
<h3>3.4 对官方三条约束的逐条考据(答疑)</h3>
<p>针对三条容易被误读的官方约束,逐一给出原文出处、准确含义、以及在 mat_mul_v3 / 950PR 上的实际适用性。</p>
<h4>3.4.1 「UnitFlag + L0C 累加时,不支持多次 Iterate 计算、一次 GetTensorC 输出」——具体是什么约束?它与&quot;K 分段累加&quot;是什么关系?</h4>
<p><strong>官方原文出处</strong>(高阶 API 层,两处):</p>
<ul>
<li>昇腾社区官网 CANN 社区版 → 算子实践参考 → 优秀实践 → Matmul 性能调优案例 → <strong>《Matmul 高阶 API 开启 UnitFlag》</strong>,约束条件第 3 条:</li>
</ul>
</blockquote>
<ul>
<li>昇腾社区官网 CANN 商用版 → 同名章节 <strong>《Matmul 高阶 API 使能 UnitFlag》</strong>,约束条件第 3 条(表述更完整):</li>
</ul>
</blockquote>
<p><strong>这是&quot;高阶 APIIterate/GetTensorC&quot;层的约束,不是&quot;基础 APIMmad/Copy&quot;层的限制</strong>——这是理解的关键,也是本节要澄清的核心误区。</p>
<p><strong>它约束的准确形态</strong>:用<strong>高阶 API</strong> 编程时,同一个 base 块如果「<code>mm.Iterate()</code> 多次K 分片在 L0C 驻留累加)、最后只 <code>mm.GetTensorC()</code> 一次(一次输出)」,且此时打开了 L0C 累加功能,那么高阶 API 内部的 UnitFlag 封装无法正确处理 → 不允许。即:</p>
<pre><code>// 高阶 API 下被禁止的形态
for (kSlice = 0; kSlice &lt; kSliceNum; kSlice++) {
mm.Iterate(); // 多次 Iterate部分和在 L0C 驻留累加
}
mm.GetTensorC(c); // 只在最后一次搬出</code></pre>
<p><strong>但是——底层基础 APIMmad/Copy 指令)完全支持 K 分段累加,且能做出很好的流水掩盖。</strong> 这是用户指出的关键点官方《UnitFlag —— Mmad 计算关键特性说明》(基础 API/矩阵计算 Tensor API有明确机制与实例逐字核实如下</p>
<p><strong>底层机制</strong>:开启 UnitFlag 后L0C Buffer 按 <strong>512B 内存块</strong>划分每块配一个「单元标志位」unit-flag指示该块可读/可写。<code>Mmad</code>(写)与 <code>Copy</code>/<code>Fixpipe</code>(读)通过 <code>unitFlag</code> 参数控制:</p>
<table><thead><tr><th>unitFlag 取值</th><th>语义(写=Mmad / 读=Fixpipe</th></tr></thead><tbody>
<tr><td><strong>2</strong></td><td>指令执行后<strong>不改变</strong>标志位:写操作写完保持&quot;占用&quot;,让后续 Mmad 还能继续写这块(用于 K 累加的中间片);读操作读完保持&quot;可读&quot;,让后续 Fixpipe 还能继续读</td></tr>
<tr><td><strong>3</strong></td><td>指令执行后<strong>翻转</strong>标志位Mmad 写完置&quot;可读&quot;(放给 Fixpipe 搬Fixpipe 读完置&quot;可写&quot;(放还给 Mmad</td></tr>
</tbody></table>
<p><strong>官方 K 分段累加实例</strong>原文A=128×1024、B=1024×128沿 K 轴迭代,每次迭代 K 长 128共 8 次 Mmad 对应 1 次 Fixpipe</p>
<ul>
<li><strong>前 7 次 Mmad 设 <code>unitFlag=2</code></strong>:写入后把标志位<strong>始终保持 0</strong>,保证后续 Mmad 能继续写入同一块 L0C Buffer即在同一块 L0C 上做 K 累加);</li>
<li><strong>最后 1 次 Mmad 设 <code>unitFlag=3</code></strong>:写入后把标志位置 1保证 Fixpipe 可以读取 L0C Buffer</li>
<li><strong>Fixpipe 设 <code>unitFlag=3</code></strong>:读取后把标志位置 0保证后续 Mmad 接口可以顺利写入。</li>
</ul>
<p><strong>结论修正</strong>:所以&quot;UnitFlag 支持 K 分段累加、最后一次搬出&quot;<strong>基础 API 层是成立的、且是官方推荐用法</strong>。用户描述的&quot;以 16×K×16 为粒度,算完一段立即让 Fixpipe 搬出对应 16×16 到 GM、同时开始下一段 16×K×16 计算&quot;的掩盖方式,正是 UnitFlag 在块内流水 + K 分段场景下的正确用法,<strong>确实能有很好的掩盖</strong></p>
<p><strong>它与高阶 API 约束的边界</strong>:高阶 API 的 <code>mm.Iterate()</code> 一次调用内部会完成 <code>singleCoreK</code> 的全部 K 分片流水L0C 原地累加是 MMAD 固有语义,这一步不受约束影响);高阶约束禁的只是&quot;<strong>跨多次 <code>Iterate()</code></strong> 且最后才一次 <code>GetTensorC()</code>&quot;这种由用户在<strong>高阶层面</strong>组织的驻留累加。换言之:</p>
<ul>
<li><strong>底层 Mmad 级</strong><code>unitFlag=2/3</code> 可自由编排 K 分段累加 → 支持;</li>
<li><strong>高阶 Iterate 级</strong><code>Iterate</code> 内部 K 分片(一次 Iterate 完成)→ 支持;<code>Iterate</code> 多次 + 末次 <code>GetTensorC</code> → 不支持。</li>
</ul>
<p><strong>mat_mul_v3 是否踩高阶约束?——不踩(常规路径)</strong>。看 <code>mat_mul_asw_kernel.h:140-141</code></p>
<pre><code>mm_.Iterate();
mm_.GetTensorC(cGlobal_[...], enAtomic || kIndex != 0); // 每次 Iterate 紧跟一次 GetTensorC</code></pre>
<p>mat_mul_v3 的普通 ASWT 是 <strong>「一次 Iterate → 一次 GetTensorC」一一配对</strong>(即便 splitKRound 循环里也是成对出现,用 <code>enAtomic || kIndex!=0</code> 在 GM 侧原子加,而非 L0C 驻留累加)。它不是&quot;多次 Iterate、一次 GetTensorC&quot;因此常规路径不触碰高阶约束UnitFlag 可用。真正会触碰的是 <code>intraBlockPartSum</code>(两 AIV 结果在 L0C 累加)这类显式 L0C 驻留累加特性——StreamK 绕道 workspace + AIV 累加正是为了避开它。</p>
<h4>3.4.2 一次 Iterate 最大支持的 K 是多少?</h4>
<p><strong>先澄清概念</strong>:官方约束里没有任何一条把 &quot;一次 Iterate 的 K 上限&quot; 与 UnitFlag 挂钩。<strong>UnitFlag 约束的不是 K 的大小,而是&quot;累加由谁、在哪个层级组织&quot;</strong>(见上一条)。一次 Iterate 能算多大的 K<strong>L0C 之外的资源L1/L0A/L0B 容量、stepKa/stepKb 流水深度、baseK</strong> 决定,与 UnitFlag 无关:</p>
<ul>
<li>单次高阶 <code>Iterate()</code> 的 K 由 <code>singleCoreK</code> 决定,<code>singleCoreK</code> 在 L1 内按 <code>stepKa·baseK</code>A 侧)/ <code>stepKb·baseK</code>B 侧)分多片流水喂给 Cube<strong>每片的中间结果在同一 L0C 上原地累加</strong>MMAD 固有累加语义,属&quot;一次 Iterate 内部&quot;,不违反高阶约束)。</li>
<li>若单次 <code>Iterate()</code> 装不下整个 KK 超过 L1/L0 一次能流水的范围),高阶层会拆成多次 <code>Iterate()</code>;此时只要不打开&quot;L0C 驻留累加 + 末次才 GetTensorC&quot;就不违反约束mat_mul_v3 用 GM 原子加规避)。</li>
</ul>
<p><strong>结论</strong><strong>没有&quot;一次 Iterate 最大 K 受 UnitFlag 限制&quot;这回事</strong>。一次 Iterate 的 K 上限由 L1/L0 容量与 stepK 流水决定(例如 950PR 上 <code>maxStepK ≤ 8</code><code>baseK ≈ 128B/dtype</code>UnitFlag 只管&quot;算出的 512B 何时搬出&quot;,不管 K 多大。把这条约束理解成&quot;K 不能超过某值&quot;是误读——它限的是&quot;累加跨几次 Iterate 的组织方式&quot;,不是&quot;K 的绝对大小&quot;</p>
<h4>3.4.3 「不允许同时存在 CO1(L0C)→GM 与 A1(L1)→GM 两种搬出流水」——是什么意思L1 能直接写回 GM 吗?</h4>
<p><strong>官方原文出处</strong>同上两篇《Matmul 高阶 API使能/开启UnitFlag》约束条件第 2 条:</p>
</blockquote>
<p><strong>这条的字面意思</strong>UnitFlag 开启时,同一个算子里不能<strong>同时</strong>出现两类搬出流水——一类是 L0C→GM结果搬出走 Fixpipe另一类是 L1(A1)→GM从 L1 直接把数据写回 GM。即搬出通路的&quot;源头&quot;必须统一为 L0C不能 L0C 和 L1 混着当搬出源。</p>
<p><strong>「L1 能直接写回 GM 吗?」——分架构看,这正是关键点</strong></p>
<ul>
<li><strong>在 950PR351x 架构)上:不能。</strong> 950 白皮书明确351x 相对上代<strong>删除了 <code>L1→GM</code> 的回写通路</strong>(同时也删除了 <code>GM→L0A/L0B</code> 的直达通路),一切矩阵数据的搬出都必须经 <code>L0C→Fixpipe→GM</code>。所以<strong>在 950PR 上根本不存在&quot;L1→GM&quot;这条物理通路</strong>,这条约束对 950PR 而言<strong>自然满足、不会触发</strong>——它更像是从支持 L1→GM 的老架构(如部分 A2/A3 代际)沿用过来的兼容性约束。</li>
<li><strong>mat_mul_v3 在 950PR 上的搬出源</strong>:普通 ASWT 的结果一律走 <code>L0C→Fixpipe→GM(C)</code><code>mm_.GetTensorC(cGlobal...)</code>),没有任何 L1→GM 的直接搬出 → 符合该约束。</li>
</ul>
<p><strong>小结</strong>:这条约束的语义是&quot;搬出源必须唯一(只能是 L0C&quot;,防止 UnitFlag 的细粒度同步在&quot;两种不同源头的搬出&quot;上产生时序混乱。在 950PR 上由于 L1→GM 通路已被硬件删除,这条约束<strong>自动成立</strong>,不会成为限制 mat_mul_v3 开 UnitFlag 的因素。</p>
<h4>3.4.4 三条约束在 mat_mul_v3 / 950PR 上的实际生效性汇总</h4>
<table><thead><tr><th>官方约束</th><th>准确含义</th><th>950PR 上是否实质限制 mat_mul_v3</th></tr></thead><tbody>
<tr><td>仅 Norm/IBShare/MDL 模板</td><td>UnitFlag 只在三个高阶模板下有效</td><td>mat_mul_v3 用 MDLGetMDLConfig<strong>满足</strong></td></tr>
<tr><td>不能同时有 CO1→GM 与 A1→GM 两种搬出</td><td>搬出源必须唯一为 L0C</td><td>351x 已删 L1→GM 通路 → <strong>自动满足,无实质限制</strong></td></tr>
<tr><td>L0C 累加时不支持多次 Iterate 一次输出</td><td>高阶层禁止&quot;跨多次 Iterate 的 L0C 驻留累加 + 末次才搬出&quot;(底层 Mmad 级 unitFlag=2/3 仍支持 K 分段累加)</td><td>mat_mul_v3 常规路径是 Iterate/GetTensorC 一一配对 → <strong>不触碰UnitFlag 可用</strong>;仅显式 L0C 驻留累加(如 intraBlockPartSum才触碰</td></tr>
<tr><td>MTE2 Bound 已掩盖时收益小</td><td>Fixpipe 已被搬运流水掩盖时无需再开</td><td>这是<strong>收益</strong>问题不是<strong>正确性</strong>问题,不影响可用性</td></tr>
</tbody></table>
<p><strong>由此可得更精确的判断</strong>mat_mul_v3 在 950PR 上常规路径(含普通 ASWT、A/B 全载、以及把 K 分片放到 GM workspace 由 AIV 累加的 StreamK<strong>都满足 UnitFlag 的可用条件且默认已开启</strong>;高阶 API 真正排除的是&quot;<strong>多次 Iterate + 末次一次 GetTensorC</strong>&quot;的组织方式——而 mat_mul_v3 用&quot;GM 原子加 / workspace + AIV 累加&quot;规避了它。这支撑 §3.3 的结论:<strong>在 950PR 上 UnitFlag 的合法域覆盖了 mat_mul_v3 的全部主路径L0C DB 的补位空间被压缩得很小</strong></p>
<hr/>
<h3>3.5 两个层级的澄清:为什么&quot;高阶约束说不能 K 分段一次输出&quot;,而&quot;底层文档却举例 K 分段累加&quot;</h3>
<p>这是最容易混淆、也最关键的一点,单独澄清。</p>
<p><strong>两个层级的对象不同</strong></p>
<table><thead><tr><th>层级</th><th>编程接口</th><th>UnitFlag 的控制量</th><th>对 K 分段累加的支持</th></tr></thead><tbody>
<tr><td>基础 API指令级</td><td><code>Mmad</code> / <code>Copy</code>/<code>Fixpipe</code> 指令</td><td>指令参数 <code>unitFlag = 2/3</code>,逐 512B 块控标志位</td><td><strong>支持</strong>:前 n1 次 Mmad 设 2保持占用、续写累加末次 Mmad 设 3释放给 Fixpipe官方明确给出 K 分 8 次迭代的实例</td></tr>
<tr><td>高阶 API对象级</td><td><code>Matmul.Iterate()</code> / <code>GetTensorC()</code></td><td><code>MatmulConfig.enUnitFlag = true</code>,由库内部统一编排</td><td><strong>受限</strong><code>Iterate</code> 内部的 K 分片流水(一次 Iterate 完成)支持;但&quot;<strong>多次 Iterate + 末次一次 GetTensorC</strong>&quot;这种高阶层组织的驻留累加不支持</td></tr>
</tbody></table>
<p><strong>为什么高阶层反而受限</strong>:高阶 <code>Matmul</code> 对象把&quot;K 分片、L0C 驻留、何时搬出&quot;都封装在 <code>Iterate()/GetTensorC()</code> 内部UnitFlag 的标志位编排也由库统一管理。当用户用高阶接口做&quot;多次 Iterate 累加 + 一次 GetTensorC&quot;时,库无法在一次 <code>GetTensorC</code> 里反推出前面若干次 Iterate 各自该用什么 unitFlag 时序,于是干脆禁止这种组合。而<strong>基础 API 层把 unitFlag 的控制权直接交给用户</strong>,用户可以自己排&quot;前 n1 次设 2、末次设 3&quot;,所以能做更灵活的 K 分段累加流水。</p>
<p><strong>对用户设想的直接回答</strong>:你描述的&quot;以 16×K×16 为粒度,每段算完立即让 Fixpipe 搬出对应 16×16 到 GM、同时开始下一段计算&quot;——<strong>在基础 API 层完全成立,且就是官方推荐用法</strong>(前段 unitFlag=2 保持占用、末段 unitFlag=3 释放)。这种&quot;K 分段 + 边算边搬&quot;能同时做到:① 块内 512B 流水掩盖;② K 分段累加。它比单纯 L0C DB 更省 L0C 容量、掩盖更细。</p>
<p><strong>但要强调一个前提</strong>:这种灵活编排走的是<strong>基础 APIMmad/Copy + 手写 unitFlag=2/3</strong>,不是 mat_mul_v3 用的高阶 <code>Matmul</code> 对象。mat_mul_v3 用高阶 API 换来的是开发效率与正确性保证,代价就是受高阶约束(不能多次 Iterate 一次输出)。如果要做你说的这种极致 K 分段流水,需要下沉到基础 API 层手写 Mmad/Copy 序列——这正是 catlass类 CUTLASS 模板库)一层在做的事。</p>
<p><strong>&quot;UnitFlag 能否完全取代 L0C DB&quot;的最终修正</strong></p>
<ul>
<li><strong>mat_mul_v3高阶 API现状</strong>下:主路径默认开 UnitFlagK 一次 Iterate 完成Fixpipe 已被块内 512B 流水掩盖,<strong>L0C DB 价值很小</strong>(且 256×256 时本就开不了 DB。DB 仅在 base 块被迫做小256×128 等)时偶发有用。</li>
<li><strong>基础 API / catlass 层</strong>UnitFlag 用 unitFlag=2/3 可覆盖 K 分段累加场景,掩盖能力进一步逼近&quot;完全取代 DB&quot;</li>
<li><strong>仍不能完全取代的根本原因</strong>:不在&quot;K 能不能分段&quot;(底层能),而在于 <strong>UnitFlag 依赖 L0C 块被&quot;写满即搬&quot;,它服务的是&quot;计算结果要尽快流出 L0C&quot;的场景</strong>;而 L0C DB 的本质是&quot;<strong>用双倍 L0C 容量换时间上的重叠</strong>&quot;,它服务的是&quot;想让 base 块更大、减少重复读&quot;的场景。当 L0C 容量成为 tiling 瓶颈(想把 baseM·baseN 做大但放不下两份UnitFlag不占额内容量反而比 DB 更优——这一层上 UnitFlag 甚至优于 DB。只有当场景落到 UnitFlag 高阶约束之外(如必须用多次 Iterate 驻留累加DB 才成为唯一选择。</li>
</ul>
<p><strong>一句话总结</strong>:在 950PR + mat_mul_v3 的语境里,<strong>UnitFlag 已经基本取代 L0C DB 作为 Fixpipe 掩盖的主手段</strong>DB 退化为&quot;高阶 API 约束之外(多次 Iterate 驻留累加)&quot;&quot;L0C 装得下两份、想换更大 base 块&quot;这两类边角场景的兜底。</p>
<hr/>
<h2>4. 对本项目性能建模与最优方案的直接推论</h2>
<ol>
<li><strong>建模时 Fixpipe 项要分两种掩盖</strong>:在性能模型 <code>T_total = max(T_MMAD, T_MTE2, T_MTE1, T_FIXPIPE)</code> 中,<code>T_FIXPIPE</code> 的实际暴露量应写成</li>
</ol>
<ul>
<li>开 UnitFlag合法域内<code>T_FIXPIPE_exposed ≈ tail</code>512B 尾开销)→ 基本可以忽略Fixpipe 通常不构成独立瓶颈;</li>
<li>不开 UnitFlag或不可用<code>T_FIXPIPE_exposed = max(0, t_fp - t_mm)</code>DB 掩盖后的残余),且仅当 <code>baseM·baseN ≤ 32768</code> 时 DB 才生效,否则 <code>T_FIXPIPE_exposed = t_fp</code>(全暴露)。</li>
</ul>
<ol>
<li><strong>最优 tiling 的取舍被简化</strong>:既然 UnitFlag 默认开baseM/baseN 的搜索<strong>不必再为&quot;能否开 L0C DB&quot;让步</strong>,可以更激进地逼近 256×256重复读最少。原先&quot;为开 DB 而把 baseN 砍半&quot;的权衡在 UnitFlag 合法域内不成立。这是对 §3.2 <code>GetRebalanceBlock</code> 的一处实质修正点。</li>
<li><strong>真正需要单独建模的 K 累加形态</strong>:需要警惕的不是 mat_mul_v3 的主路径,而是&quot;<strong>在 L0C 里做跨 Iterate 驻留累加</strong>&quot;这一类形态(核内 split-K 驻留 / intraBlockPartSum 等)。这类形态被 UnitFlag 约束排除,只能靠 L0C DB 做块间掩盖。mat_mul_v3 的 StreamK 用 workspace + AIV 累加规避了它,因此主路径不受影响;但若未来引入 L0C 驻留累加的新模板,性能模型需为其单列 Fixpipe 掩盖方式(回到 DB 主导)。</li>
</ol>
<hr/>
<h2>附录:关键证据索引</h2>
<table><thead><tr><th>证据</th><th>位置</th></tr></thead><tbody>
<tr><td>高阶 API UnitFlag 约束(在线)</td><td>昇腾社区官网 → CANN → 算子实践参考 → 优秀实践 → Matmul 性能调优案例 → 《Matmul 高阶 API 开启 UnitFlag》社区版/《Matmul 高阶 API 使能 UnitFlag》商用版&quot;约束条件&quot;一节</td></tr>
<tr><td>高阶 API UnitFlag 约束(本地)</td><td><code>昇腾NPU知识库/CANN社区版9.2.0-beta.1/01_AscendC算子开发/292_..._开启UnitFlag.md</code>第15-21行<code>昇腾NPU知识库/CANN商用版9.0.0/01_AscendC算子开发/203_..._使能UnitFlag.md</code>第17/21/23行表述最完整</td></tr>
<tr><td><strong>基础 API UnitFlag 机制unitFlag=2/3、K 分段累加实例,在线)</strong></td><td>昇腾社区官网 → CANN → API 参考 → Ascend C API → SIMD API → 基础 API → 矩阵计算Tensor API→ Mmad 计算关键特性说明 → <strong>《UnitFlag》</strong>用户所给链接hiascend.com/.../Mmad计算关键特性说明/UnitFlag.md</td></tr>
<tr><td>基础 API UnitFlag 机制(本地缓存)</td><td><code>昇腾NPU/.tmp/unitflag_raw2.txt</code>(已抓取的页面正文,含 128×1024 八次迭代实例原文)</td></tr>
<tr><td>GetMDLConfig 签名第7参=enUnitFlag</td><td><code>昇腾NPU知识库/CANN商用版9.0.0/02_API参考/AscendC_API/631_..._GetMDLConfig.md</code></td></tr>
<tr><td>enUnitFlag 默认值与约束</td><td><code>昇腾NPU知识库/CANN商用版9.0.0/02_API参考/AscendC_API/626_..._MatmulConfig.md</code></td></tr>
<tr><td>351x 删除 L1→GM / GM→L0 直达通路</td><td><code>昇腾NPU知识库/00_硬件/昇腾950_NPU架构白皮书.txt</code>CV融合/数据通路章节);同仓 <code>昇腾950PR/昇腾950PR架构解读.md</code> §5.1</td></tr>
<tr><td>MM_CFG_NO_PRELOAD 显式开 enUnitFlag</td><td><code>ops-nn/matmul/mat_mul_v3/op_kernel/mat_mul_v3_common.h:72</code></td></tr>
<tr><td>普通 ASWT Iterate/GetTensorC 一一配对</td><td><code>ops-nn/matmul/mat_mul_v3/op_kernel/arch35/mat_mul_asw_kernel.h:140-141</code></td></tr>
<tr><td>StreamK 回调显式 unitFlag=3</td><td><code>ops-nn/matmul/mat_mul_v3/op_kernel/arch35/mat_mul_stream_k_kernel.h:36-37</code></td></tr>
<tr><td>StreamK K 分片走 workspace + AIV 累加</td><td><code>ops-nn/matmul/mat_mul_v3/op_kernel/arch35/mat_mul_stream_k_kernel.h:188-259</code><code>mat_mul_stream_k_block.h</code></td></tr>
<tr><td>dbL0C 判据 baseM·baseN≤32768</td><td><code>ops-nn/matmul/mat_mul_v3/op_host/op_tiling/arch35/matmul_v3_tiling_helper.cpp:490</code></td></tr>
</tbody></table></body></html>