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

在复杂的异构计算软件栈中,如果说前端框架(如 PyTorch/TensorFlow)是提出需求的“客户”,而运行时(Runtime)是执行任务的“工兵”,那么 GE (Graph Engine) 就是连接这两者、运筹帷幄的“总工程师”。ge 仓库是整个 AI 编译流程的核心与大脑,它负责接收上层框架传递的原始计算图,并将其转化为一幅经过深度优化、适配底层硬件的高效执行蓝图。

GE 的使命远不止于简单的图遍历。它是一个集图表示、分析、优化、划分和代码生成于一体的复杂系统。本文将带您深入 GE 的内部,揭示其作为编译核心的六大关键技术支柱。

一、 图的表示与生命周期管理

GE 的一切工作都围绕着其核心数据结构——ge::Graph 展开。这张图并非简单的节点与边的集合,而是一个承载了丰富语义信息的对象。GE 为上层应用提供了一套完整的 C++ API,用于构建、操作和销毁这张图。

1.1 前端适配层的接口

GE 通过 ge::Initializege::Finalize 等接口来管理其全局资源和会话状态。前端适配器(如 torch_npu)会调用 GE API 将框架层的算子逐一转换为 ge::Operator 对象,并构建它们之间的依赖关系。

1.2 基于 Metadef 的统一语言

GE 本身不定义算子的具体形态,而是遵循 metadef 仓库中定义的元数据协议。无论是 TensorDesc 的形状(Shape)与格式(Format),还是算子(Operator)的属性(Attribute),都使用这套统一的“语言”进行描述,确保了信息在整个编译栈中无损传递。

1.3 图的校验与完整性检查

在图构建的初期,GE 就会进行初步的静态检查。它会利用算子原型库(Op Prototype)中定义的 Verify 函数,检查每个节点的输入数量、数据类型是否匹配,以及属性值是否合法,从而在编译的早期阶段就捕获大量构图错误。

二、 图优化引擎:Graph Pass 框架

原始计算图往往是冗余且低效的。GE 的核心价值在于其强大的图优化引擎,该引擎由一系列被称为 Graph Pass 的优化阶段组成。每个 Pass 都是一个独立的模块,专注于一种特定的图转换。

  1. 常量折叠(Constant Folding):在编译期间预先计算图中所有由常量构成的子图。例如,一个由多个常量 Tensor 进行的加法操作,会被直接计算出结果,替换为一个单独的常量节点,从而减少运行时的计算开销。
  2. 算子融合(Operator Fusion):这是 GE 最关键的优化之一。它将多个访存密集型的小算子合并成一个大的、计算密集型的融合算子。例如,一个典型的 Conv2D -> BiasAdd -> ReLU 序列会被融合成一个单一的 FusedConv2D 算子。这极大地减少了数据在片外高带宽内存(HBM)和片上缓存之间的往返次数,是突破“内存墙”的关键。
  3. 内存优化(Memory Optimization):通过对图进行活性分析(Liveness Analysis),GE 能够精确计算出每个 Tensor 的生命周期。基于此,它可以实现一个高效的内存复用方案,让生命周期不重叠的 Tensor 共享同一块显存空间,从而显著降低模型的峰值显存占用。

三、 数据布局优化与格式转换

不同的计算单元对数据的内存布局有着不同的偏好。例如,通用的 CPU 倾向于处理 NCHW 格式的张量,而专用的矩阵计算单元为了最大化访存带宽,可能需要 NC1HWC0 或其他更复杂的 Fractal 格式。

GE 在图优化阶段会自动进行数据布局的决策与转换:

  • 格式推导:GE 会根据算子原型库中的信息,推断出每个算子最优的输入输出格式。
  • 插入转排算子:当一个算子的输出格式与下一个算子的输入格式不匹配时,GE 会在它们之间自动插入一个 TransData(格式转换)算子。
  • 消除冗余转换:GE 的优化 Pass 还会进一步分析,合并或消除连续的、可抵消的格式转换操作,避免不必要的开销。

四、 算子选择与代码生成(Kernel Selection & Generation)

经过优化的图中的每个节点,仍然是一个逻辑概念。GE 的下一步是为每个节点找到能够在硬件上执行的二进制代码,即 Kernel

4.1 多源算子库匹配

GE 维护着一个多来源的算子库系统。当需要为一个节点选择 Kernel 时,它会按照优先级进行查找:

  1. 预编译算子库(Pre-compiled Library):对于常见的、性能要求极高的算子(如 MatMul, Conv2D),通常会有手写的、经过高度优化的 Kernel 存在于一个预编译库中。
  2. DSL 自动生成:对于大量长尾算子,GE 会调用一个基于领域特定语言(DSL)的算子生成器(如 TBE),根据算子的数学描述实时生成并编译出高效的 Kernel 代码。

4.2 AOT 与 JIT 的协同

GE 支持两种编译模式。在 AOT(Ahead-of-Time)模式下,所有 Kernel 的选择和编译都在模型离线构建时完成。而在 JIT(Just-in-Time)模式下,特别是在处理动态 Shape 时,Kernel 的选择和编译可能会被推迟到模型实际执行时,根据真实的输入尺寸来完成。

五、 从逻辑图到执行图的下沉(Lowering)

当所有优化完成、所有 Kernel 选定后,GE 的最后一步是生成最终的执行计划,这个过程称为“下沉(Lowering)”。

它将逻辑上的 ge::Graph 转换为一个或多个物理执行图,其中包含了详细的执行指令。这些指令被打包成一个个 Task,并安排到不同的硬件流(Stream)上以实现并行执行。一个 Task 的定义可以被概念化为如下结构,它包含了 Runtime 执行所需的所有信息:

// 概念性结构定义:任务描述符
// GE 将优化后的图节点转换为这样的结构,供 Runtime 解析执行
struct TaskDescriptor {
    // 任务的唯一标识符
    uint64_t task_id;

    // 任务类型:如 Kernel 启动、内存拷贝、事件同步等
    enum class TaskType {
        KERNEL_LAUNCH,
        MEMCPY_H2D,
        MEMCPY_D2D,
        EVENT_RECORD
    } type;
  
    // 执行此任务的硬件流 ID
    uint32_t stream_id;

    // 如果是 Kernel 启动任务
    struct {
        void* func_stub;      // 指向 Kernel 函数的入口地址
        void* kernel_args;    // 指向打包好的 Kernel 参数块
        uint32_t block_dim;   // 硬件执行的块维度
    } kernel_info;

    // 任务依赖关系,指向前驱任务的 ID 列表
    std::vector<uint64_t> dependencies;
};

最终,这些 Task 序列和相关的权重数据被序列化,生成一个可被 Runtime 直接加载执行的离线模型文件(.om 文件)。

六、 分布式图的切分与通信算子插入

在多设备、多节点的训练场景下,GE 承担着图切分(Graph Partitioning)和通信优化的重任。

6.1 策略驱动的图切分

GE 会根据用户指定的并行策略(如数据并行、模型并行、流水线并行),自动对原始的单机计算图进行切分。它通过复杂的算法分析计算量、显存占用和通信开销,找到最优的“切割点”。

6.2 通信算子自动插入

在图的切割边缘,GE 会自动插入相应的集合通信(Collective Communication)算子。

  • 对于数据并行,它会在梯度计算后插入 HcomAllReduce 算子来同步梯度。
  • 对于模型并行或流水线并行,它会插入 HcomSendHcomReceive 算子来传递中间的激活值。

这些通信算子直接对接底层的通信库(如 HCCL),确保了跨设备数据传输的高效性,使得开发者可以像编写单机代码一样进行大规模分布式训练。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐