298 lines
28 KiB
Markdown
298 lines
28 KiB
Markdown
# UnitFlag 能否完全取代 L0C Double Buffer —— 昇腾 950PR(DAV_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` 的函数签名后**定死**:
|
||
|
||
```cpp
|
||
// 官方 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 个实参是 `true` → **enUnitFlag 被显式开启**。而 mat_mul_v3 的普通 ASWT、A/B 全载、SplitK 等模板 kernel 的默认模板参数就是 `MM_CFG = MM_CFG_NO_PRELOAD`;StreamK 又在自定义搬出回调 `CustomDataCopyOut` 里再显式 `fixpipeParams.unitFlag = MM_FIX_PIPE_UNIT_FLAG(=3)`。
|
||
|
||
**结论**:mat_mul_v3 在 950PR 的主路径上,**UnitFlag 默认是开启的**(普通模板经 MM_CFG_NO_PRELOAD,StreamK 经回调)。这与"MDL 模板默认不使能"并不矛盾——MDL 默认关,但 mat_mul_v3 用 `GetMDLConfig` 把这一位显式置真了。
|
||
|
||
---
|
||
|
||
## 1. 两条掩盖路径的时序本质(先把"流水"画清楚)
|
||
|
||
设 MTE1(L1→L0)与 MTE2(GM→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 ≤ L0C`(256KB)→ `baseM·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`,瓶颈转移到 Fixpipe,DB 掩盖不充分。
|
||
|
||
### 1.3 只开 UnitFlag —— 块内 512B 细粒度流水
|
||
|
||
UnitFlag 把单个 base 块按 512B 切成若干 micro-tile,MMAD 每算完一个 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_fp`,Fixpipe 也是边算边搬,Cube 不停(是 Fixpipe 流水成为瓶颈,而非 Cube 空等)。
|
||
|
||
---
|
||
|
||
## 2. 块内能 max 了,块间还需不需要考虑?—— 叠加性论证
|
||
|
||
这是你追问的核心。我们严格区分两种情形。
|
||
|
||
### 2.1 情形 A:K 不在单块内累加(一次 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 情形 B:K 在单块内分片累加(多次 Iterate 累加进同一 L0C)
|
||
|
||
当 `singleCoreK` 很大、被切成多个 `baseK` 分片时,同一 base 块要做多轮 `MMAD→(部分结果留在 L0C)`,**Fixpipe 只在最后一次累加完成后才整体搬出**。
|
||
|
||
- 这里 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 有两层官方文档,约束范围不同,必须分开看**
|
||
> - **高阶 API 层**(`AscendC::Matmul` 对象 + `MatmulConfig.enUnitFlag`):约束见《Matmul 高阶 API 开启 UnitFlag》;
|
||
> - **基础 API 层**(`Mmad`/`Copy` 指令 + 指令参数 `unitFlag`):机制与取值见《UnitFlag —— Mmad 计算关键特性说明》。
|
||
>
|
||
> mat_mul_v3 走**高阶 API**(`mm_.Iterate()` / `mm_.GetTensorC()`),所以高阶 API 层的约束对它直接生效;基础 API 层是底层硬件机制,用来理解约束的来由。两层的"能不能做 K 分段累加"答案**不一样**(详见 §3.4.1、§3.5)。
|
||
|
||
**高阶 API 层官方约束**(出处:昇腾官网 → CANN → 算子实践参考 → 优秀实践 → Matmul 性能调优案例 → 《Matmul 高阶 API 开启 UnitFlag》社区版 / 《Matmul 高阶 API 使能 UnitFlag》商用版,"约束条件"一节,逐字摘录):
|
||
|
||
1. **模板限制**:UnitFlag 功能仅支持 **Norm、IBShare、MDL** 三个模板。
|
||
2. **流水互斥**:开启 UnitFlag 功能时,不支持算子内**同时存在 L0C Buffer(CO1)搬出到 Global Memory 和 L1 Buffer(A1)搬出到 Global Memory 的两种流水**。
|
||
3. **累加限制**:开启 UnitFlag 功能时,若同时开启 **L0C 累加** 功能,不支持**多次 Iterate 计算、一次 GetTensorC 输出**。
|
||
4. **收益前提**:仅当 MMAD 流水与 FIXPIPE 流水**串行执行且未被其他流水掩盖**(比如 MTE2 Bound)时,开启 UnitFlag 才有收益;否则收益很小。
|
||
|
||
这四条是官方文档明确写出的原文(第 2、3 条在 §3.4 逐条考据)。落在高阶 API 约束之外的场景,UnitFlag 不可用,**只能退回 L0C DB** 做块间掩盖。
|
||
|
||
### 3.3 从 mat_mul_v3 实际看:默认已开 UnitFlag,DB 是「补位」而非「主选」
|
||
|
||
- mat_mul_v3 主路径 `MM_CFG_NO_PRELOAD` 已把 enUnitFlag 置真,StreamK 又在回调里显式 `unitFlag=3` → **UnitFlag 是该算子的默认掩盖手段**。
|
||
- 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 输出」——具体是什么约束?它与"K 分段累加"是什么关系?
|
||
|
||
**官方原文出处**(高阶 API 层,两处):
|
||
|
||
- 昇腾社区官网 CANN 社区版 → 算子实践参考 → 优秀实践 → Matmul 性能调优案例 → **《Matmul 高阶 API 开启 UnitFlag》**,约束条件第 3 条:
|
||
> 开启 UnitFlag 功能时,若同时开启 **L0C 累加** 功能,不支持**多次计算、一次输出**。
|
||
- 昇腾社区官网 CANN 商用版 → 同名章节 **《Matmul 高阶 API 使能 UnitFlag》**,约束条件第 3 条(表述更完整):
|
||
> 使能 UnitFlag 功能时,若同时使能 **L0C 累加功能**,不支持**多次 Iterate 计算、一次 GetTensorC 输出**。
|
||
|
||
**这是"高阶 API(Iterate/GetTensorC)"层的约束,不是"基础 API(Mmad/Copy)"层的限制**——这是理解的关键,也是本节要澄清的核心误区。
|
||
|
||
**它约束的准确形态**:用**高阶 API** 编程时,同一个 base 块如果「`mm.Iterate()` 多次(K 分片在 L0C 驻留累加)、最后只 `mm.GetTensorC()` 一次(一次输出)」,且此时打开了 L0C 累加功能,那么高阶 API 内部的 UnitFlag 封装无法正确处理 → 不允许。即:
|
||
|
||
```cpp
|
||
// 高阶 API 下被禁止的形态
|
||
for (kSlice = 0; kSlice < kSliceNum; kSlice++) {
|
||
mm.Iterate(); // 多次 Iterate,部分和在 L0C 驻留累加
|
||
}
|
||
mm.GetTensorC(c); // 只在最后一次搬出
|
||
```
|
||
|
||
**但是——底层基础 API(Mmad/Copy 指令)完全支持 K 分段累加,且能做出很好的流水掩盖。** 这是用户指出的关键点,官方《UnitFlag —— Mmad 计算关键特性说明》(基础 API/矩阵计算 Tensor API)有明确机制与实例,逐字核实如下:
|
||
|
||
**底层机制**:开启 UnitFlag 后,L0C Buffer 按 **512B 内存块**划分,每块配一个「单元标志位」(unit-flag),指示该块可读/可写。`Mmad`(写)与 `Copy`/`Fixpipe`(读)通过 `unitFlag` 参数控制:
|
||
|
||
| unitFlag 取值 | 语义(写=Mmad / 读=Fixpipe) |
|
||
|---|---|
|
||
| **2** | 指令执行后**不改变**标志位:写操作写完保持"占用",让后续 Mmad 还能继续写这块(用于 K 累加的中间片);读操作读完保持"可读",让后续 Fixpipe 还能继续读 |
|
||
| **3** | 指令执行后**翻转**标志位:Mmad 写完置"可读"(放给 Fixpipe 搬);Fixpipe 读完置"可写"(放还给 Mmad) |
|
||
|
||
**官方 K 分段累加实例**(原文):A=128×1024、B=1024×128,沿 K 轴迭代,每次迭代 K 长 128,共 8 次 Mmad 对应 1 次 Fixpipe:
|
||
|
||
- **前 7 次 Mmad 设 `unitFlag=2`**:写入后把标志位**始终保持 0**,保证后续 Mmad 能继续写入同一块 L0C Buffer(即在同一块 L0C 上做 K 累加);
|
||
- **最后 1 次 Mmad 设 `unitFlag=3`**:写入后把标志位置 1,保证 Fixpipe 可以读取 L0C Buffer;
|
||
- **Fixpipe 设 `unitFlag=3`**:读取后把标志位置 0,保证后续 Mmad 接口可以顺利写入。
|
||
|
||
**结论修正**:所以"UnitFlag 支持 K 分段累加、最后一次搬出"在**基础 API 层是成立的、且是官方推荐用法**。用户描述的"以 16×K×16 为粒度,算完一段立即让 Fixpipe 搬出对应 16×16 到 GM、同时开始下一段 16×K×16 计算"的掩盖方式,正是 UnitFlag 在块内流水 + K 分段场景下的正确用法,**确实能有很好的掩盖**。
|
||
|
||
**它与高阶 API 约束的边界**:高阶 API 的 `mm.Iterate()` 一次调用内部会完成 `singleCoreK` 的全部 K 分片流水(L0C 原地累加是 MMAD 固有语义,这一步不受约束影响);高阶约束禁的只是"**跨多次 `Iterate()`** 且最后才一次 `GetTensorC()`"这种由用户在**高阶层面**组织的驻留累加。换言之:
|
||
- **底层 Mmad 级**:`unitFlag=2/3` 可自由编排 K 分段累加 → 支持;
|
||
- **高阶 Iterate 级**:`Iterate` 内部 K 分片(一次 Iterate 完成)→ 支持;`Iterate` 多次 + 末次 `GetTensorC` → 不支持。
|
||
|
||
**mat_mul_v3 是否踩高阶约束?——不踩(常规路径)**。看 `mat_mul_asw_kernel.h:140-141`:
|
||
|
||
```cpp
|
||
mm_.Iterate();
|
||
mm_.GetTensorC(cGlobal_[...], enAtomic || kIndex != 0); // 每次 Iterate 紧跟一次 GetTensorC
|
||
```
|
||
|
||
mat_mul_v3 的普通 ASWT 是 **「一次 Iterate → 一次 GetTensorC」一一配对**(即便 splitKRound 循环里也是成对出现,用 `enAtomic || kIndex!=0` 在 GM 侧原子加,而非 L0C 驻留累加)。它不是"多次 Iterate、一次 GetTensorC",因此常规路径不触碰高阶约束,UnitFlag 可用。真正会触碰的是 `intraBlockPartSum`(两 AIV 结果在 L0C 累加)这类显式 L0C 驻留累加特性——StreamK 绕道 workspace + AIV 累加正是为了避开它。
|
||
|
||
#### 3.4.2 一次 Iterate 最大支持的 K 是多少?
|
||
|
||
**先澄清概念**:官方约束里没有任何一条把 "一次 Iterate 的 K 上限" 与 UnitFlag 挂钩。**UnitFlag 约束的不是 K 的大小,而是"累加由谁、在哪个层级组织"**(见上一条)。一次 Iterate 能算多大的 K,由 **L0C 之外的资源(L1/L0A/L0B 容量、stepKa/stepKb 流水深度、baseK)** 决定,与 UnitFlag 无关:
|
||
|
||
- 单次高阶 `Iterate()` 的 K 由 `singleCoreK` 决定,`singleCoreK` 在 L1 内按 `stepKa·baseK`(A 侧)/ `stepKb·baseK`(B 侧)分多片流水喂给 Cube,**每片的中间结果在同一 L0C 上原地累加**(MMAD 固有累加语义,属"一次 Iterate 内部",不违反高阶约束)。
|
||
- 若单次 `Iterate()` 装不下整个 K(K 超过 L1/L0 一次能流水的范围),高阶层会拆成多次 `Iterate()`;此时只要不打开"L0C 驻留累加 + 末次才 GetTensorC",就不违反约束(mat_mul_v3 用 GM 原子加规避)。
|
||
|
||
**结论**:**没有"一次 Iterate 最大 K 受 UnitFlag 限制"这回事**。一次 Iterate 的 K 上限由 L1/L0 容量与 stepK 流水决定(例如 950PR 上 `maxStepK ≤ 8`、`baseK ≈ 128B/dtype`),UnitFlag 只管"算出的 512B 何时搬出",不管 K 多大。把这条约束理解成"K 不能超过某值"是误读——它限的是"累加跨几次 Iterate 的组织方式",不是"K 的绝对大小"。
|
||
|
||
#### 3.4.3 「不允许同时存在 CO1(L0C)→GM 与 A1(L1)→GM 两种搬出流水」——是什么意思?L1 能直接写回 GM 吗?
|
||
|
||
**官方原文出处**:同上两篇《Matmul 高阶 API(使能/开启)UnitFlag》,约束条件第 2 条:
|
||
> 使能 UnitFlag 功能时,不支持算子内同时存在 **CO1(L0C) 搬出到 Global Memory** 和 **A1(L1) 搬出到 Global Memory** 的两种流水。
|
||
|
||
**这条的字面意思**:UnitFlag 开启时,同一个算子里不能**同时**出现两类搬出流水——一类是 L0C→GM(结果搬出,走 Fixpipe),另一类是 L1(A1)→GM(从 L1 直接把数据写回 GM)。即搬出通路的"源头"必须统一为 L0C,不能 L0C 和 L1 混着当搬出源。
|
||
|
||
**「L1 能直接写回 GM 吗?」——分架构看,这正是关键点**:
|
||
|
||
- **在 950PR(351x 架构)上:不能。** 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 用 MDL(GetMDLConfig)→ **满足** |
|
||
| 不能同时有 CO1→GM 与 A1→GM 两种搬出 | 搬出源必须唯一为 L0C | 351x 已删 L1→GM 通路 → **自动满足,无实质限制** |
|
||
| L0C 累加时不支持多次 Iterate 一次输出 | 高阶层禁止"跨多次 Iterate 的 L0C 驻留累加 + 末次才搬出"(底层 Mmad 级 unitFlag=2/3 仍支持 K 分段累加) | mat_mul_v3 常规路径是 Iterate/GetTensorC 一一配对 → **不触碰,UnitFlag 可用**;仅显式 L0C 驻留累加(如 intraBlockPartSum)才触碰 |
|
||
| MTE2 Bound 已掩盖时收益小 | Fixpipe 已被搬运流水掩盖时无需再开 | 这是**收益**问题不是**正确性**问题,不影响可用性 |
|
||
|
||
**由此可得更精确的判断**:mat_mul_v3 在 950PR 上常规路径(含普通 ASWT、A/B 全载、以及把 K 分片放到 GM workspace 由 AIV 累加的 StreamK)**都满足 UnitFlag 的可用条件且默认已开启**;高阶 API 真正排除的是"**多次 Iterate + 末次一次 GetTensorC**"的组织方式——而 mat_mul_v3 用"GM 原子加 / workspace + AIV 累加"规避了它。这支撑 §3.3 的结论:**在 950PR 上 UnitFlag 的合法域覆盖了 mat_mul_v3 的全部主路径,L0C DB 的补位空间被压缩得很小**。
|
||
|
||
---
|
||
|
||
### 3.5 两个层级的澄清:为什么"高阶约束说不能 K 分段一次输出",而"底层文档却举例 K 分段累加"?
|
||
|
||
这是最容易混淆、也最关键的一点,单独澄清。
|
||
|
||
**两个层级的对象不同**:
|
||
|
||
| 层级 | 编程接口 | UnitFlag 的控制量 | 对 K 分段累加的支持 |
|
||
|---|---|---|---|
|
||
| 基础 API(指令级) | `Mmad` / `Copy`/`Fixpipe` 指令 | 指令参数 `unitFlag = 2/3`,逐 512B 块控标志位 | **支持**:前 n−1 次 Mmad 设 2(保持占用、续写累加),末次 Mmad 设 3(释放给 Fixpipe),官方明确给出 K 分 8 次迭代的实例 |
|
||
| 高阶 API(对象级) | `Matmul.Iterate()` / `GetTensorC()` | `MatmulConfig.enUnitFlag = true`,由库内部统一编排 | **受限**:`Iterate` 内部的 K 分片流水(一次 Iterate 完成)支持;但"**多次 Iterate + 末次一次 GetTensorC**"这种高阶层组织的驻留累加不支持 |
|
||
|
||
**为什么高阶层反而受限**:高阶 `Matmul` 对象把"K 分片、L0C 驻留、何时搬出"都封装在 `Iterate()/GetTensorC()` 内部,UnitFlag 的标志位编排也由库统一管理。当用户用高阶接口做"多次 Iterate 累加 + 一次 GetTensorC"时,库无法在一次 `GetTensorC` 里反推出前面若干次 Iterate 各自该用什么 unitFlag 时序,于是干脆禁止这种组合。而**基础 API 层把 unitFlag 的控制权直接交给用户**,用户可以自己排"前 n−1 次设 2、末次设 3",所以能做更灵活的 K 分段累加流水。
|
||
|
||
**对用户设想的直接回答**:你描述的"以 16×K×16 为粒度,每段算完立即让 Fixpipe 搬出对应 16×16 到 GM、同时开始下一段计算"——**在基础 API 层完全成立,且就是官方推荐用法**(前段 unitFlag=2 保持占用、末段 unitFlag=3 释放)。这种"K 分段 + 边算边搬"能同时做到:① 块内 512B 流水掩盖;② K 分段累加。它比单纯 L0C DB 更省 L0C 容量、掩盖更细。
|
||
|
||
**但要强调一个前提**:这种灵活编排走的是**基础 API(Mmad/Copy + 手写 unitFlag=2/3)**,不是 mat_mul_v3 用的高阶 `Matmul` 对象。mat_mul_v3 用高阶 API 换来的是开发效率与正确性保证,代价就是受高阶约束(不能多次 Iterate 一次输出)。如果要做你说的这种极致 K 分段流水,需要下沉到基础 API 层手写 Mmad/Copy 序列——这正是 catlass(类 CUTLASS 模板库)一层在做的事。
|
||
|
||
**对"UnitFlag 能否完全取代 L0C DB"的最终修正**:
|
||
|
||
- 在 **mat_mul_v3(高阶 API)现状**下:主路径默认开 UnitFlag,K 一次 Iterate 完成,Fixpipe 已被块内 512B 流水掩盖,**L0C DB 价值很小**(且 256×256 时本就开不了 DB)。DB 仅在 base 块被迫做小(256×128 等)时偶发有用。
|
||
- 在 **基础 API / catlass 层**:UnitFlag 用 unitFlag=2/3 可覆盖 K 分段累加场景,掩盖能力进一步逼近"完全取代 DB"。
|
||
- **仍不能完全取代的根本原因**:不在"K 能不能分段"(底层能),而在于 **UnitFlag 依赖 L0C 块被"写满即搬",它服务的是"计算结果要尽快流出 L0C"的场景**;而 L0C DB 的本质是"**用双倍 L0C 容量换时间上的重叠**",它服务的是"想让 base 块更大、减少重复读"的场景。当 L0C 容量成为 tiling 瓶颈(想把 baseM·baseN 做大但放不下两份)时,UnitFlag(不占额内容量)反而比 DB 更优——这一层上 UnitFlag 甚至优于 DB。只有当场景落到 UnitFlag 高阶约束之外(如必须用多次 Iterate 驻留累加)时,DB 才成为唯一选择。
|
||
|
||
**一句话总结**:在 950PR + mat_mul_v3 的语境里,**UnitFlag 已经基本取代 L0C DB 作为 Fixpipe 掩盖的主手段**;DB 退化为"高阶 API 约束之外(多次 Iterate 驻留累加)"和"L0C 装得下两份、想换更大 base 块"这两类边角场景的兜底。
|
||
|
||
---
|
||
|
||
## 4. 对本项目性能建模与最优方案的直接推论
|
||
|
||
1. **建模时 Fixpipe 项要分两种掩盖**:在性能模型 `T_total = max(T_MMAD, T_MTE2, T_MTE1, T_FIXPIPE)` 中,`T_FIXPIPE` 的实际暴露量应写成
|
||
- 开 UnitFlag(合法域内):`T_FIXPIPE_exposed ≈ tail`(512B 尾开销)→ 基本可以忽略,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 主导)。
|
||
|
||
---
|
||
|
||
## 附录:关键证据索引
|
||
|
||
| 证据 | 位置 |
|
||
|---|---|
|
||
| 高阶 API UnitFlag 约束(在线) | 昇腾社区官网 → CANN → 算子实践参考 → 优秀实践 → Matmul 性能调优案例 → 《Matmul 高阶 API 开启 UnitFlag》(社区版)/《Matmul 高阶 API 使能 UnitFlag》(商用版)"约束条件"一节 |
|
||
| 高阶 API UnitFlag 约束(本地) | `昇腾NPU知识库/CANN社区版9.2.0-beta.1/01_AscendC算子开发/292_..._开启UnitFlag.md`(第15-21行)、`昇腾NPU知识库/CANN商用版9.0.0/01_AscendC算子开发/203_..._使能UnitFlag.md`(第17/21/23行,表述最完整) |
|
||
| **基础 API UnitFlag 机制(unitFlag=2/3、K 分段累加实例,在线)** | 昇腾社区官网 → CANN → API 参考 → Ascend C API → SIMD API → 基础 API → 矩阵计算(Tensor API)→ Mmad 计算关键特性说明 → **《UnitFlag》**(用户所给链接:hiascend.com/.../Mmad计算关键特性说明/UnitFlag.md) |
|
||
| 基础 API UnitFlag 机制(本地缓存) | `昇腾NPU/.tmp/unitflag_raw2.txt`(已抓取的页面正文,含 128×1024 八次迭代实例原文) |
|
||
| 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` |
|
||
| 351x 删除 L1→GM / GM→L0 直达通路 | `昇腾NPU知识库/00_硬件/昇腾950_NPU架构白皮书.txt`(CV融合/数据通路章节);同仓 `昇腾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-259`、`mat_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` |
|