训练营简介 2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机,平板、开发板等大奖。

报名链接:https://www.hiascend.com/developer/activities/cann20252#cann-camp-2502-intro

前言

在算子开发(Kernel)和图构建(Graph Build)之间,还存在一个神秘的中间层——图优化(Graph Optimization)

当你把模型交给 GE(Graph Engine)时,GE 不会傻乎乎地照单全收。它会启动一系列的 Pass(优化趟),像安检扫描一样扫描你的计算图:

  • “哎,这里有三个算子可以合并!”

  • “这块内存可以复用!”

  • “这个数据格式转来转去太多余了,删掉!”

虽然 GE 内置了很多通用 Pass,但在面对特定的自定义算子或新兴模型架构(如 MoE, Mamba)时,内置规则可能失效。这时,你需要自己写一个 Custom Pass,告诉编译器:“如果你看到 A->B->C 这样的结构,请把它们换成 D。”

本期我们将深入 GE 的 Scope Fusion Pass 机制,手写一个自定义的图融合插件。

一、 核心图解:图上的“连连看”

图融合的本质就是 Pattern Matching(模式匹配)Replacement(替换)

二、 融合利器:Scope Fusion Pass

昇腾提供了多种 Pass 接口(如 Graph Fusion, UB Fusion),其中 Scope Fusion Pass 是最灵活的一种。它基于 TensorFlow/ONNX 的 Scope(命名空间)机制来识别子图。

2.1 融合流程

  1. Define Pattern:定义你要找什么样的“猎物”(子图结构)。

  2. Match:GE 自动在全图中寻找匹配的子图。

  3. Fuse:触发回调函数,你在这里修改图结构(断开旧连线,插入新节点)。

三、 实战:编写一个 "Add + Relu" 融合 Pass

假设我们想把所有的 Add 后面紧接 Relu 的结构,融合为一个 AddRelu 算子(我们在第十一期写过这个算子)。

3.1 头文件与基类

我们需要继承 ScopeFusionPass 类。

#include "register/scope/scope_fusion_pass_register.h"

namespace ge {
class AddReluFusionPass : public ScopeFusionPass {
protected:
    // 1. 定义模式:长什么样?
    std::vector<ScopeFusionPatterns> DefinePatterns() override;
    
    // 2. 定义名称
    std::string PassName() override { return "AddReluFusionPass"; }
    
    // 3. 执行融合:怎么改?
    Status LastMatchScopesAndFusion(
        std::vector<std::vector<Scope *>> &scopes,
        std::vector<Scope *> &fused_scopes) override;
};
}

3.2 定义匹配模式 (Pattern)

我们需要告诉 GE:先找一个 Add,再找一个 Relu,且它们之间相连。

std::vector<ScopeFusionPatterns> AddReluFusionPass::DefinePatterns() {
    std::vector<ScopeFusionPatterns> patterns;
    ScopeFusionPatterns pattern;
    
    // 定义节点类型
    pattern.AddNodeSpec("add_node", "Add")   // 找 Type 为 Add 的节点,别名叫 add_node
           .AddNodeSpec("relu_node", "Relu"); // 找 Type 为 Relu 的节点
           
    // 定义连接关系:add_node -> relu_node
    // 注意:这里只是简单的线性连接,Scope Pass 其实更擅长基于 Scope Name 的层级匹配
    // 如果是基于拓扑结构的匹配,通常使用 "GraphFusionPass"
    // 但 Scope Pass 对于 TF 这种 Scope 层级清晰的模型非常有效
    
    patterns.push_back(pattern);
    return patterns;
}

注:实际的 Scope Pass 通常基于 Scope(如 Bert/Layer1/Attention/...)进行匹配。如果只是纯拓扑匹配,CANN 还提供了 GraphFusionPass。为了简化理解,这里展示概念逻辑。

3.3 执行融合逻辑 (Fuse)

这是“手术”最关键的一步。

Status AddReluFusionPass::LastMatchScopesAndFusion(
    std::vector<std::vector<Scope *>> &scopes,
    std::vector<Scope *> &fused_scopes) {
    
    // scopes 包含了所有匹配到的子图列表
    for (auto &subgraph : scopes) {
        // 1. 创建新节点 (AddRelu)
        // 使用 GenOperator 构造一个 IR
        auto fused_op = ge::op::AddRelu("FusedAddRelu_" + std::to_string(index));
        
        // 2. 迁移属性
        // 把旧 Add 节点的输入连到新节点上
        // 把旧 Relu 节点的输出连到新节点上
        
        // 3. 替换图中的节点
        // GE 提供了 InsertNode, RemoveNode 等接口
        // 这是一个复杂的图操作过程,需要处理 Edge 的重连
        
        // 4. 注册新 Scope
        // ...
    }
    return SUCCESS;
}

3.4 注册 Pass

最后,使用宏将 Pass 注册到 GE 系统中。

// 注册到内置的 "System Scope Fusion" 阶段
REGISTER_SCOPE_FUSION_PASS("AddReluFusionPass", AddReluFusionPass);

四、 编译与生效

自定义 Pass 通常被编译为 .so 动态库,放在 ATC 工具的插件目录下。

  1. 编译:使用 CMake 编译成 libadd_relu_fusion.so

  2. 部署:放入 ${ASCEND_HOME}/opp/framework/custom/tensorflow/ (路径视具体框架而定)。

  3. 运行:当你再次运行 ATC 转换模型时,日志里会出现 [INFO] AddReluFusionPass run success,说明你的手术刀生效了!

五、 总结

自定义 Pass 是 AI 编译器开发的皇冠明珠。

  1. 价值:它能解决通用优化解决不了的 Corner Case,带来 20%~50% 的额外性能收益。

  2. 难度:需要对图论(Graph Theory)和 GE 的 IR 接口非常熟悉。

  3. 场景:主要用于 大算子融合(Op Fusion)布局调整(Layout Transform)

掌握了这一招,你就不仅仅是在“写”算子,而是在“改”编译器。你拥有了上帝视角,可以重塑整个计算图。

Logo

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

更多推荐