diff --git a/BMM/BMM算子优化分析_Release/ASW_Basic分支分析_v1.2.html b/BMM/BMM算子优化分析_Release/ASW_Basic分支分析_v1.2.html deleted file mode 100644 index 82aea52..0000000 --- a/BMM/BMM算子优化分析_Release/ASW_Basic分支分析_v1.2.html +++ /dev/null @@ -1,515 +0,0 @@ - - -
- - -目标芯片:昇腾 950PR(DAV_3510)。本文为 ASW_Basic 分支的独立分析,自包含完整推导链。v1.2 修正 dValue 定义与尾轮重切约束,新增与源码实现的对比分析。-
ASW_Basic 是 BMM 的兜底分支——核间切 M/N(或混合切),不做 batch 合并或 K 维切分。本文给出完整的时延建模、核间分配策略分析(证明 B 优先分组在任何场景下都不优于线性映射)、实现方案的逐步推导(含尾轮重切的 Bound 类型分级策略),以及与源码实现的逐维度对比。
-核心结论:**B 优先分组不优于线性映射;尾轮重切在 host 端零代价、$n_{wave} \le 3$ 时应重切且计算 Bound 下 dValue 可放宽;源码的 swizzle/核间分配/AL1 全载与理论高度一致,主要差距在降核模式未实现、IsCapable 无并行度校验、尾轮未按 Bound 类型区分重切策略。**
-BMM 中,当 B ≥ C(batch 数 ≥ AIC 核数)时核间切 B 是免费的(每核独立处理若干 batch,无共享无依赖),由 IterBatch/MergeBatch 承接。当 B < C 或 B ≥ C 但 IterBatch/MergeBatch 条件不满足时,需要切 M/N 来填满所有核——这就是 ASW_Basic。
-ASW_Basic 是实践中最常命中的分支:B 可大可小可等 1,交叉广播也由此承接(对广播侧做 L1/L2 驻留,共享关系与切 M/N 同构)。
-核间:B × mCnt × nCnt 个输出块,按 B→M→N 线性映射分配到 C 核
-核内:每核处理若干 [singleCoreM, singleCoreN] 输出块
- └── 每块内部:GM→L1→L0→Cube→L0C→Fixpipe 标准流水
- └── K 维不切(singleCoreK = K),按 kL1 分块搬入 L1
-数据流:
-GM ──MTE2──> L1 ──MTE1──> L0A/L0B ──MMAD──> L0C ──Fixpipe──> GM
- ↑_____________ L2 Cache(读 5.2TB/s)_____________↑
-| 符号 | 含义 | 表达式/取值 |
|---|---|---|
| $B$ | batch 数 | 输入参数 |
| $M, N, K$ | 矩阵维度 | 输入参数 |
| $C$ | AIC 核数 | 32 |
| $dtype$ | 输入元素字节数 | BF16 → 2B |
| $outB$ | 输出元素字节数 | BF16 → 2B |
| $L0C$ | L0C 容量/核 | 256KB |
| $L0A, L0B$ | L0A/L0B 容量/核 | 各 64KB |
| $L1$ | L1 容量/核 | 512KB |
| $L2$ | L2 容量(共享) | 128MB |
| $BW_{pc}$ | 单核 GM 带宽份额 | $W_{GM}/C = 50$ GB/s |
| $Q_{16}$ | 单核 Cube BF16 峰值算力 | 486/32 ≈ 15.2 TFLOPS |
| $W_{GM}$ | GM 带宽 | 1.6 TB/s |
| $BW_{L2}$ | L2 读带宽 | 5.2 TB/s |
*Tiling 参数*:
-| 符号 | 含义 | 约束层级 |
|---|---|---|
| $\text{BaseM}, \text{BaseN}$ | L0 级 tile 的 M/N 维度 | L0C 容量直接约束 |
| $baseK$ | L0 级 tile 的 K 维度 | L0A/L0B 容量约束 |
| $\text{singleCoreM}, \text{singleCoreN}$ | 每核输出 tile 的 M/N 维度 | L1 容量 + 并行度 + 搬移效率 |
| $k_{L1}$ | GM→L1 的 K 向粒度 | dValue ≥ 256B |
| $mCnt, nCnt$ | 单 batch 内 M/N 向块数 | $\lceil M/\text{singleCoreM} \rceil$ |
| $W$ | swizzle 窗口宽度 | $\max\{d : d \mid C, d \le \lfloor\sqrt{C}\rfloor\}$ |
层次关系:$K = \text{singleCoreK} \ge k_{L1} \ge baseK$,$\text{singleCoreM} \ge \text{BaseM}$,$\text{singleCoreN} \ge \text{BaseN}$。
-搬移效率:dValue 与 min_TileSize
-GM→L1 搬移使用 Nd2Nz DMA,每次搬移的关键参数:
-| 参数 | 含义 | A 矩阵 [singleCoreM, $k_{L1}$] | B 矩阵 [$k_{L1}$, singleCoreN] |
|---|---|---|---|
| nValue | 行数(非连续维) | singleCoreM | $k_{L1}$ |
| dValue | 每行连续字节数(连续维) | $k_{L1} \cdot dtype$ | $\text{singleCoreN} \cdot dtype$ |
| 总搬移量 | nValue × dValue | $\text{singleCoreM} \cdot k_{L1} \cdot dtype$ | $k_{L1} \cdot \text{singleCoreN} \cdot dtype$ |
dValue 是 ND 排布中连续维的字节数——对 ND 格式的右矩阵 B:非转置时连续维为 N,dValue = $\text{singleCoreN} \cdot dtype$;转置时连续维为 K,dValue = $k_{L1} \cdot dtype$。dValue 不是两个维度的乘积。
-两级搬移效率阈值:
-同时满足:
-逐条解释:
-| 项 | 含义 | 公式 |
|---|---|---|
| $T_{MTE2}$ | GM→L1 搬移时延 | $r_{in} \cdot S_{in} / (C \cdot BW_{pc})$ |
| $T_{MMAD}$ | Cube 计算时延 | $2BMNK / (C \cdot Q_{16})$ |
| $T_{FIX}$ | 输出写回时延 | $B \cdot MN \cdot outB / (C \cdot W_{pc})$ |
| $T_{drain}$ | 末块排空时延 | $O(T_{comp} + T_{write})$ |
其中 $S_{in} = B(MK + KN) \cdot dtype$ 为输入总量,$r_{in} \ge 1$ 为重复读倍率(GM 输入流量 / 输入总量)。$r_{in} = 1$ 表示每字节只从 GM 读一次(后续复用全命中 L2)——这是 GM 输入流量的下界。
-关键观察:$T_{MMAD}$ 和 $T_{FIX}$ 与核间分配策略无关——**分配策略只影响 $T_{MTE2}$**(通过 L2 命中率影响 $r_{in}$)。优化目标:让 $r_{in}$ 尽量接近 1。
-总输出块数 $= B \cdot mCnt \cdot nCnt$。并行度约束的正确形式是:
-即总块数至少能填满 C 核。这与 $mCnt \cdot nCnt \ge \lceil C/B \rceil$ 等价(两边同乘 B 后取整)。
-**不应要求 $B \cdot mCnt \cdot nCnt \;\%\; C = 0$**(整除)。整除是充分不必要条件——不整除时产生尾轮,尾轮的处理见 §五。
-每波活跃核所需的 A 行带 + B 列带:
-其中 $sM$ = singleCoreM,$sN$ = singleCoreN,$W$ 为 swizzle 窗口宽度。
-| 维度 | B 优先分组 | B→M→N 线性映射 |
|---|---|---|
| 每波活跃 batch 数 | B 个(每组一个) | 1 个($mCnt \cdot nCnt \ge C$ 时) |
| 每组/每波 swizzle 窗口 | $W_g = \max\{d \mid d \mid C_g,\; d \le \sqrt{C_g}\}$ | $W = \max\{d \mid d \mid C,\; d \le \sqrt{C}\}$ |
| 每波 L2 工作集 | $B \cdot (W_g \cdot sM + \frac{C_g}{W_g} \cdot sN) \cdot K \cdot dtype$ | $(W \cdot sM + \frac{C}{W} \cdot sN) \cdot K \cdot dtype$ |
| C 不整除 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——线性映射的工作集是分组方案的一半。
-为什么线性映射工作集更小:
-**场景 I:$mCnt \cdot nCnt \ge C$ 且单波工作集 ≤ L2**(最常见)
-线性映射:每波 C 个块在同一 batch 内,L2 命中率高。B 分组:B 个 batch 同时活跃,总工作集 $B$ 倍。线性严格更优。
-**场景 II:$S_{in} \le L2$(全部输入放得下 L2)**
-两种策略的 GM 流量相同($r_{in} = 1$,每字节只读一次)。但 B 分组在 C % B ≠ 0 时负载不均(组间核数不等)。线性不劣于分组。
-**场景 III:$mCnt \cdot nCnt < C$(单 batch 块数不够填满所有核)**
-线性映射:每波横跨 $\lceil C/(mCnt \cdot nCnt) \rceil$ 个 batch——这些 batch 的数据同时活跃。B 分组:每组 $C_g$ 核但只有 $mCnt \cdot nCnt < C_g$ 个块 → 组内有空闲核。线性映射至少所有核都有活干。线性严格更优。
-**场景 IV:$mCnt \cdot nCnt < C/B$(每组连自己的核都填不满)**
-B 分组每组空闲 $C_g - mCnt \cdot nCnt$ 个核,总算力浪费 $C - B \cdot mCnt \cdot nCnt$ 个核。线性映射无此问题。线性严格更优。
-B 优先分组在任何场景下都不优于线性映射。 根本原因:
-因此 ASW_Basic 采用 B→M→N 线性映射作为核间分配策略。
-L0C 是 Cube 的累加器,BaseM × BaseN 是每次 Cube 计算的输出 tile。BaseM/N 应尽量把 L0C 用满——L0C 利用率越高,每次 Cube 计算的输出越大,单位计算的启动/排空开销摊得越薄。
-L0C 双缓冲 vs UnitFlag 单缓冲:
-传统做法用 L0C 双缓冲实现 tile 间流水——计算 tile N+1 时,fixpipe 同时写出 tile N:
-但昇腾 950PR 的 Fixpipe 支持 UnitFlag——MMAD 每完成一个 16×16×16 基本块(512B 结果),Fixpipe 立即将其写出,无需等整个 L0C tile 算完。UnitFlag 提供的是 tile 内部的细粒度流水(16×16×16 粒度),替代双缓冲的 tile 间粗粒度流水(BaseM×BaseN 粒度)。
-UnitFlag 单缓冲下,L0C 只需一份 buffer:
-BaseM/N 可放大 $\sqrt{2}$ 倍(如 256→362),mCnt/nCnt 相应减小,L2 重复读率降低(单 batch 块数减少 → 每行 A 被更少的组读取)。
-*时延对比*(单 tile 粒度,BF16 输出):
-| 方案 | tile 大小 | tile 间流水 | tile 内流水 | 单 tile 时延 |
|---|---|---|---|---|
| 双缓冲 | 181×181(32761 元素) | ✓(tile N 写出 ∥ tile N+1 计算) | ✗ | max($T_{comp}$, $T_{write}$) |
| UnitFlag 单缓冲 | 256×256(65536 元素) | ✗ | ✓(16×16×16 粒度) | max($T_{comp}$, $T_{write}$) |
两种方案的稳态时延相同(都是 max($T_{comp}$, $T_{write}$)),但 UnitFlag 单缓冲的 tile 更大 → 总 tile 数更少 → 循环开销更小。但当前 BMM ASW kernel 未启用 UnitFlag(unitFlag = 0,注释 "each l0 only process one block, disable unit flag"),且源码在 baseM=baseN=256 时已自动选 dbL0C=1(256×256×4B×2 > L0C)——即源码已经是单缓冲 + 无 UnitFlag,tile 到顶但无流水交叠。
*建议*:对计算 Bound 的 case 启用 UnitFlag(MMAD 与 Fixpipe 流水并行),可将单 tile 时延从 $T_{comp} + T_{write}$ 降至 $\max(T_{comp}, T_{write})$。对访存 Bound 的 case(MTE2 Bound),UnitFlag 收益小([CANN 文档](https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/920beta1/API/ascendcopapi/atlasascendc_api_07_0003.html):MTE2 Bound 时 MMAD/FIX 流水可被搬移掩盖)。
-BaseM/BaseN 的长宽比跟随 SingleCoreM/SingleCoreN(进而跟随 M/N),对齐 16 的倍数。baseK 由 L0A/L0B 容量决定(L1→L0 搬移无 dValue 要求,dValue 约束的是 GM→L1 的 $k_{L1}$):
-核间不切 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 容量约束)。
-*BaseM/N 的具体确定过程*(host 端枚举,16 对齐遍历):
-*与源码的差异*:源码默认 baseM=baseN=256(硬编码 "256 is better base"),再由 cubeBound 模型(L2 供数能力 + 溢出惩罚 + K 向复用)枚举收缩。理论直接从 L0C 容量出发,cubeBound 的三项修正可从第一性原理推导:①L2 供数速率 vs Cube 耗数速率 → 访存 Bound 时应缩 tile;②工作集超 L2 → 增大 tile 减少重复读;③K 越大越偏 compute bound → 可放大 tile。两者方向一致,源码多了实测调优的余量(CUBE_BOUND_RATIO=0.85)。
-SingleCoreM × SingleCoreN 是每核每次处理的输出区域,不受 L0 容量直接约束——一个 [SingleCoreM, SingleCoreN] tile 内部由若干 [BaseM, BaseN] L0 tile 组成($\text{SingleCoreM} \ge \text{BaseM}$,$\text{SingleCoreN} \ge \text{BaseN}$)。SingleCoreM/N 的核心影响是 GM→L1 搬移效率和 L2 重复读率:
-*约束链*:
-约束 1——并行度下限:总块数须填满 C 核。
-约束 2——L1 容量(双缓冲下驻留当前 tile 的输入):
-约束 3——搬移效率(dValue 见 §1.4):
-约束 4——SingleCoreM/N 是 BaseM/N 的整数倍(工程实现要求,保证 L0 tile 边界对齐)。
-*选取策略*:在满足约束 1 的前提下,SingleCoreM/N 尽量大(搬移效率和 L2 复用最大化)。长宽比跟随 M/N($\text{singleCoreM}/\text{singleCoreN} \approx M/N$),对齐到 BaseM/BaseN 的整数倍。
-*SingleCoreM/N 的具体确定过程*(host 端枚举,与尾轮处理联动):
-*与源码的差异*:源码用 cubeBound 模型 + balanceRate ≥ 0.9 剪枝,不解耦 SingleCoreM/N 与 BaseM/N(baseM=256 已是 SingleCore 级参数,L0 tile 由 stepM/stepN 二次切分)。理论的两层分离使约束链更清晰——SingleCoreM/N 由 L1 + 并行度 + 搬移效率决定,BaseM/N 由 L0C 决定,各司其职。
-*例*(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$ → 取 64(16 对齐)= 128B ✓。
-总输出块数 = $B \times mCnt \times nCnt$。核间分配采用 B→M→N 线性映射(分析见 §四):块按 (b, m, n) 字典序编号,核 i 处理块 i, i+C, i+2C, …。B 不整除 C 时尾波不满载的核少分一块,无需 B | C。
-尾轮处理(host 端预处理,零 NPU 开销):
-尾轮定义
-当 $B \cdot mCnt \cdot nCnt \;\%\; C \neq 0$ 时,总块数不能被 C 整除,最后一波(尾轮)不满载——只有 $B \cdot mCnt \cdot nCnt \;\%\; C$ 个核有活干,其余核空闲。
-尾轮重切:是否值得?
-关键前提:tiling 在 host(CPU)上完成,不占 NPU 时间。 重切只需 host 多算一套 tiling 参数下发给 NPU,零 NPU 开销。这改变了收益/代价的平衡——重切的代价仅为 host 端多一次枚举,而收益是 NPU 端尾波时延的缩短。
-不重切时:尾波 $r$ 个块用 $r$ 个核,每核处理 1 块,时延 = $T_{block}$,$C - r$ 核空闲。
-重切时:把 $r$ 个块沿 M 或 N 维再切 $s$ 份,变成 $r \cdot s$ 个小块,$r \cdot s$ 个核各处理 1 小块,时延 ≈ $T_{block}/s$(每小块计算量/搬移量/写回量均为原块的 $1/s$)。
-*重切收益*:
-*占总时延比例*:
-$n_{wave} = 2$、$s = C/r$ 时:$r=16$ → 收益 25%;$r=8$ → 收益 37.5%。$n_{wave} = 3$ 时:$r=16$ → 16.7%;$r=8$ → 25%。**$n_{wave} \le 3$ 时收益显著,应重切。**
-*重切约束的推导*:沿 N 切 $s$ 份后,小块变为 $[\text{singleCoreM},\; \text{singleCoreN}/s]$。小块须满足两类约束——但约束的严格程度取决于算子是访存 Bound 还是计算 Bound。
-**访存 Bound($T_{MTE2} \ge T_{MMAD}$)**:搬移是瓶颈,小块的 dValue 和搬移量不能降——否则搬移更慢,总时延反而增加。约束与主 tile 相同:
-**计算 Bound($T_{MMAD} > T_{MTE2}$)**:搬移被计算掩盖,dValue 和搬移量约束可放宽——即使搬移效率降低,只要搬移时间仍 ≤ 计算时间,总时延不变。放宽的定量依据:
-小块计算时延 $T_{comp}^{tail} = 2 \cdot \text{singleCoreM} \cdot \text{singleCoreN}_{tail} \cdot K / Q_{16}$,搬移时延 $T_{load}^{tail} = (\text{singleCoreM} + \text{singleCoreN}_{tail}) \cdot k_{L1} \cdot dtype / BW_{pc}$。搬移被掩盖的条件:
-记右端为 $x_{min}$(搬移掩盖下限),则计算 Bound 下的约束为:
-**计算 Bound 越严重(K 越大),$x_{min}$ 越小,$s$ 上限越大**——极端情况 $x_{min} \to 16$(对齐底线),$s \le \text{singleCoreN}/16$。
-*例*(singleCoreM=singleCoreN=256、$k_{L1}$=128、K=1024、BF16):$x_{min} = 256 \times 128 \times 2 \times 15.2 \times 10^{12} / (50 \times 10^9 \times (2 \times 256 \times 1024 - 128 \times 2 \times 15.2 \times 10^{12} / 50 \times 10^9)) \approx 1$——几乎无约束,$s \le 256/16 = 16$。
-*最优切分因子*:先判定 Bound 类型($T_{MMAD}$ vs $T_{MTE2}$),再选约束集:
-*例*(C=32、$N_{blk}=40$、$n_{wave}=2$、$r=8$、singleCoreM=singleCoreN=256、$k_{L1}$=128、BF16):
-*切分方向选择*:优先沿 N 切(保持 A 行带完整,L2 中 A 数据不变)。计算 Bound 下若 N 向对齐卡住($\text{singleCoreN}/16 < C/r$),改沿 M 切(A 矩阵 dValue = $k_{L1} \cdot dtype$ 不变,仅受对齐约束 $\text{singleCoreM}/16$)。
-结论:$n_{wave} \le 3$ 且 $r < C$ 时应重切尾轮——host 端零代价,NPU 端收益 $T_{block}(1-1/s^*)$。$n_{wave} \ge 4$ 时收益 < 25%,可不重切(通过选择使尾波占比小的 mCnt/nCnt 组合来优化)。
-尾轮影响的量化
-设总块数 $N_{blk} = B \cdot mCnt \cdot nCnt$,总波数 $n_{wave} = \lceil N_{blk} / C \rceil$,尾波块数 $r = N_{blk} \bmod C$($r = 0$ 时无尾波)。
-不重切时尾波导致的额外时延(相对于完美整除):
-重切后尾波时延降为 $T_{block}/s^*$,额外时延:
-当 $r \cdot s^* = C$(完美重切)时 $\Delta T_{tail}^{re} = 0$——尾波完全消除。
-切 B(零共享,先试)→ 切 M(右矩阵 $KN\cdot\text{dtype} \le L2$ 则驻留 L2)→ 切 N(对称)→ 混合切(靠 swizzle + L2 切分管理)→ 降核(见 Step 8)。
-问题:核间切 M/N 后,同一时刻 C 个核各算一个输出块,它们所需的 A 行块与 B 列块集合就是当前"活跃工作集"。若按行优先顺序朴素分配,一波 C 个块横跨的 A 行、B 列很宽,活跃工作集超过 L2 就回 GM 读(1.6TB/s),重复读代价真实发生。swizzle 要做的就是编排输出块的执行顺序,把每一波核的活跃工作集压到最小。
-做法:把 M 向每 W 个基本块划为一个"窗口",遍历顺序为"窗口内先扫 M、扫满 W 行再进下一列 N;一个窗口扫完再进下一个窗口",且奇数窗口行 N 向反向(蛇形)。效果有二:
-W 怎么取:一波 C 个块的 L2 足迹约为
-由均值不等式,$W + C/W$ 在 $W = \sqrt{C}$ 处取最小——窗口越接近"方形"(W 行 × C/W 列),足迹越小。同时 W 须整除 C,保证每个窗口恰好被整数波核覆盖、窗口边界不把波次切碎。合起来即:
-C=32 时 $\sqrt{32} \approx 5.66$,因子 {1,2,4,8,…} 中不超过它的最大者是 4,故 W=4。
-实例(C=32,W=4,M̃=8,Ñ=8;数字为块的全局执行顺序,一波 32 块):
-窗口0(N 正向) 窗口1(N 蛇形反向)
- ν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
-块 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。
-窗内为什么不蛇形(对上例中"块 3=(μ3,ν0) → 块 4=(μ0,ν1) 而非 (μ3,ν1)"的说明):蛇形的收益来自"相邻遍历段共享边界数据",要分两种边界看:
-BatchMatMulAswBlock::UpdateBasicIndex:仅 rowIdx 为奇时 n 反向,窗内 m 最快序不反向),正是这个收益结构的直接实现。切的是什么:将 mCnt×nCnt 个基本块划分为若干执行组——每组覆盖输出平面上一个连续矩形区域(若干 singleCoreM × singleCoreN 基本块的集合),使该组所需的 A 行带 + B 列带输入工作集 ≤ L2 可用读入空间;组内所有基本块算完再进下一组,输入只在跨组时换一次。
-为什么需要它:滑窗压缩的只是"同一波"的足迹;若整个工作集超 128MB L2,跨波次复用落空——上一波窗口的 A 行早被挤出,下一波又得回 GM 读。且 L2 是读写共用的:输出经 fixpipe 写出时若驻留 L2(dirty),会压缩读入可用空间;若直写 GM,则占用与读共享的 1.6TB/s 总线。所以 L2 切分必须与写出策略联合决策。记输入总量 $S_{in} = B(MK+KN)\cdot\text{dtype}$,输出总量 $S_{out} = B \cdot MN \cdot outB$。
-两个不变量(一切分析的起点):
-重复读倍率:$r_{in}$ = GM 输入流量 / $S_{in}$。$r_{in} = 1$ 表示每个输入数据从 GM 只读一遍(后续复用全在 L2 命中)——这是 GM 输入流量的下界,L2 管理的全部目标就是让 $r_{in}$ 尽量接近 1。
-先判定写出会不会 Bound。平均写出带宽需求:
-只与 K、outB 有关(K 越小,单位时间输出越密)。例(BF16 输出,C·Q₁₆=432 TFLOPS):K=512 → 844 GB/s;K=256 → 1.69 TB/s,已超 GM 总线——此时任何策略都写出 Bound,L2 缓冲只能削峰(fixpipe 以 5.2TB/s 写 L2 吸收突发),平均速率仍受总线限制,应预期 Fixpipe 成为 $T_{total}$ 的 max 项。
-分场景决策:
-**场景 A:$S_{in} + S_{out} \le L2$(全驻留)**。输入读一遍($r_{in}=1$),输出驻留 L2(dirty)异步回写 GM——写出走 5.2TB/s L2 写口,不与读争,也削平了 GM 写突发。无需切分。
-**场景 B:$S_{in} \le L2$ 但 $S_{in} + S_{out} > L2$(输入能驻留,加上输出超了)。策略:输入驻留、输出直写 GM**。理由链:
-例:B=8、M=N=4096、K=512、BF16——$S_{in}$≈67MB ≤ L2,$S_{out}$≈268MB 直写 GM;T_MMAD≈318µs,总流量速率 (67+268)MB/318µs ≈ 1.05TB/s < 1.6TB/s ✓。
-**场景 C:$S_{in} > L2$(输入本身超)。需要分组执行——把 mCnt×nCnt 个基本块划分为若干执行组**,每组内所有基本块的输入工作集不超过 L2 可用空间。输出直写 GM(不占 L2 读入空间)。
-L2 的软件可控手段:L2 是 Cache 而非 Buffer,软件无法精确控制"哪些数据在 L2 里"。可用的控制手段:
-SetL2CacheHint):标记输入为 allocate(读入 L2)或 non-allocate(直读 GM 不过 L2);标记输出为 non-allocate(直写 GM 不占 L2);执行组的划分:
-*问题*:mCnt×nCnt 个基本块(每块输出 singleCoreM×singleCoreN),按什么粒度分组,使每组的输入工作集 ≤ L2?
-*每组输入工作集*:一组覆盖 M 向 $m_{grp}$ 个基本块、N 向 $n_{grp}$ 个基本块,即覆盖输出区域 $[m_{grp} \cdot \text{singleCoreM},\; n_{grp} \cdot \text{singleCoreN}]$。该区域需要读入的输入:
-*目标*:最小化组数(组数越少,输入从 GM 的重复读次数越少)。每行 A 被 $n_{grp}$ 个组各读一次,每列 B 被 $m_{grp}$ 个组各读一次:
-*求解*:约束 $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 向基本块数与输出平面形状成正比时取到:
-总组数 $= \lceil mCnt/m_{grp} \rceil \times \lceil nCnt/n_{grp} \rceil$。
-组内 swizzle:每组内部按 ASW 滑窗蛇形执行(窗口 $W = \max\{d \mid d \mid C,\; d \le \lfloor\sqrt{C}\rfloor\}$,C=32 时 W=4),保证同一波 C 个核的活跃工作集最小。组间切换时输入整体换入——上一组的 A 行带和 B 列带全部失效,从 GM 重新读入下一组的数据。
-*例*(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} > 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 次。
-块内分配用错位分核(对角线分配):线性块号先取 m,n 方向叠加随块号递增的相位偏移,使同一时刻各核落在 M×N 平面的不同对角线上——避免多核同一拍并发读同一行 A / 同一列 B 的同一地址(同地址并发读会串行化,等效带宽打折)。例(8 核、4×4=16 个基本块,k0~k7 为核号):
-行优先(不错位): 错位分核(对角线):
- 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)
-冲突度量与优选规则:
-即同一时刻并发核访问同一 A/B 块的最大冲突数不超过阈值(经验值 6);切分方案中优先选尾波不满载占比小(拖尾 < 一半)的。遍历大方向由 calOrder 决定(0=M 优先、1=N 优先),按形状选共享矩阵更能驻留 L2 的方向。
-补充:若输出会被后续算子立即消费(融合场景),输出驻留 L2 让下游读命中,场景 B/C 的策略反过来;本文按单算子边界分析。
-$\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。
-单边无 batch 且该侧矩阵小($M \le 256$、$MK\cdot\text{dtype}\cdot 2 \le L1$、对侧每核循环 ≥4 轮)时小侧整个常驻 L1、只搬一次(L1 全载)。
-tiling 时 usedCoreNum = ⌈P⌉(不强制 C),SingleCoreM/N 在 L0C 容量内取最大($\text{singleCoreM} \times \text{singleCoreN} \times 4\text{B} \le L0C$),每核按标准核内流水(L1→L0→Cube→L0C→Fixpipe)处理自己的输出块;核间无共享无依赖,无需 swizzle 与 L2 切分。降核后 GM 并发搬移核数若 < minCoreNum,带宽利用率上限被压低——这正是降核区 case 时延的瓶颈所在,也是"时延绝对值小、不再继续优化"的定量注脚。
源码参考 [cann-ops-nn](https://gitcode.com/cann/ops-nn/tree/master/matmul/batch_mat_mul_v3)(DAV_3510/arch35)。以下按实现方案的 Step 0-8 逐维度对比。
-源码(batch_matmul_v3_asw_basic_tiling.cpp IsCapable):
// 源码:https://gitcode.com/cann/ops-nn/tree/master/matmul/batch_mat_mul_v3
-1. A/B 非连续转置状态一致(混合则拒绝)
-2. BatchA == BatchB(广播由 ITER_BATCH_BROADCAST 承接)
-3. batchBias <= 1
-4. dtype 限 FP16/BF16
-// 无其他条件——兜底
-| 维度 | 理论 | 源码 | 差异 |
|---|---|---|---|
| 并行度校验 | $P = B \cdot MN \cdot 4B / L0C \ge C$ | 无 | 源码不检查 P,靠优先级序靠前分支截胡 |
| 降核模式 | $P < C$ → usedCoreNum = ⌈P⌉ | 无 | GetNumBlocks() 固定返回 32 核 |
| batch 结构 | 无限制 | BatchA == BatchB | 源码排除广播(由分支 4 承接) |
影响:源码缺少降核分支。P < C 的 case 进入 ASW_Basic 后,所有 32 核仍参与调度,但部分核无实际工作——引入不必要的调度开销。理论上这些 case 应走降核模式(usedCoreNum = ⌈P⌉)。
-| 维度 | 理论 | 源码 |
|---|---|---|
| 目标 | BaseM × BaseN = L0C/(2×4B) = 32768,先把 L0C 用满 | baseM=256, baseN=256(硬编码 "256 is better base") |
| 长宽比 | 跟随 M/N | 由 cubeBound 模型寻优 |
| baseK | min(L0A/2·BaseM, L0B/2·BaseN),无 dValue 要求 | baseK = 128B/dtype(最小高效粒度) |
源码的 cubeBound 模型(GetRebalanceBlock,matmul_v3_tiling_helper.cpp L387-492):
三项含义:① L2 供数速率 ÷ Cube 耗数速率(理论阈值);② 工作集超 L2 时的访存惩罚(抬高 edge,倾向更大 tile);③ K 向复用修正(K 越大 edge 越小,偏 compute bound)。乘以 CUBE_BOUND_RATIO=0.85 余量。枚举 baseM/baseN 递减,balanceRate ≥ 0.9 剪枝。
-差异分析:理论(修正前)按双缓冲取 L0C 满载的一半(32768 元素),源码用 256×256=65536 元素——源码的 baseM×baseN 恰好等于 L0C 单缓冲上限(256×256×4B = 256KB = L0C)。源码的 dbL0C 逻辑:dbL0C = (baseM × baseN × 4B × 2 > L0C) ? 1 : 2——baseM=baseN=256 时 512KB > 256KB,自动选 dbL0C=1(单缓冲)。
这说明源码的 baseM/baseN 不是 L0 级 tile 尺寸,而是 SingleCoreM/N 级参数。源码中 L0 级 tile 由 stepM/stepN 二次切分决定(per-core tile = baseM × stepM)。理论的分层(SingleCoreM ≥ BaseM)在源码中无显式概念。
-UnitFlag 现状:BMM ASW kernel 中 unitFlag = 0(block_mmad_iterbatch.h L315 注释:"each l0 only process one block, disable unit flag")——MMAD 与 Fixpipe 之间是指令级同步(整个 L0C tile 算完才写出)。若启用 UnitFlag,MMAD 每完成一个 16×16×16 块(512B),Fixpipe 立即写出——tile 内部细粒度流水替代 tile 间粗粒度流水,单缓冲即可达到双缓冲的流水效果。对计算 Bound 的 case,UnitFlag 可将单 tile 时延从 $T_{comp} + T_{write}$ 降至 $\max(T_{comp}, T_{write})$。
| 维度 | 理论 | 源码 |
|---|---|---|
| 概念 | 独立于 L0,由 GM→L1 效率 + L2 复用决定 | 无显式概念,per-core tile = baseM × stepM |
| 约束 | 并行度 + L1 容量 + 搬移效率 + BaseM 整数倍 | cubeBound 模型 + balanceRate ≥ 0.9 |
| 选取策略 | 尽量大(搬移效率最大化) | 枚举寻优(cubeBound + 尾块感知) |
关键差异:理论把 SingleCoreM/N 和 BaseM/N 分为两层(SingleCoreM ≥ BaseM),源码中 baseM=256 实际上已经是 SingleCoreM 级参数。理论的分层更清晰,源码更保守但经实测调优。
-| 维度 | 理论 | 源码 |
|---|---|---|
| 分配策略 | B→M→N 线性映射 | 一致(index = blockIdx + round × usedCoreNum) |
| B 优先分组 | 证明不优 | 源码确实采用线性映射 |
| 尾轮 | 不重新切分 | 一致(balanceRate ≥ 0.9 剪枝) |
结论:核间分配策略与理论一致。源码的 UpdateBasicIndex(batch_mat_mul_v3_asw_block_advanced.h)实现了 B→M→N 线性映射 + 划窗蛇形。
| 维度 | 理论 | 源码 |
|---|---|---|
| 窗口大小 | $W = \max\{d : d \mid C, d \le \lfloor\sqrt{C}\rfloor\}$ | GetAswWindowLen:逻辑一致 |
| 蛇形 | 窗口行间 N 向反向,窗内不蛇形 | 一致(rowIdx % 2 != 0 时 n 反向) |
| 尾窗 | 未提及 | 有 tailWindow 分支处理 mCnt 不整除窗长 |
结论:swizzle 实现与理论一致。源码的窗长公式与理论 $W = \max\{d : d \mid C, d \le \lfloor\sqrt{C}\rfloor\}$ 完全相同。但源码对细长 shape(mCnt 小或 nCnt≫mCnt)的方形窗假设不成立时,退化为行优先(mainWindow = min(aswWindowLen, mCnt)),未按 shape 长宽比自适应窗形。
| 维度 | 理论 | 源码 |
|---|---|---|
| 启用条件 | $S_{in} > L2$ 时分组 | isBigSize(>100MB) && cBatchDimAll < usedCoreNum && transConflict ≤ 6 |
| 冲突度量 | transConflict ≤ 6 | 一致 |
| 写出策略 | $S_{in} \le L2 < S_{in}+S_{out}$ 时输出直写 GM | 仅 enableL2Cache flag + SetL2CacheHint,无显式输出 non-allocate 决策链 |
| 尾波控制 | 优先选尾波不满载占比小的方案 | TAIL_CONFLICT_RATIO = 0.5 |
差异分析:源码有两个额外限制:
-源码中理论要求的"输出直写 GM vs 驻留 L2"的写出策略联合决策(场景 B:$S_{in} \le L2 < S_{in}+S_{out}$)未见显式实现。
-| 维度 | 理论 | 源码 |
|---|---|---|
| 条件 | M ≤ 256,batchA=1,A 驻留 L1,每核 ≥4 轮 | 一致(IsCapable L31-72) |
| A 搬移 | GM→L1 一次搬入 | 一致(Nd2Nz DataCopy 一次搬入) |
| B 搬移 | 每基本块一次 | 一致 |
结论:AL1 全载是理论与源码最吻合的分支。源码注释有笔误("m should be larger than 256" 实为 ≤256),需注意。
-| 维度 | 理论 | 源码 |
|---|---|---|
| 尾轮重切 | 不重切(收益有限、工程代价真实) | 一致(balanceRate ≥ 0.9 剪枝) |
| 尾轮量化 | $\Delta T_{tail}/T_{total} \approx (1-r/C)/n_{wave}$ | GetBalanceRateWithTail(L223-246) |
结论:尾轮处理策略一致。源码通过选择使尾波占比尽量小的 mCnt/nCnt 组合来优化,而非重新切分尾轮。
-| 维度 | 理论 | 源码 |
|---|---|---|
| 实现 | usedCoreNum = ⌈P⌉,其余核闲置 | 未实现——GetNumBlocks() 固定返回 32 |
影响:P < C 的 case(小 M/N、小 B)进入 ASW_Basic 后,所有 32 核参与调度但部分核无实际工作。理论上应只调度 ⌈P⌉ 核。这类 case 的时延绝对值小,但多余核的调度开销(tiling 计算、kernel 启动、上下文切换)是真实存在的。
-| 维度 | 评价 |
|---|---|
| 核间分配 + swizzle | ✓ 与理论一致 |
| AL1 全载特化 | ✓ 与理论最吻合 |
| 尾轮处理 | ✓ 一致(不重切,选小尾波组合) |
| BaseM/BaseN 分层 | ⚠️ 源码无显式 SingleCore/Base 分层,baseM=256 实为 SingleCore 级 |
| L2 管理 | ⚠️ 比理论保守(100MB 阈值 + batch 数限制),缺输出写出策略联合决策 |
| 降核模式 | ✗ 未实现 |
| 经验常数 | ⚠️ CUBE_BOUND_RATIO=0.85、balanceRate=0.9、transConflict=6、TAIL_CONFLICT_RATIO=0.5 等无官方文档推导,边界 case 值得实测复核 |
总体判断:源码的 ASW_Basic 实现在核间分配、swizzle、AL1 全载上与理论高度一致,是经实测调优的工程实现。理论分析的价值在于:
-