适用范围: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>(); 

对比 #ifdefif 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 需求、头开销消除
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐