Files

330 lines
32 KiB
HTML
Raw Permalink 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.0">
<title>ASW_Basic 分支理论最优实现分析</title>
<script>
MathJax = {
tex: {
inlineMath: [['$','$']],
displayMath: [['$$','$$']],
tags: 'ams',
processEscapes: true
}
};
</script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<style>
:root{--ink:#1f2933;--muted:#5f6b7a;--accent:#0b6bcb;--accent2:#0e9f6e;--line:#d9e2ec;--code-bg:#f4f6f9}
*{box-sizing:border-box}
body{font-family:"PingFang SC","Microsoft YaHei","Helvetica Neue",Arial,sans-serif;color:var(--ink);background:#eef2f6;margin:0;line-height:1.8}
.page{max-width:1020px;margin:0 auto;padding:32px 44px 80px;background:#fff;box-shadow:0 0 24px rgba(0,0,0,.06)}
h1{font-size:28px;border-bottom:3px solid var(--accent);padding-bottom:12px;margin-top:8px}
h2{font-size:22px;margin-top:44px;border-left:6px solid var(--accent);padding-left:12px;color:#0b3d73}
h3{font-size:18px;margin-top:30px;color:#0b3d73;border-bottom:1px dashed var(--line);padding-bottom:6px}
h4{font-size:16px;margin-top:20px;color:#123}
table{border-collapse:collapse;width:100%;margin:14px 0;font-size:14px}
th,td{border:1px solid var(--line);padding:7px 10px;text-align:left;vertical-align:top}
th{background:#eaf2fb;color:#0b3d73}
tr:nth-child(even) td{background:#f8fafc}
code,pre{font-family:"JetBrains Mono",Consolas,Menlo,monospace;font-size:13px}
code{background:var(--code-bg);padding:1px 5px;border-radius:4px;color:#9d2c5e}
pre{background:var(--code-bg);border:1px solid var(--line);border-radius:8px;padding:14px;overflow-x:auto;line-height:1.55}
pre code{background:none;color:#243447;padding:0}
blockquote{background:#eafaf3;border-left:5px solid var(--accent2);padding:10px 16px;border-radius:0 8px 8px 0;margin:14px 0}
.math{background:#fafbfc;border:1.5px solid #c3d6ee;border-radius:10px;padding:10px 22px;margin:14px 0;overflow-x:auto}
ul.tight li,ol.tight li{margin:3px 0}
hr{border:none;border-top:1px solid var(--line);margin:24px 0}
</style>
</head>
<body>
<div class="page">
<h1>ASW_Basic 分支BMM 兜底分支的理论最优实现分析</h1>
<blockquote>目标芯片:昇腾 950PRDAV_3510。本文从《BMM 算子优化分析 v0.98》第八章抽出独立成篇,自包含完整推导链。</blockquote>
<h2>摘要</h2>
<p>ASW_Basic 是 BMM 的兜底分支——核间切 M/N或混合切不做 batch 合并或 K 维切分。本文给出完整的时延建模、核间分配策略分析(证明 B 优先分组在任何场景下都不优于线性映射)、实现方案的逐步推导,以及尾轮处理策略。</p>
<hr>
<h2>一、问题定义与执行模型</h2>
<h3>1.1 分支定位</h3>
<p>BMM 中,当 B ≥ Cbatch 数 ≥ AIC 核数)时核间切 B 是免费的(每核独立处理若干 batch无共享无依赖由 IterBatch/MergeBatch 承接。当 B &lt; C 或 B ≥ C 但 IterBatch/MergeBatch 条件不满足时,需要切 M/N 来填满所有核——这就是 ASW_Basic。</p>
<p>ASW_Basic 是实践中最常命中的分支B 可大可小可等 1交叉广播也由此承接对广播侧做 L1/L2 驻留,共享关系与切 M/N 同构)。</p>
<h3>1.2 执行模型</h3>
<pre><code>核间B × mCnt × nCnt 个输出块,按 B→M→N 线性映射分配到 C 核
核内:每核处理若干 [singleCoreM, singleCoreN] 输出块
└── 每块内部GM→L1→L0→Cube→L0C→Fixpipe 标准流水
└── K 维不切singleCoreK = K按 kL1 分块搬入 L1</code></pre>
<p>数据流:</p>
<pre><code>GM ──MTE2──&gt; L1 ──MTE1──&gt; L0A/L0B ──MMAD──&gt; L0C ──Fixpipe──&gt; GM
↑_____________ L2 Cache读 5.2TB/s_____________↑</code></pre>
<h3>1.3 符号定义</h3>
<table><tr><th>符号</th><th>含义</th><th>表达式/取值</th></tr>
<tr><td>$B$</td><td>batch 数</td><td>输入参数</td></tr>
<tr><td>$M, N, K$</td><td>矩阵维度</td><td>输入参数</td></tr>
<tr><td>$C$</td><td>AIC 核数</td><td>32</td></tr>
<tr><td>$dtype$</td><td>输入元素字节数</td><td>BF16 → 2B</td></tr>
<tr><td>$outB$</td><td>输出元素字节数</td><td>BF16 → 2B</td></tr>
<tr><td>$L0C$</td><td>L0C 容量/核</td><td>256KB</td></tr>
<tr><td>$L0A, L0B$</td><td>L0A/L0B 容量/核</td><td>各 64KB</td></tr>
<tr><td>$L1$</td><td>L1 容量/核</td><td>512KB</td></tr>
<tr><td>$L2$</td><td>L2 容量(共享)</td><td>128MB</td></tr>
<tr><td>$BW_{pc}$</td><td>单核 GM 带宽份额</td><td>$W_{GM}/C = 50$ GB/s</td></tr>
<tr><td>$Q_{16}$</td><td>单核 Cube BF16 峰值算力</td><td>486/32 ≈ 15.2 TFLOPS</td></tr>
<tr><td>$W_{GM}$</td><td>GM 带宽</td><td>1.6 TB/s</td></tr>
<tr><td>$BW_{L2}$</td><td>L2 读带宽</td><td>5.2 TB/s</td></tr></table>
<p>*Tiling 参数*</p>
<table><tr><th>符号</th><th>含义</th><th>约束层级</th></tr>
<tr><td>$\text{BaseM}, \text{BaseN}$</td><td>L0 级 tile 的 M/N 维度</td><td>L0C 容量直接约束</td></tr>
<tr><td>$baseK$</td><td>L0 级 tile 的 K 维度</td><td>L0A/L0B 容量约束</td></tr>
<tr><td>$\text{singleCoreM}, \text{singleCoreN}$</td><td>每核输出 tile 的 M/N 维度</td><td>L1 容量 + 并行度 + 搬移效率</td></tr>
<tr><td>$k_{L1}$</td><td>GM→L1 的 K 向粒度</td><td>dValue ≥ 256B</td></tr>
<tr><td>$mCnt, nCnt$</td><td>单 batch 内 M/N 向块数</td><td>$\lceil M/\text{singleCoreM} \rceil$</td></tr>
<tr><td>$W$</td><td>swizzle 窗口宽度</td><td>$\max\{d : d \mid C, d \le \lfloor\sqrt{C}\rfloor\}$</td></tr></table>
<p>层次关系:$K = \text{singleCoreK} \ge k_{L1} \ge baseK$$\text{singleCoreM} \ge \text{BaseM}$$\text{singleCoreN} \ge \text{BaseN}$。</p>
<hr>
<h2>二、进入条件</h2>
<p>同时满足:</p>
<ol class="tight">
<li>$P = \dfrac{B \cdot MN \cdot 4\text{B}}{L0C} \ge C$(并行度补齐)</li>
<li>无 batch 结构限制BatchA=BatchB、交叉广播均可典型进入路径$B &lt; C$(切 B 买不满核),或 $B \ge C$ 但 IterBatch/MergeBatch 条件不满足时的兜底</li>
<li><b>降核模式</b>$P &lt; C$ 且不满足 StreamK 进入条件 → 只用 $\lceil P \rceil$ 个核,其余核闲置</li>
</ol>
<p>逐条解释:</p>
<ol class="tight">
<li><b>并行度补齐</b>:以 L0C 满载为基本块粒度B×M×N 能切出至少 C 个独立输出块,则切 M/N或混合切并行度够用。切 M/N 的固有代价是共享矩阵被多核重复读,但共享部分驻留 128MB L2 时重复读以 5.2TB/s 命中 L2 而非 1.6TB/s 的 GM代价大部分被吸收。</li>
<li><b>兜底性质</b>ASW_Basic 是实践中最常命中的分支——B 可大可小可等 1交叉广播也由此承接对广播侧做 L1/L2 驻留,共享关系与切 M/N 同构)。</li>
<li><b>降核模式</b>P &lt; C 且 K 也不够格走 StreamK 时,并行度凑不满核。此时与其强行把 M/N 切得更碎tile 跌破 min_TileSize、dValue 跌破 128B搬移效率崩塌反而更慢不如<b>只用 ⌈P⌉ 个核</b>、每核承担一个完整输出块L0C 满载粒度),其余核闲置。这类 case 的时延绝对值小,继续切分引入的调度与搬移效率损失大于并行收益——降核是理性选择而非偷懒。</li>
</ol>
<hr>
<h2>三、时延建模</h2>
<h3>3.1 端到端时延</h3>
<div class="math">$$
T = \max(T_{MTE2},\; T_{MMAD},\; T_{FIX}) + T_{drain}
$$</div>
<table><tr><th></th><th>含义</th><th>公式</th></tr>
<tr><td>$T_{MTE2}$</td><td>GM→L1 搬移时延</td><td>$r_{in} \cdot S_{in} / (C \cdot BW_{pc})$</td></tr>
<tr><td>$T_{MMAD}$</td><td>Cube 计算时延</td><td>$2BMNK / (C \cdot Q_{16})$</td></tr>
<tr><td>$T_{FIX}$</td><td>输出写回时延</td><td>$B \cdot MN \cdot outB / (C \cdot W_{pc})$</td></tr>
<tr><td>$T_{drain}$</td><td>末块排空时延</td><td>$O(T_{comp} + T_{write})$</td></tr></table>
<p>其中 $S_{in} = B(MK + KN) \cdot dtype$ 为输入总量,$r_{in} \ge 1$ 为重复读倍率GM 输入流量 / 输入总量)。$r_{in} = 1$ 表示每字节只从 GM 读一次(后续复用全命中 L2——<b>这是 GM 输入流量的下界</b></p>
<p><b>关键观察</b>$T_{MMAD}$ 和 $T_{FIX}$ 与核间分配策略无关——**分配策略只影响 $T_{MTE2}$**(通过 L2 命中率影响 $r_{in}$)。优化目标:让 $r_{in}$ 尽量接近 1。</p>
<h3>3.2 并行度约束的正确形式</h3>
<p>总输出块数 $= B \cdot mCnt \cdot nCnt$。并行度约束的正确形式是:</p>
<div class="math">$$
B \cdot mCnt \cdot nCnt \ge C
$$</div>
<p>即总块数至少能填满 C 核。这与 $mCnt \cdot nCnt \ge \lceil C/B \rceil$ 等价(两边同乘 B 后取整)。</p>
<p>**不应要求 $B \cdot mCnt \cdot nCnt \;\%\; C = 0$**(整除)。整除是充分不必要条件——不整除时产生尾轮,尾轮的处理见 §五。</p>
<hr>
<h2>四、核间分配策略分析</h2>
<h3>4.1 两种候选策略</h3>
<ul class="tight">
<li><b>B 优先分组</b>C 核分为 B 组,每组 $C_g = \lfloor C/B \rfloor$ 或 $\lceil C/B \rceil$ 核独立处理一个 batch 的 $mCnt \times nCnt$ 个块,组内做 swizzle</li>
<li><b>线性映射</b>:块按 (b, m, n) 字典序编号block 0 = (0,0,0)block 1 = (0,0,1)block $mCnt \cdot nCnt$ = (1,0,0),…),核 i 依次处理块 i, i+C, i+2C, …</li>
</ul>
<h3>4.2 L2 工作集对比</h3>
<p>每波活跃核所需的 A 行带 + B 列带:</p>
<div class="math">$$
WS_{wave} = \big(W \cdot sM + \tfrac{C}{W} \cdot sN\big) \cdot K \cdot dtype
$$</div>
<p>其中 $sM$ = singleCoreM$sN$ = singleCoreN$W$ 为 swizzle 窗口宽度。</p>
<table><tr><th>维度</th><th>B 优先分组</th><th>B→M→N 线性映射</th></tr>
<tr><td>每波活跃 batch 数</td><td>B 个(每组一个)</td><td>1 个($mCnt \cdot nCnt \ge C$ 时)</td></tr>
<tr><td>每组/每波 swizzle 窗口</td><td>$W_g = \max\{d \mid d \mid C_g,\; d \le \sqrt{C_g}\}$</td><td>$W = \max\{d \mid d \mid C,\; d \le \sqrt{C}\}$</td></tr>
<tr><td>每波 L2 工作集</td><td>$B \cdot (W_g \cdot sM + \frac{C_g}{W_g} \cdot sN) \cdot K \cdot dtype$</td><td>$(W \cdot sM + \frac{C}{W} \cdot sN) \cdot K \cdot dtype$</td></tr>
<tr><td>C 不整除 B</td><td>组间核数不等 → 负载不均</td><td>无影响(总块数任意)</td></tr></table>
<p><b>数值例</b>C=32、B=4、sM=sN=256、K=1024、BF16分组 $C_g=8$、$W_g=2$8 的最大因子 ≤ √8≈2.83),总工作集 = $4 \times (2 \times 256 + 4 \times 256) \times 1024 \times 2 = 12$ MB线性 $W=4$,工作集 = $(4 \times 256 + 8 \times 256) \times 1024 \times 2 = 6$ MB——<b>线性映射的工作集是分组方案的一半</b></p>
<p><b>为什么线性映射工作集更小</b></p>
<ol class="tight">
<li><b>更多核参与同一 batch 的 swizzle</b> → $W = \sqrt{C} &gt; W_g = \sqrt{C/B}$ → 工作集更接近"方形"下限(均值不等式:$W + C/W$ 在 $W = \sqrt{C}$ 处最小);</li>
<li><b>同一时刻只有 1 个 batch 的数据活跃</b> → L2 只需缓存 1 份 A/B 数据,而非 B 份。</li>
</ol>
<h3>4.3 分场景严格比较</h3>
<p>**场景 I$mCnt \cdot nCnt \ge C$ 且单波工作集 ≤ L2**(最常见)</p>
<p>线性映射:每波 C 个块在同一 batch 内L2 命中率高。B 分组B 个 batch 同时活跃,总工作集 $B$ 倍。<b>线性严格更优。</b></p>
<p>**场景 II$S_{in} \le L2$(全部输入放得下 L2**</p>
<p>两种策略的 GM 流量相同($r_{in} = 1$,每字节只读一次)。但 B 分组在 C % B ≠ 0 时负载不均(组间核数不等)。<b>线性不劣于分组。</b></p>
<p>**场景 III$mCnt \cdot nCnt &lt; C$(单 batch 块数不够填满所有核)**</p>
<p>线性映射:每波横跨 $\lceil C/(mCnt \cdot nCnt) \rceil$ 个 batch——这些 batch 的数据同时活跃。B 分组:每组 $C_g$ 核但只有 $mCnt \cdot nCnt &lt; C_g$ 个块 → <b>组内有空闲核</b>。线性映射至少所有核都有活干。<b>线性严格更优。</b></p>
<p>**场景 IV$mCnt \cdot nCnt &lt; C/B$(每组连自己的核都填不满)**</p>
<p>B 分组每组空闲 $C_g - mCnt \cdot nCnt$ 个核,总算力浪费 $C - B \cdot mCnt \cdot nCnt$ 个核。线性映射无此问题。<b>线性严格更优。</b></p>
<h3>4.4 结论</h3>
<p><b>B 优先分组在任何场景下都不优于线性映射。</b> 根本原因:</p>
<ol class="tight">
<li>L2 是共享 Cache工作集越小命中率越高——线性映射同一时刻只激活 1 个 batch 的数据,工作集最小;</li>
<li>swizzle 窗口 $W \propto \sqrt{\text{参与核数}}$——更多核参与同一 batch 的 swizzle窗口更大工作集更方形</li>
<li>线性映射对 B 与 C 的关系无要求B 不整除 C 时无负载不均。</li>
</ol>
<p>因此 ASW_Basic 采用 <b>B→M→N 线性映射</b>作为核间分配策略。</p>
<hr>
<h2>五、尾轮处理</h2>
<h3>5.1 尾轮的定义</h3>
<p>当 $B \cdot mCnt \cdot nCnt \;\%\; C \neq 0$ 时,总块数不能被 C 整除,最后一波(尾轮)不满载——只有 $B \cdot mCnt \cdot nCnt \;\%\; C$ 个核有活干,其余核空闲。</p>
<h3>5.2 尾轮是否重新切分?</h3>
<p><b>不重新切分。</b> 尾轮使用与主轮相同的 SingleCoreM/N、BaseM/N——理由</p>
<ol class="tight">
<li><b>重新切分的收益有限</b>:尾轮只有一波,时延 = 单块时延。即使重新切分把 C 核都用满,单块时延也不会缩短(计算量/核数不变,只是每核分到的块更小,但启动开销不变)</li>
<li><b>重新切分的代价真实</b>:需要额外的 tiling 参数(第二套 SingleCoreM/N/BaseM/N、kernel 分支逻辑、L1 buffer 重新分配——工程复杂度高</li>
<li><b>尾轮占比小</b>:总波数 $= \lceil B \cdot mCnt \cdot nCnt / C \rceil$,尾轮只占 1/总波数。总波数越大,尾轮影响越小</li>
</ol>
<p><b>源码实现</b><code>MatMulV3TilingHelper::GetRebalanceBlock</code>):使用 balanceRate ≥ 0.9 评估负载均衡尾块感知评分GetBalanceRateWithTail。尾轮不重新切分仅通过选择使尾波占比尽量小的 mCnt/nCnt 组合来优化。</p>
<h3>5.3 尾轮影响的量化</h3>
<p>设总块数 $N_{blk} = B \cdot mCnt \cdot nCnt$,总波数 $n_{wave} = \lceil N_{blk} / C \rceil$,尾波块数 $r = N_{blk} \bmod C$$r = 0$ 时无尾波)。</p>
<p>尾波导致的额外时延(相对于完美整除):</p>
<div class="math">$$
\Delta T_{tail} = \begin{cases} 0 & r = 0 \\ T_{block} \cdot \big(1 - \frac{r}{C}\big) & r > 0 \end{cases}
$$</div>
<p>其中 $T_{block}$ 为单块时延。相对于总时延的比例:</p>
<div class="math">$$
\frac{\Delta T_{tail}}{T_{total}} \approx \frac{1 - r/C}{n_{wave}}
$$</div>
<p>总波数 $n_{wave}$ 越大,尾波影响越小。当 $n_{wave} \ge 4$ 时,尾波影响 &lt; 25%。</p>
<hr>
<h2>六、实现方案</h2>
<h3>Step 0BaseM / BaseN 的确定L0 级 tile先把 L0C 用满)</h3>
<p>L0C 是 Cube 的累加器BaseM × BaseN 是每次 Cube 计算的输出 tile。BaseM/N 应<b>尽量把 L0C 用满</b>——L0C 利用率越高,每次 Cube 计算的输出越大,单位计算的启动/排空开销摊得越薄:</p>
<div class="math">$$
\text{BaseM} \times \text{BaseN} = \frac{L0C}{2 \times 4\text{B}} = 32768 \text{ 元素}
$$</div>
<p>双缓冲两份FP32 4B/元素。BaseM/BaseN 的长宽比跟随 SingleCoreM/SingleCoreN进而跟随 M/N对齐 16 的倍数。baseK 由 L0A/L0B 容量决定L1→L0 搬移无 dValue 要求dValue 约束的是 GM→L1 的 $k_{L1}$</p>
<div class="math">$$
baseK = \min\Big(\frac{L0A}{2 \cdot \text{BaseM} \cdot \text{dtype}},\; \frac{L0B}{2 \cdot \text{BaseN} \cdot \text{dtype}}\Big) \text{ 向下 16 对齐}
$$</div>
<p>核间不切 K 时 K 维度层次关系:$K = \text{singleCoreK} \ge k_{L1} \ge baseK$——$k_{L1}$ 是 GM→L1 的 K 向粒度(须 $k_{L1} \cdot \text{dtype} \ge 256\text{B}$baseK 是 L1→L0 的 K 向粒度(仅受 L0A/L0B 容量约束)。</p>
<h3>Step 1SingleCoreM / SingleCoreN 的确定(每核输出 tile≥ BaseM/N</h3>
<p>SingleCoreM × SingleCoreN 是每核每次处理的输出区域,<b>不受 L0 容量直接约束</b>——一个 [SingleCoreM, SingleCoreN] tile 内部由若干 [BaseM, BaseN] L0 tile 组成($\text{SingleCoreM} \ge \text{BaseM}$$\text{SingleCoreN} \ge \text{BaseN}$。SingleCoreM/N 的核心影响是 <b>GM→L1 搬移效率和 L2 重复读率</b></p>
<ul class="tight">
<li>SingleCoreM/N 越大 → 单次 GM→L1 搬移量越大dValue 越有保障L2 中同一份 A 行带/B 列带被更多核复用</li>
<li>SingleCoreM/N 越小 → 总块数 mCnt×nCnt 越多,并行度越高,但搬移效率降低</li>
</ul>
<p>*约束链*</p>
<p><b>约束 1——并行度下限</b>:总块数须填满 C 核。</p>
<div class="math">$$
B \cdot mCnt \cdot nCnt \ge C \iff mCnt \cdot nCnt \ge \Big\lceil \frac{C}{B} \Big\rceil
$$</div>
<p><b>约束 2——L1 容量</b>(双缓冲下驻留当前 tile 的输入):</p>
<div class="math">$$
2 \cdot (\text{singleCoreM} + \text{singleCoreN}) \cdot k_{L1} \cdot \text{dtype} \le L1,\qquad k_{L1} \cdot \text{dtype} \ge 256\text{B}
$$</div>
<p><b>约束 3——搬移效率</b></p>
<div class="math">$$
\text{singleCoreM} \cdot k_{L1} \cdot \text{dtype} \ge min\_TileSize,\qquad k_{L1} \cdot \text{singleCoreN} \cdot \text{dtype} \ge min\_TileSize
$$</div>
<p><b>约束 4——SingleCoreM/N 是 BaseM/N 的整数倍</b>(工程实现要求,保证 L0 tile 边界对齐)。</p>
<p>*选取策略*:在满足约束 1 的前提下SingleCoreM/N 尽量大(搬移效率和 L2 复用最大化)。长宽比跟随 M/N$\text{singleCoreM}/\text{singleCoreN} \approx M/N$),对齐到 BaseM/BaseN 的整数倍。</p>
<p>*例*B=8、M=N=2048、K=1024、BF16$\lceil C/B \rceil = 4$,取 $mCnt = nCnt = 2$ → singleCoreM = singleCoreN = 1024。BaseM = BaseN = $\lfloor\sqrt{32768}\rfloor_{16} = 176$。SingleCoreM/BaseM = 1024/176 ≈ 5.8 → 取 5整数倍→ singleCoreM = 880。约束 2$k_{L1} \le 512\text{KB}/(2 \times 1760 \times 2\text{B}) = 74$ → 取 6416 对齐)= 128B ✓。</p>
<h3>Step 2mCnt / nCnt 与核间分配</h3>
<div class="math">$$
mCnt = \Big\lceil \frac{M}{\text{singleCoreM}} \Big\rceil,\qquad nCnt = \Big\lceil \frac{N}{\text{singleCoreN}} \Big\rceil
$$</div>
<p>总输出块数 = $B \times mCnt \times nCnt$。核间分配采用 B→M→N 线性映射(分析见 §四):块按 (b, m, n) 字典序编号,核 i 处理块 i, i+C, i+2C, …。B 不整除 C 时尾波不满载的核少分一块,无需 B | C。</p>
<h3>Step 3核间切分维度选择按共享代价从低到高</h3>
<p>切 B零共享先试→ 切 M右矩阵 $KN\cdot\text{dtype} \le L2$ 则驻留 L2→ 切 N对称→ 混合切(靠 swizzle + L2 切分管理)→ 降核(见 Step 8</p>
<h3>Step 4swizzle——ASW 滑窗蛇形</h3>
<p><b>问题</b>:核间切 M/N 后,同一时刻 C 个核各算一个输出块,它们所需的 A 行块与 B 列块集合就是当前"活跃工作集"。若按行优先顺序朴素分配,一波 C 个块横跨的 A 行、B 列很宽,活跃工作集超过 L2 就回 GM 读1.6TB/s重复读代价真实发生。<b>swizzle 要做的就是编排输出块的执行顺序,把每一波核的活跃工作集压到最小。</b></p>
<p><b>做法</b>:把 M 向每 W 个基本块划为一个"窗口",遍历顺序为"窗口内先扫 M、扫满 W 行再进下一列 N一个窗口扫完再进下一个窗口",且奇数窗口行 N 向反向(蛇形)。效果有二:</p>
<ul class="tight">
<li>同一波 C 个核的块集中在同一个窗口内 ⇒ 活跃 A 行块只有 W 个、活跃 B 列块只有 C/W 条带;</li>
<li>蛇形反向使相邻窗口行首尾相接——上一窗口末尾的 B 列带与下一窗口开头的 B 列带是同一条,跨窗口切换时工作集增量最小。</li>
</ul>
<p><b>W 怎么取</b>:一波 C 个块的 L2 足迹约为</p>
<div class="math">$$
footprint \approx \big(W \cdot M^t K + \tfrac{C}{W} \cdot K N^t\big) \cdot \text{dtype}
$$</div>
<p>由均值不等式,$W + C/W$ 在 $W = \sqrt{C}$ 处取最小——窗口越接近"方形"W 行 × C/W 列),足迹越小。同时 W 须整除 C保证每个窗口恰好被整数波核覆盖、窗口边界不把波次切碎。合起来即</p>
<div class="math">$$
W = \max\{\,d \mid d \mid C,\; d \le \lfloor\sqrt{C}\rfloor\,\}
$$</div>
<p>C=32 时 $\sqrt{32} \approx 5.66$,因子 {1,2,4,8,…} 中不超过它的最大者是 4故 W=4。</p>
<p><b>实例</b>C=32W=4M̃=8Ñ=8数字为块的全局执行顺序一波 32 块):</p>
<pre><code>窗口0N 正向) 窗口1N 蛇形反向)
ν0 ν1 ν2 … ν7 ν0 ν1 … ν7
μ0 0 4 8 … 28 μ4 60 56 … 32
μ1 1 5 9 … 29 μ5 61 57 … 33
μ2 2 6 10 … 30 μ6 62 58 … 34
μ3 3 7 11 … 31 μ7 63 59 … 35</code></pre>
<p>块 31 = (μ3, ν7),块 32 = (μ4, ν7)——相邻两个块共用同一条 B 列带ν7窗口切换几乎零增量。对比朴素行优先Ñ=16 时):一波横跨 2 个 A 行块 + 16 条 B 列带,足迹 (2·M^t + 16·N^t)·K·dtype滑窗为 (4·M^t + 8·N^t)·K·dtype——M^t≈N^t 时足迹缩小 1/3。</p>
<p><b>窗内为什么不蛇形</b>(对上例中"块 3=(μ3,ν0) → 块 4=(μ0,ν1) 而非 (μ3,ν1)"的说明):蛇形的收益来自"相邻遍历段共享边界数据",要分两种边界看:</p>
<ul class="tight">
<li><b>窗内列间边界</b>ν0→ν1相邻两段共享的是同一组 A 行块W 个),它们在整个窗口期间<b>全程驻留 L2</b>,无论按什么顺序扫,工作集不变——窗内蛇形零收益;</li>
<li><b>窗口行边界</b>窗口0→窗口1A 行整体换血μ0..3 → μ4..7),此时 B 列带的连续性决定换血成本——不蛇形则下一窗口从 ν0 开始LRU 上最久未用、早已被挤出 L2 的冷带),蛇形则延续上一窗口末尾的 ν7最热线带<b>蛇形只标在窗口行号上</b>(源码 <code>BatchMatMulAswBlock::UpdateBasicIndex</code>:仅 <code>rowIdx</code> 为奇时 n 反向,窗内 m 最快序不反向),正是这个收益结构的直接实现。</li>
</ul>
<h3>Step 5L2 分组(工作集超 L2 时)</h3>
<p><b>切的是什么</b>:将 mCnt×nCnt 个基本块划分为若干<b>执行组</b>——每组覆盖输出平面上一个连续矩形区域(若干 singleCoreM × singleCoreN 基本块的集合),使该组所需的 A 行带 + B 列带输入工作集 ≤ L2 可用读入空间;组内所有基本块算完再进下一组,输入只在跨组时换一次。</p>
<p><b>为什么需要它</b>:滑窗压缩的只是"同一波"的足迹;若整个工作集超 128MB L2跨波次复用落空——上一波窗口的 A 行早被挤出,下一波又得回 GM 读。且 L2 是<b>读写共用</b>的:输出经 fixpipe 写出时若驻留 L2dirty会压缩读入可用空间若直写 GM则占用与读共享的 1.6TB/s 总线。所以 L2 切分必须与写出策略联合决策。记输入总量 $S_{in} = B(MK+KN)\cdot\text{dtype}$,输出总量 $S_{out} = B \cdot MN \cdot outB$。</p>
<p><b>两个不变量</b>(一切分析的起点):</p>
<ul class="tight">
<li>GM 流量下界 $= S_{in} + S_{out}$:输入至少读一遍、输出最终至少要写一遍到 GM与 L2 策略无关;</li>
<li>L2 读入可用空间:$L2_{read} = L2 - S_{out}^{resident}$$S_{out}^{resident}$ 为驻留 L2 的输出量)——写出驻留 L2 会压缩读入空间,这是写出策略影响读入复用的通道。</li>
</ul>
<p><b>重复读倍率</b>$r_{in}$ = GM 输入流量 / $S_{in}$。$r_{in} = 1$ 表示每个输入数据从 GM 只读一遍(后续复用全在 L2 命中)——这是 GM 输入流量的下界L2 管理的全部目标就是让 $r_{in}$ 尽量接近 1。</p>
<p><b>先判定写出会不会 Bound</b>。平均写出带宽需求:</p>
<div class="math">$$
BW_{out} = \frac{S_{out}}{T_{MMAD}} = \frac{B \cdot MN \cdot outB}{2BMNK\,/\,(C \cdot Q_{16})} = \frac{C \cdot Q_{16} \cdot outB}{2K}
$$</div>
<p>只与 K、outB 有关K 越小单位时间输出越密。例BF16 输出C·Q₁₆=432 TFLOPSK=512 → 844 GB/sK=256 → 1.69 TB/s已超 GM 总线——此时<b>任何策略都写出 Bound</b>L2 缓冲只能削峰fixpipe 以 5.2TB/s 写 L2 吸收突发),平均速率仍受总线限制,应预期 Fixpipe 成为 $T_{total}$ 的 max 项。</p>
<p><b>分场景决策</b></p>
<p>**场景 A$S_{in} + S_{out} \le L2$(全驻留)**。输入读一遍($r_{in}=1$),输出驻留 L2dirty异步回写 GM——写出走 5.2TB/s L2 写口,不与读争,也削平了 GM 写突发。无需切分。</p>
<p>**场景 B$S_{in} \le L2$ 但 $S_{in} + S_{out} &gt; L2$(输入能驻留,加上输出超了)<b>。策略:</b>输入驻留、输出直写 GM**。理由链:</p>
<ol class="tight">
<li>$S_{in} \le L2$ ⇒ 全部输入可驻留 L2跨波次复用全部命中 ⇒ $r_{in} = 1$GM 输入流量达到下界 $S_{in}$</li>
<li>输出在本算子内只写不读、零复用收益;若输出也驻留 L2dirty超出 L2 的部分会把输入挤出——被挤出的输入后续得回 GM 重读 ⇒ $r_{in} &gt; 1$GM 流量超出下界;</li>
<li>故让输出直写 GMfixpipe L0C→GM不占 L2把 128MB 全部留给输入,保住 $r_{in} = 1$——GM 总流量保持下界 $S_{in} + S_{out}$</li>
<li>代价是输出即刻占用 GM 写带宽(与读共享总线),须校验总线不爆:$(S_{in} + S_{out})/T_{MMAD} \le W_{GM}$。</li>
</ol>
<p>B=8、M=N=4096、K=512、BF16——$S_{in}$≈67MB ≤ L2$S_{out}$≈268MB 直写 GMT_MMAD≈318µs总流量速率 (67+268)MB/318µs ≈ 1.05TB/s &lt; 1.6TB/s ✓。</p>
<p>**场景 C$S_{in} &gt; L2$(输入本身超)<b>。需要分组执行——把 mCnt×nCnt 个基本块划分为若干</b>执行组**,每组内所有基本块的输入工作集不超过 L2 可用空间。输出直写 GM不占 L2 读入空间)。</p>
<p><b>L2 的软件可控手段</b>L2 是 Cache 而非 Buffer软件无法精确控制"哪些数据在 L2 里"。可用的控制手段:</p>
<ul class="tight">
<li><b>Cache Hint</b><code>SetL2CacheHint</code>):标记输入为 allocate读入 L2或 non-allocate直读 GM 不过 L2标记输出为 non-allocate直写 GM 不占 L2</li>
<li><b>CMO</b>Cache Maintenance OperationPrefetch/Writeback/Invalidate在关键节点主动管理 L2 内容;</li>
<li><b>执行顺序</b>swizzle通过编排基本块的执行顺序控制同一时刻的活跃工作集——<b>这是最主要的 L2 管理手段</b></li>
</ul>
<p><b>执行组的划分</b></p>
<p>*问题*mCnt×nCnt 个基本块(每块输出 singleCoreM×singleCoreN按什么粒度分组使每组的输入工作集 ≤ L2</p>
<p>*每组输入工作集*:一组覆盖 M 向 $m_{grp}$ 个基本块、N 向 $n_{grp}$ 个基本块,即覆盖输出区域 $[m_{grp} \cdot \text{singleCoreM},\; n_{grp} \cdot \text{singleCoreN}]$。该区域需要读入的输入:</p>
<div class="math">$$
WS_{grp} = B \cdot K \cdot \big(m_{grp} \cdot \text{singleCoreM} + n_{grp} \cdot \text{singleCoreN}\big) \cdot \text{dtype} \;\le\; L2
$$</div>
<p>*目标*:最小化组数(组数越少,输入从 GM 的重复读次数越少)。每行 A 被 $n_{grp}$ 个组各读一次,每列 B 被 $m_{grp}$ 个组各读一次:</p>
<div class="math">$$
r_{in} = \frac{n_{grp} \cdot M + m_{grp} \cdot N}{M + N}
$$</div>
<p>*求解*:约束 $m_{grp} \cdot \text{singleCoreM} + n_{grp} \cdot \text{singleCoreN} \le D$(其中 $D = L2/(B \cdot K \cdot \text{dtype})$),最小化 $n_{grp} \cdot M + m_{grp} \cdot N$。最优在组内 M/N 向基本块数与输出平面形状成正比时取到:</p>
<div class="math">$$
m_{grp} = \Big\lfloor \frac{D}{2 \cdot \text{singleCoreM}} \Big\rfloor,\qquad n_{grp} = \Big\lfloor \frac{D}{2 \cdot \text{singleCoreN}} \Big\rfloor
$$</div>
<p>总组数 $= \lceil mCnt/m_{grp} \rceil \times \lceil nCnt/n_{grp} \rceil$。</p>
<p><b>组内 swizzle</b>:每组内部按 ASW 滑窗蛇形执行(窗口 $W = \max\{d \mid d \mid C,\; d \le \lfloor\sqrt{C}\rfloor\}$C=32 时 W=4保证同一波 C 个核的活跃工作集最小。组间切换时输入整体换入——上一组的 A 行带和 B 列带全部失效,从 GM 重新读入下一组的数据。</p>
<p>*例*B=64、M=N=2048、K=1024、BF16、singleCoreM=singleCoreN=256$S_{in} = 64 \times (2048 \times 1024 + 1024 \times 2048) \times 2 = 537\text{MB} &gt; 128\text{MB}$。$D = 128\text{MB}/(64 \times 1024 \times 2\text{B}) = 1024$ 元素。$m_{grp} = \lfloor 1024/(2 \times 256) \rfloor = 2$$n_{grp} = 2$。每组覆盖 $[512, 512]$ 的输出区域,工作集 $= 64 \times 1024 \times (512+512) \times 2 = 128\text{MB} = L2$,恰好装满。总组数 $= (2048/256/2)^2 = 16$。$r_{in} = (2 \times 2048 + 2 \times 2048)/(2048+2048) = 2$——每行 A 被读 2 次,每列 B 被读 2 次。</p>
<p>块内分配用<b>错位分核</b>(对角线分配):线性块号先取 mn 方向叠加随块号递增的相位偏移,使同一时刻各核落在 M×N 平面的不同对角线上——避免多核同一拍并发读同一行 A / 同一列 B 的同一地址同地址并发读会串行化等效带宽打折。例8 核、4×4=16 个基本块k0~k7 为核号):</p>
<pre><code>行优先(不错位): 错位分核(对角线):
n0 n1 n2 n3 n0 n1 n2 n3
m0 k0 k1 k2 k3 m0 k0 k4 . .
m1 k4 k5 k6 k7 m1 . k1 k5 .
m2 . . . . m2 . . k2 k6
m3 . . . . m3 k7 . . k3
同一波A 行带 m0 被 k0~k3 同读 同一波:每行带、每列带
4 路同地址冲突) 最多 2 核同读(冲突 4→2</code></pre>
<p>冲突度量与优选规则:</p>
<div class="math">$$
transConflict = \max\big(\lceil C / mCnt \rceil,\; \lceil C / nCnt \rceil\big) \le 6
$$</div>
<p>即同一时刻并发核访问同一 A/B 块的最大冲突数不超过阈值(经验值 6切分方案中优先选尾波不满载占比小拖尾 &lt; 一半)的。遍历大方向由 calOrder 决定0=M 优先、1=N 优先),按形状选共享矩阵更能驻留 L2 的方向。</p>
<p>补充:若输出会被后续算子立即消费(融合场景),输出驻留 L2 让下游读命中,场景 B/C 的策略反过来;本文按单算子边界分析。</p>
<h3>Step 6核内 tilingBaseM/BaseN/BaseK——L0 级 tile受 L0 容量直接约束)</h3>
<p>$\text{BaseM} \times \text{BaseN} \times 4\text{B} \times DB \le L0C$$\text{BaseM} \times k_{L0} \times \text{dtype} \times 2 \le L0A$、$k_{L0} \times \text{BaseN} \times \text{dtype} \times 2 \le L0B$;内轴按 dValue 256B/512B 对齐。SingleCoreM/N 内部按 BaseM/BaseN 进一步切分为 L0 tile 逐个计算。L1 按容量开双缓冲,余量充足开 4 buffer。</p>
<h3>Step 7内部特化参数极限不是独立分支</h3>
<p>单边无 batch 且该侧矩阵小($M \le 256$、$MK\cdot\text{dtype}\cdot 2 \le L1$、对侧每核循环 ≥4 轮)时小侧整个常驻 L1、只搬一次L1 全载)。</p>
<h3>Step 8降核模式实现</h3>
<p>tiling 时 <code>usedCoreNum = ⌈P⌉</code>(不强制 CSingleCoreM/N 在 L0C 容量内取最大($\text{singleCoreM} \times \text{singleCoreN} \times 4\text{B} \le L0C$每核按标准核内流水L1→L0→Cube→L0C→Fixpipe处理自己的输出块核间无共享无依赖无需 swizzle 与 L2 切分。降核后 GM 并发搬移核数若 &lt; minCoreNum带宽利用率上限被压低——这正是降核区 case 时延的瓶颈所在,也是"时延绝对值小、不再继续优化"的定量注脚。</p>
<hr>
<h2>参考文献</h2>
<ol class="tight">
<li>[昇腾 950 NPU 架构白皮书](https://public-download.obs.cn-east-2.myhuaweicloud.com/ascend/%E6%98%87%E8%85%BE950%20NPU%E6%9E%B6%E6%9E%84%E7%99%BD%E7%9A%AE%E4%B9%A6.pdf)华为技术有限公司2026</li>
<li>[cann-ops-nn 源码仓](https://gitcode.com/cann/ops-nn/tree/master/matmul/batch_mat_mul_v3)</li>
</ol>
</div>
</body>
</html>