CANN 组织链接: https://atomgit.com/cann
GE 仓库链接: https://atomgit.com/cann/ge


在 CANN 全栈体系中,Graph Engine (GE) 扮演着“高级编译器”的角色。它位于深度学习框架(如 PyTorch/TensorFlow)与底层运行时(Runtime)之间,负责将用户定义的计算逻辑(计算图)转化为硬件可执行的指令序列。GE 的核心价值在于其强大的图级优化能力、静态内存规划以及对异构计算资源的自动化编排。本文将深入剖析 GE 的内部流水线,揭示其如何通过六大核心阶段将模型推理性能推向极致。

1. 前端接入与中间表示(IR)构建

GE 的第一道工序是“去框架化”。不同的深度学习框架拥有不同的算子定义与数据结构,GE 通过统一的中间表示(Intermediate Representation, IR)实现了上层框架与底层硬件的解耦。

1.1 算子原型的多态映射

GE 的 Parser 模块负责加载原始模型(如 ONNX 或 Protobuf)。

  • Schema 校验:根据算子原型定义(Operator Prototype),检查输入输出张量的维度、数据类型是否合法。
  • 属性推导:将框架特有的算子属性(如 Pad 模式、Dilation 系数)映射为 GE 内部图节点(Node)的标准属性,确保语义的一致性。

1.2 计算图的规范化(Normalization)

原始计算图往往包含冗余节点或非标准连接。

  • 控制边补全:为了保证执行顺序,GE 会在有数据依赖但无显式连接的节点间插入控制依赖边(Control Edge)。
  • 子图内联:对于定义为函数的子图(Subgraph),GE 支持将其内联展开到主图中,以便进行全局优化。

1.3 形状推导(Shape Inference)

在静态图编译阶段,GE 必须确定所有中间张量的形状。

  • 未知维度处理:对于动态 Shape 场景,GE 引入了 Placeholder 机制,并构建 Shape 传递链,推导每个节点的输出范围(Range),为后续的内存分配提供依据。

2. 图级优化流水线(Optimizer Pipeline)

一旦计算图被构建为 GE 的内部数据结构,Pass Manager 就会接管流程。它依次执行数十个优化 Pass,对计算图进行数学等价变换。

2.1 算术简化与常量折叠

  • 常量传播:对于输入全为常量的子图(如 Add(Const_A, Const_B)),GE 会在编译期直接计算出结果,并用一个新的 Const 节点替换原子图。
  • 代数化简:自动识别冗余计算,例如 x * 1、x + 0 或 Dropout(inference_mode),直接移除无用节点(Identity Removal)。

2.2 数据布局转换(Layout Conversion)

NPU 的 Cube 单元偏好分形格式(如 NC1HWC0),而框架通常输出 NCHW。

  • 格式插入:GE 自动在计算路径上插入 TransData 节点,将数据转换为硬件友好的格式。
  • 格式流转:通过全图分析,尽可能让数据在大部分时间内保持分形格式,减少不必要的 TransData 转换次数,降低数据搬运开销。

2.3 算子实现选择

同一个算子可能有多种实现(Kernel),对应不同的精度或性能特征。

  • Cost Model 驱动:GE 根据当前 Tensor 的 Shape 和数据类型,查询算子信息库(Ops Info),选择理论算力最高或内存占用最小的 Kernel 实现。

3. 高级算子融合(UB Fusion)

算子融合是提升 NPU 性能的关键。GE 通过模式匹配(Pattern Matching),将多个小算子“捏”成一个大算子,从而利用片上统一缓冲区(Unified Buffer, UB)进行数据交换,避免频繁访问片外 HBM。

3.1 融合模式匹配引擎

GE 内置了多种融合策略(Pass):

  • UB 融合:将 Conv + BiasAdd + ReLU 融合为 Conv2D 的单次调用。
  • ElemWise 融合:将连续的逐元素操作(如 Add -> Mul -> Sigmoid)合并为一个 Kernel。

3.2 垂直与水平融合

  • 垂直融合:沿着数据流方向合并节点,减少中间结果的回写。
  • 水平融合:对于输入相同、操作类型相同的并列节点(如多头注意力机制中的多个 MatMul),GE 将其合并为一个大算子,提升指令并行度。

3.3 范围融合(Scope Fusion)

针对复杂的特定结构(如 LSTM Cell 或 Transformer Block),GE 支持基于 Scope 的大颗粒度融合,将数十个节点替换为一个高性能的自定义算子库调用。

4. 静态内存规划(Memory Allocation)

不同于运行时的动态分配,GE 在编译阶段就完成了内存规划,实现了显存的“零碎片”管理。

4.1 活跃度分析(Liveness Analysis)

GE 对计算图进行拓扑排序,分析每个 Tensor 的生命周期(创建时间点与最后一次使用时间点)。

  • 虚拟地址分配:构建虚拟内存空间,为每个 Tensor 分配一个相对于基地址的偏移量(Offset)。

4.2 内存复用(Memory Reuse)

  • Block 复用:如果 Tensor A 的生命周期结束,Tensor B 才开始,且 B 的大小小于等于 A,则 B 可以复用 A 的内存空间。
  • 拓扑着色:GE 使用图着色算法解决内存复用冲突问题,旨在最小化总的内存占用峰值(Peak Memory)。

4.3 权重与常量管理

对于权重(Weight)和常量(Const),GE 将其打包并在模型加载时一次性搬运至 Device 侧常驻内存,避免推理过程中的 PCIe 带宽占用。

5. 流分配与并发控制

为了压榨 NPU 的多流并发能力,GE 负责将计算图拆解为并行任务。

5.1 逻辑流(Logic Stream)划分

GE 分析图中节点的依赖关系,识别出可以并行执行的子图分支。

  • Stream ID 分配:为每个节点打上 Stream 标签。互不依赖的分支(如 Inception 模块的多个路径)被分配到不同的 Stream。

5.2 事件(Event)同步插入

当不同 Stream 之间存在数据依赖时,GE 自动插入 SendEvent 和 RecvEvent 指令。

  • 粗粒度同步:确保生产者 Stream 完成数据写入后,消费者 Stream 才能开始读取。

5.3 引擎指派(Engine Assignment)

计算图中可能包含 CPU 算子(如特定预处理)和 NPU 算子。GE 负责标记每个节点的执行引擎(AICore, AICpu, HostCpu),并插入必要的数据拷贝节点(MemcpyAsync)以跨越不同的硬件域。

6. 任务生成与模型序列化

编译的最后阶段是将优化后的图序列化为离线模型(Offline Model, OM)。

6.1 Task Info 生成

GE 遍历最终的优化图,为每个节点生成任务描述符(Task Descriptor)。

  • 参数固化:将算子的 Tiling 参数、Kernel Bin 地址、输入输出内存偏移量写入 Task 结构体。

6.2 离线模型构建

生成的 OM 文件是一个自包含的二进制包,包含:

  • Model Header:版本信息、总内存大小、输入输出节点信息。
  • Partition Table:不同子图的入口地址。
  • Instruction Stream:序列化后的硬件指令流。

6.3 内部优化 Pass 的代码结构示例

为了展示 GE 内部是如何通过 Pass 机制修改计算图的,以下是一个简化的 C++ 类结构,模拟了一个“去除无用节点”的优化 Pass 实现逻辑。

namespace cann {
namespace ge {

// 优化 Pass 基类
class GraphOptimizerPass {
public:
    virtual Status Run(ComputeGraph& graph) = 0;
};

// 具体的优化 Pass:移除 Identity 节点 (y = x)
// 这种节点在框架转换过程中常见,但在硬件执行时是多余的
class RemoveIdentityPass : public GraphOptimizerPass {
public:
    Status Run(ComputeGraph& graph) override {
        bool changed = false;
      
        // 遍历图中所有节点
        for (auto& node : graph.GetAllNodes()) {
            if (node->GetType() != "Identity") {
                continue;
            }

            // 获取 Identity 节点的输入和输出锚点
            auto in_anchor = node->GetInDataAnchor(0);
            auto out_anchor = node->GetOutDataAnchor(0);
          
            // 获取前驱节点 (Producer)
            auto peer_out_anchor = in_anchor->GetPeerOutAnchor();
            auto producer_node = peer_out_anchor->GetOwnerNode();

            // 遍历 Identity 的所有后继节点 (Consumers)
            auto consumer_anchors = out_anchor->GetPeerInAnchors();
            for (auto& consumer_in_anchor : consumer_anchors) {
                // 核心操作:断开 Identity,直接连接 Producer 和 Consumer
                // 1. 解除 Identity -> Consumer 的连接
                GraphUtils::RemoveEdge(out_anchor, consumer_in_anchor);
              
                // 2. 建立 Producer -> Consumer 的连接
                GraphUtils::AddEdge(peer_out_anchor, consumer_in_anchor);
            }

            // 从图中彻底移除 Identity 节点对象
            graph.RemoveNode(node);
            changed = true;
        }
      
        return changed ? Status::SUCCESS : Status::NO_CHANGE;
    }
};

// Pass 管理器,负责调度优化序列
class PassManager {
    std::vector<std::shared_ptr<GraphOptimizerPass>> passes_;
public:
    void AddPass(std::shared_ptr<GraphOptimizerPass> pass) {
        passes_.push_back(pass);
    }
  
    Status Run(ComputeGraph& graph) {
        for (auto& pass : passes_) {
            pass->Run(graph);
            // 某些 Pass 可能需要重新校验图的完整性
            graph.Verify(); 
        }
        return Status::SUCCESS;
    }
};

} // namespace ge
} // namespace cann

通过这种链式的 Pass 处理机制,GE 能够将一个原始、粗糙的计算图,逐步打磨成一个精简、高效、内存紧凑的硬件执行方案,体现了从算法逻辑到物理实现的完美跨越。

更多推荐