[AI][昇腾950] Scalar 性能优化
适用范围:David 系列DaVinci AICore
核心目标:消除 Scalar Bound,降低头开销,提升 MainScalar 发射效率
本文视角:从 C++ 语言特征、硬件 Cache 控制、切分算法、模板化 四个维度系统阐述优化方法
0. 四维优化框架总览
Scalar Bound 消除
│
┌──────────────┬───────┴───────┬──────────────┐
│ │ │ │
① C++ 语言特征 ② 硬件Cache控制 ③ 切分算法 ④ 模板化
(编译期能做什么) (硬件给什么手段) (数据怎么分) (代码怎么组织)
│ │ │ │
· 常量折叠 · icache 预取 · Tile 适配 · constexpr 模板
· 循环展开 · dcache preload · Double Buffer · if-constexpr 分发
· 分支消除 · L2 const · L2 共享切分 · SFINAE/Concept
· 内联与vf_call · full cacheline · K 维切分 · 策略类注入
· 协程状态机 · 延迟Copy · 静态tiling · super kernel 模板
四个维度并非孤立,而是协同:切分算法决定数据布局 → 模板化把布局编进类型 → C++ 特征在编译期折叠常量 → 硬件 Cache 机制保证数据按时到位。
1. Scalar Bound 现象与根因(快速回顾)
1.1 什么是 Scalar Bound
MainScalar 是 AICore 的串行控制单元,负责取指、译码、参数计算、指令发射、流程控制。当 MainScalar 的指令吞吐跟不上 MTE/Vector/Cube 的执行能力时,计算单元空闲等待,称为 Scalar Bound。
理想(无 Scalar Bound):
Scalar: [cfg][cfg]....[cfg].... ← 轻量,提前配置好
MTE: [════搬运════][═══搬运═══] ← 持续满载
Cube: [═══计算═══][═══计算═══] ← 持续满载
Scalar Bound:
Scalar: [cfg][cfg][cfg][cfg][cfg][cfg]... ← 串行配置成瓶颈
MTE: [搬运]....[搬运].... ← 空闲等待配置
Cube: [算]....[算].... ← 空闲等待数据/配置
▲
计算单元空转,性能浪费
1.2 根因分类与四维归属
| 根因类别 | 具体原因 | 对应优化维度 |
|---|---|---|
| A. 标量计算过重 | 地址计算、循环计数、tiling 运算过重 | ③ 切分算法 + ④ 模板化(静态 tiling) |
| B. 指令发射开销 | 每条 MTE/Vector/Cube 需 Scalar 配置描述符 | ① C++(内联/常量折叠)+ ④ 模板化 |
| C. 同步等待 | wait_flag 阻塞 Scalar 流水 | ③ 切分算法(Double Buffer) |
| D. 控制流开销 | 分支/循环/函数调用打断流水,icache miss | ① C++(分支消除/循环展开)+ ② Cache |
| E. 队列满 | mte2/3/vector 等队列满阻塞发射 | ③ 切分算法(tile 平衡);指令乱序 |
| F. 头开销 | 算子启动调度→取指→预热 | ② Cache(icache 预取)+ ④ 模板化(super kernel) |
| G. 串行依赖 | MainScalar 串行调用 vf_call,无法并发 | ① C++(vf_call 融合)+ ④ 模板化 |
1.3 自检:是否处于 Scalar Bound
| 现象 | 判定方法 |
|---|---|
| Cube/Vector 利用率低,MTE 利用率也低 | profiling 三条流水均未打满 → 大概率 Scalar Bound |
| 算子 MFU 低,增大 tile 无改善 | tile 增大本应提高计算占比,无改善说明瓶颈在控制 |
| 算子入口段(前 50 行)耗时长 | 头开销 + Scalar 配置集中 |
| 反汇编 scalar 段指令占比高 | scalar 流水指令数 / 总指令数 > 30% |
2. 维度一:C++ 语言特征优化
核心思想:把能在编译期算清楚的,绝不留到运行期让 MainScalar 算。
2.1 常量折叠与常量传播
问题:运行期传入的参数(如 tileM、tileK、stride)每次都需 Scalar 做地址运算 addr = base + i*stride。
C++ 手段:用 constexpr / 模板非类型参数把参数固化到类型中,触发编译器常量折叠。
// ❌ 运行期参数:每次循环 Scalar 都要算 addr
void kernel(int tileM, int tileK, int stride) {
for (int i = 0; i < tileM; ++i) {
auto addr = base + i * stride; // Scalar 运行期乘法
// ...
}
}
// ✅ 编译期常量:stride 变成立即数,地址生成用 LEA
template <int TileM, int TileK>
void kernel() {
constexpr int stride = TileK * sizeof(half); // 编译期算好
for (int i = 0; i < TileM; ++i) {
auto addr = base + i * stride; // 编译为 i * immediate
// ...
}
}
收益:
- 消除 Scalar 运行期乘除
2.2 循环展开与展开因子
问题:循环计数器维护、循环条件判断、分支跳转打断 Scalar 流水,导致 icache miss。
C++ 手段:#pragma unroll 或模板递归展开。
// ❌ 动态循环:Scalar 需维护循环计数 + 分支
for (int k = 0; k < K; k += tileK) {
mmad(c, a + k, b + k);
}
// ✅ 编译期展开:Scalar 只发 N 条 mmad,无循环开销
template <int K, int TileK>
struct UnrolledMmad {
static_assert(K % TileK == 0);
static constexpr int N = K / TileK;
template <int Idx = 0>
static inline void run(c, a, b) {
if constexpr (Idx < N) {
mmad(c, a + Idx * TileK, b + Idx * TileK);
run<Idx + 1>(c, a, b);
}
}
};
UnrolledMmad<256, 32>::run(c, a, b); // 展开 8 次
2.3 分支消除与 Predicate 策略
问题:if 分支产生 BB 块跳转,打断 Scalar 顺序取指,引发 icache miss
C++ 手段:用三元运算、Predicate、if constexpr(编译期消除)替代运行期分支。
// ❌ 运行期分支:产生两个 BB 块
if (remainder > 0) {
process_tail(remainder);
}
// ✅ 编译期消除:边界用模板特化处理
template <bool HasTail>
void process() {
if constexpr (HasTail) {
process_tail();
}
// HasTail=false 时,process_tail 在编译期被完全删除
}
// ⚠️ Predicate 反向策略(R13):部分场景 Predicate 反而增加运行时开销
// 编译器会对部分热路径去 Predicate。手写时不要无脑 Predicate 化,
// 需结合 profiling 判断
2.4 内联与 vf_call 融合
问题:MainScalar 串行调用 vf_call(Vector Function Call),
// ❌ 函数调用:每次 vf_call 保存现场 + 跳转
void vec_add(LocalTensor dst, LocalTensor a, LocalTensor b) { /*...*/ }
void kernel() {
vec_add(t1, a, b); // vf_call 1
vec_add(t2, c, d); // vf_call 2,串行等待
}
// ✅ 内联融合 + 函数对象:编译器合并为一条 vf_call
struct Add {
static inline __attribute__((always_inline)) void apply(
LocalTensor dst, LocalTensor a, LocalTensor b) { /*...*/ }
};
template <typename... Ops>
struct FusedVfCall {
static inline void run() {
// 编译器将多个 Ops::apply 融合为单个 vf_call,
// 对应 David FA 算子验证:VF_call 融合获 30% 性能
(Ops::apply(), ...);
}
};
FusedVfCall<Add, Add, Mul>::run();
实测收益(David FA 算子):
- 单纯转换:仅获手写 19% 性能
- 常量折叠 + 循环展开 + if 消除 + 常量 Tile + VF_call 融合:再获 30% 性能
3. 维度二:硬件 Cache 控制优化
核心思想:让数据和指令在 MainScalar 需要时已在 Cache,消除等待。
3.1 David 系列存储与 Cache 层次
┌─────────────────────────────────────────────────────┐
│ HBM (GM) ← 多核共享,高带宽高延迟 │
├─────────────────────────────────────────────────────┤
│ L2 ← Die 间共享,片上缓冲 │
├─────────────────────────────────────────────────────┤
│ L1 (LocalMemory) ← 核私有 │
│ L0A/L0B/L0C ← Cube 寄存器级 │
│ UB (256KB ) ← 核私有,Vector 工作区 │
│ RF ( thread) ← 寄存器 │
├─────────────────────────────────────────────────────┤
│ icache ← 指令缓存 │
│ dcache ← 数据缓存 │
└─────────────────────────────────────────────────────┘
3.2 Icache 优化(消除头开销与控制流 miss)
头开销主要来自 icache miss:算子启动时指令尚未进入 icache,Scalar 取指阻塞。David 头开销约 1us。
| 手段 | 硬件/软件机制 | 收益 | 优化维度联动 |
|---|---|---|---|
| L2 预取参数 | 算子启动前 SDMA 预取参数到 L2 | 消除首拍 icache miss | ④ 模板化(预取偏移常量化) |
| 指令 preload 到 L2 | 指令预加载,与数据隔离 | 减少头开销 | ③ 切分(super kernel 减少加载次数) |
| super kernel | 融合算子减少 launch 次数 | 部分消除 icache miss | ④ 模板化 |
| BB 块重排 | 编译器 R5 热路径前置 | 减少 miss | ① C++(分支消除减少 BB) |
代码示例:
// 模板化预取偏移:编译期算好预取地址
template <int ParamSize>
void prefetch_params(uintptr_t gm_addr) {
// RTS 配置 preload 大小,配合 SDMA 预取
asm_preload_l2(gm_addr, ParamSize);
}
prefetch_params<4096>(param_gm); // 常量参数触发编译器优化
3.3 Dcache 优化
| 手段 | 硬件/软件机制 | 收益 |
|---|---|---|
| dcache preload | mte2 改为 preload | 消除首拍 dcache miss |
| Full cacheline write | 算子不感知,硬件自动覆盖 | 消除 dcache write miss |
| 延迟 Copy | GM→L1 只记地址+参数,LoadData 时才真实拷贝 | 减少冗余搬运 |
延迟 Copy 详解:
传统: GM --Copy--> L1 --LoadData--> L0 --Mmad--> L0C
延迟: GM --记addr--> L1(仅记录) --LoadData时才Copy--> L0 --Mmad--> L0C
收益: 当数据无需额外转换时,A1/B1 映射为 Null,消除一次搬运
3.4 同步最小化与 Cache 一致性
| 原则 | 说明 | 与 Cache 关系 |
|---|---|---|
| 按需同步 | 只在 producer→consumer 边界插入 SetFlag/WaitFlag | 避免冗余 wait 导致 Scalar 阻塞 |
| 批量同步 | 多个独立搬运合并一个 Flag | 减少 Scalar 指令数 |
| 避免冗余 wait | 编译器/静态分析识别可消除的 wait | 释放 Scalar 流水 |
| 异步优先 | notify/wait 异步代替同步轮询 | 配合 FCM 事件驱动 |
| GM 强一致性 | GM 为强一致性访问,操作 GM 需清 Cache + 同步 | 软件 clear cache + 同步 |
4. 维度三:切分算法优化
核心思想:Tile 形状与层级容量精准匹配,减少 Scalar 切分逻辑与同步开销。
4.1 Double Buffer 隐藏搬运(根因 C 同步等待)
目标:让 MTE 搬运与 Cube/Vector 计算时间重叠,Scalar 只需一次配置多块 buffer。
传统(单 buffer):
Scalar: [cfg_DMA0][wait0][cfg_MMAD0][wait0][cfg_DMA1][wait1]...
MTE: [搬运0]............[搬运1]............
Cube: [算0]............[算1]....
问题: MTE 与 Cube 串行,Scalar 频繁 wait
Double Buffer:
Scalar: [cfg_DMA0][cfg_DMA1][wait0][cfg_MMAD0][cfg_DMA2][wait1]...
MTE: [搬运buf0][搬运buf1][搬运buf2]...
Cube: [LoadData0][MMAD0] [LoadData1][MMAD1]...
收益: MTE 与 Cube 重叠,吞吐翻倍
4.2 Tile 大小与层级容量适配
目标:tile 大小匹配内存层级容量,避免溢出导致的额外 Scalar 切分逻辑。
| 内存层级 | 容量(David) | Tile 建议约束 | 切分算法要点 |
|---|---|---|---|
| UB | 256KB | tileM×tileK×sizeof(half)×N_buffer ≤ 256KB | 算子必须动态适应 UB 变化 |
| L1 | LocalMemory | tileM×tileK ≤ L1 容量 | Cube 数据 |
| L0A/L0B | tileM×tileK ≤ L0 容量 |
// ❌ 硬编码 tile(跨代迁移会溢出 UB)
constexpr int tileM = 128;
constexpr int tileK = 64;
// ✅ 模板化动态适应 UB 容量
template <typename ArchConfig>
struct TilePolicy {
static constexpr int UB_SIZE = ArchConfig::UB_SIZE;
static constexpr int N_BUFFER = 2; // double buffer
static constexpr int DTYPE = sizeof(half);
static constexpr int tileM = 128;
// 编译期算出 tileK,确保不溢出
static constexpr int tileK = UB_SIZE / (tileM * DTYPE * N_BUFFER);
};
using _Tile = TilePolicy<Config>; // UB=256K → tileK=8
4.3 K 维切分与 L2 共享
传统 Cube 矩阵乘只在核内分 K 维。
集群内 4 核分 K(L2 buffer 共享):
Core0: C += A[K0:K1] × B[K0:K1] ─┐
Core1: C += A[K1:K2] × B[K1:K2] ─┤ 结果聚合到 L2
Core2: C += A[K2:K3] × B[K2:K3] ─┤
Core3: C += A[K3:K4] × B[K3:K4] ─┘
Scalar 优化: 每核只配自己的 K 段,无需串行处理全 K
A/B 分块通过 L2 buffer 共享,减少 HBM 搬运
约束:L2 buffer 仅集群模式可用,且不支持原子/减少操作,结果聚合需 Scalar 显式同步。
4.4 控制流简化与切分联动
切分算法本身会引入控制流(处理尾块、对齐边界),需与 ① C++ 特征联动:
| 手段 | 切分侧 | C++ 侧 |
|---|---|---|
| 分支消除 | 尾块用 Padding 对齐 | if constexpr 编译期消除 |
| 循环展开 | K 维按 tileK 整除 | 模板递归展开 |
| BB 块重排 | 热路径(大 tile)前置 | |
| 函数内联 | 小 tile 处理函数 | always_inline |
| 避免深度嵌套 | 减少切分层级 | 减少 Scalar 栈消耗 |
5. 维度四:模板化优化
核心思想:用 C++ 模板把硬件配置、Tile 策略、算子特化编进类型系统,编译期生成最优代码。
5.1 硬件代际配置模板化
// 硬件代际配置
struct Config {
static constexpr int UB_SIZE = 256 * 1024;
static constexpr int ICACHE_LINE = 512; // 512B prefetch
};
5.2 if-constexpr 编译期分发
if constexpr(C++17)在编译期消除分支,不产生 BB 块,直接解决根因 D 控制流开销:
template <bool UseL2, bool UseDoubleBuf>
void matmulImpl() {
if constexpr (UseL2) {
// UseL2 共享路径,编译期生成,
clusterMatmul();
} else {
legacyMatmul();
}
if constexpr (UseDoubleBuf) {
// 双 buffer 路径
pipelineDoubleBuf();
} else {
pipelineSingleBuf();
}
}
// 调用:模板参数决定生成哪份代码
matmulImpl<true, true>(); // 最优路径
matmulImpl<false, true>();
对比 #ifdef:if constexpr 是类型安全的、可被编译器优化(死代码消除、常量传播),而 #ifdef 是预处理阶段文本替换,无法受益于编译器分析。
5.3 策略类注入(Policy-Based Design)
将 Tile 策略、同步策略、Cache 策略抽象为策略类,通过模板注入,编译期生成特化代码:
// 策略类
struct DefaultTile {
static constexpr int M = 128, K = 32, N = 128;
};
struct LargeTile {
static constexpr int M = 256, K = 64, N = 256;
};
struct SyncMin {
static void wait() { /* 按需同步 */ }
};
struct SyncSafe {
static void wait() { /* 保守同步 */ }
};
struct CacheL15 {
static void prefetch() { /* L1.5 const cache 预取 */ }
};
struct CacheLegacy {
static void prefetch() { /* L2 预取 */ }
};
// 算子模板注入策略
template <typename TilePolicy, typename SyncPolicy, typename CachePolicy>
struct Kernel {
static void run() {
CachePolicy::prefetch();
// 用 TilePolicy::M/K/N,编译期常量
for (int i = 0; i < TilePolicy::M; ++i) { /*...*/ }
SyncPolicy::wait();
}
};
// 实例化:编译期生成 4 份特化代码,运行期零分支
using FastKernel = Kernel<LargeTile, SyncMin, CacheL15>;
using SafeKernel = Kernel<DefaultTile, SyncSafe, CacheLegacy>;
5.4 Concept(C++20)约束算子接口
C++20 Concept 替代 SFINAE,编译期约束算子模板参数,错误信息更友好,且可触发不同的编译期优化路径:
template <typename T>
concept TilePolicy = requires {
{ T::M } -> std::convertible_to<int>;
{ T::K } -> std::convertible_to<int>;
{ T::N } -> std::convertible_to<int>;
T::M > 0 && T::K > 0 && T::N > 0;
};
template <typename T>
concept HasL15 = requires { { T::HAS_L15 } -> std::same_as<bool>; };
template <TilePolicy TP, HasL15 Arch>
void optimizedMatmul() {
if constexpr (Arch::HAS_L15) {
// L1.5 路径,编译期保证 Arch 有 HAS_L15 字段
}
}
5.5 Super Kernel 模板化
目标:多个小算子融合为一个大 kernel,减少 launch 次数,消除重复头开销(对应根因 F 头开销)。
传统:
[kernel1: 头开销+计算][kernel2: 头开销+计算][kernel3: 头开销+计算]
↑ 3 次头开销 + 3 次 icache miss
super kernel:
[super_kernel: 1次头开销 + kernel1 + kernel2 + kernel3]
↑ 1 次头开销,icache 只 miss 一次
模板化实现:
// Super Kernel 模板:编译期融合多个算子
template <typename... Kernels>
struct SuperKernel {
static void run() {
// 一次性 InitBuffer(共享资源)
TPipe pipe;
// 展开:每个 Kernel 的 run 共享同一个 pipe
(Kernels::run(pipe), ...);
}
};
using FusedFA = SuperKernel<Matmul, Softmax, Matmul, Linear>;
FusedFA::run(); // 一次 launch,一次头开销
5.6 模板化与编译器优化的协同
模板化产生的特化代码更易被编译器优化:
| 模板化手段 | 触发的编译器优化 |
|---|---|
constexpr 参数 |
立即数使能、常量折叠 |
| 模板非类型参数 | Pattern 优化、LEA 优化 |
if constexpr |
死代码消除、BB 块减少 |
| 模板递归展开 | 循环展开、指令数减少 |
| Super Kernel 模板 | 减少 Sreg 需求、头开销消除 |
更多推荐




所有评论(0)