开发者
资源
[object Object]

在通算融合类算子的实现中,通信操作使用,矩阵乘计算操作使用。关于更多集合通信的内容和相关概念请参考。通算融合算子的开发过程与一般算子相同,但请注意,当前通算融合算子暂不支持,仅支持

下文将以AllGatherMatmulCustom算子(简称AllGatherMatmul)的实现为例,从算子分析、数据流分析、创建算子工程、原型定义、Tiling实现、Kernel实现、编译与运行等方面介绍通算融合算子的设计和实现流程。本样例中算子的完整代码请参见。该样例仅支持在Atlas A2 训练系列产品/Atlas A2 推理系列产品上运行。

[object Object]

算子分析是指明确算子的数学表达式、输入、输出,核函数的名称等信息。

  1. 明确算子的数学表达式及通信计算逻辑。

    AllGatherMatmul算子实现了通信和矩阵乘法的融合。算子逻辑为:对输入的通信矩阵a做AllGather通信得到Matmul计算的左矩阵,即通信结果gather_out,将gather_out和右矩阵b做Matmul运算得到输出c。对应的数学表达式为:

    [object Object]
  2. 明确输入、输出和属性。

    • a、b为源操作数,a为通信的输入矩阵,形状为[M, K];b为Matmul的右矩阵,形状为[K, N]。在样例中,M、K、N分别固定为512、5120、640。
    • gather_out为目的操作数,存放AllGather通信结果,形状为[M * rankDim, K],其中,rankDim为通信域内的卡数,在样例中固定为8。
    • c为目的操作数,存放Matmul运算结果,形状为[M * rankDim, N]。
    • 算子输入输出的数据类型为float16,为:ND。
    • group为算子属性,表示通信域名称,明确算子运行时所在的通信域。
  3. 确定核函数名称和参数。

    • 本样例中核函数命名为all_gather_matmul_custom。
    • 根据对算子输入输出的分析,确定核函数的参数aGM,bGM,cGM,gatherOutGM;aGM,bGM为输入在Global Memory上的内存地址,cGM,gatherOutGM为输出在Global Memory上的内存地址。注意,核函数的参数和单算子API调用的输入输出在命名上存在区别,原因是核函数的参数是输入输出在Global Memory上的内存地址,而单算子API调用时输入输出的类型是aclTensor,两者并不完全一致。
  4. 确定算子实现所需接口。

    • 算子涉及AllGather通信,查看Ascend C API参考中的通信相关接口,需要使用来实现AllGather通信。
    • 算子涉及Matmul左右矩阵在外部存储和内部存储间的数据搬运,查看Ascend C API参考中的数据搬运接口,需要使用来实现数据搬运。
    • 计算过程涉及矩阵计算操作,查看Ascend C API参考中的矩阵计算相关接口,需要使用实现矩阵乘计算。

表 1 AllGatherMatmulCustom算子规格

[object Object][object Object]

[object Object][object Object]

AllGatherMatmul算子的数据在卡间进行AllGather通信,在卡内进行Matmul计算,通信和计算按照数据切分后的主块、尾块分多次进行,流水互相掩盖。分析过程中,假定通信矩阵的切分策略为按M轴进行切分,切分后主块数(tileCnt)为2,尾块数(tailCnt)为1,则可得到通信计算掩盖示意图如下。

图 1 AllGatherMatmul通信计算掩盖示意图[object Object][object Object]

AllGather的功能为将通信域内所有卡的输入按照卡id重新排序,然后拼接起来,再将结果发送到所有卡。因此,AllGather的结果中包含本卡数据,即本卡输入的通信矩阵a,算子无需等待这部分数据的通信完成,也无需对数据进行切分,可直接基于完整的通信矩阵a进行Matmul计算。AllGatherMatmul算子首先做本卡数据的Matmul计算,这样做的好处在于主块1的通信能与Matmul计算互相掩盖,同时,主块1、主块2、尾块1的计算无需再包括对本卡数据的Matmul计算,可以减少后续主尾块的计算量,增加通信计算的掩盖率,从而提高性能。注意,不是所有的通算融合算子都适合首先进行本卡数据的Matmul计算。因为AllGatherMatmul算子的通信在计算之前,所以先进行本卡数据的Matmul计算,可以实现本卡数据计算和第一次通信之间的互相掩盖。如果是计算在通信前的算子,如MatmulAllReduce,建议将本卡数据的计算放在最后,与最后一次通信互相掩盖,如下图所示。

图 2 MatmulAllReduce通信计算掩盖示意图[object Object][object Object]

AllGatherMatmul算子逻辑分析:

  1. AI Core将要执行的通信信息写入Global Memory中的消息区,实现任务下发。消息区是特定地址的Global Memory,AI Core和AI CPU通过向其写入和轮询读取来实现消息在两者间的传递,这些操作统一封装于中。

    图 3 通算融合算子通信流程示意图[object Object][object Object]

  2. AI CPU从消息区读取到所有通信任务信息,开始基于HCCS(华为缓存一致性系统,用于CPU/NPU之间的高速互联)或RoCE(承载在融合以太网上的RDMA技术,即跨越以太网的RDMA通信方式)等链路执行第一轮AllGather集合通信任务。与此同时,AI Core开始对本卡数据进行Matmul计算。

    下图是通信卡数为4时,第一轮通信与本卡计算的示意图。tile 1表示图示为第一轮通信和与其相互掩盖的矩阵乘计算的处理流程。图中切分后的小矩阵中形如X-Y的数字表示它所在的数据块对应于第X张卡第Y块数据。

    图 4 AllGatherMatmul第一轮通信与rank0上的本卡数据矩阵乘示意图[object Object][object Object]

  3. AI CPU完成第一轮通信任务后,向消息区写入第一轮通信任务已完成的消息,并开始执行第二轮通信任务。同时,AI Core在完成本卡数据的Matmul计算后,通过轮询消息区等到第一轮通信任务已完成的消息,开始进行第一轮通信结果即主块1的Matmul计算。

    下图是通信卡数为4时,第二轮通信与rank0计算的示意图。tile 2表示图示为第二轮通信和与其相互掩盖的矩阵乘计算的处理流程。

    图 5 AllGatherMatmul第二轮通信与rank0上主块1的矩阵乘示意图[object Object][object Object]

  4. 类似步骤3,逐步完成剩余所有数据块的通信和计算。

[object Object]

创建通算融合算子的算子工程与一般算子相同,具体请参考章节。本样例基于如下原型定义json文件,使用自定义算子工程生成工具msOpGen,为AllGatherMatmul算子创建算子工程。

[object Object]
[object Object]

相比于一般算子,通算融合算子在实现时,有如下约束:

  • 必须定义一个表示算子通信域名称的属性。通信域是集合通信执行的上下文,管理对应的通信实体(例如一个NPU就是一个通信实体)和通信所需的资源。
  • 必须通过原型注册中的MC2接口注册该算子为通算融合算子,并通过HcclGroup接口配置该算子的通信域名称。

AllGatherMatmul算子使用"group"属性表示该算子的通信域名称,其在算子原型中定义如下:

[object Object]

AllGatherMatmul算子的完整原型定义如下:

[object Object]
[object Object]

通算融合算子Tiling策略的设计主要包括通信切分策略、Matmul多核切分和核内切分策略。

  • 通信切分策略:每轮通信数据块的大小,对通算融合算子的性能有较大影响。样例中按照主块M轴长度448对通信矩阵A的M轴进行切分。具体场景中如何确定切分策略请参考

  • Matmul多核切分和核内切分:

    • 多核切分: 根据当前核数,对输入shape的M、K、N进行多核切分,得到单核内shape大小singleCoreM、singleCoreK、singleCoreN。
    • 核内切分: 根据Local Memory的大小约束,对单核内的shape大小进一步切分,得到A、B、C矩阵参与一次矩阵乘指令的shape大小baseM、baseN、baseK。

    如上所述,通信矩阵被切分为主块、尾块,主块、尾块的通信结果以及本卡数据需要分别进行Matmul计算。如下图,主块、尾块和本卡数据在M轴的长度分别为tileM、tailM和rankM,即Matmul计算时的左矩阵存在三种不同的形状,因此需要分别以通信矩阵主块、尾块和本卡数据块的大小为矩阵乘原始的输入形状,调用提供的Tiling接口,得到对应这三种形状的多核切分和核内切分策略。这里,singleCoreM、baseM等概念和相关原理的介绍请参考

    图 6 AllGatherMatmul算子在rank0的矩阵乘示意图[object Object][object Object]

下面给出Tiling实现的关键步骤:

  1. 定义AllGatherMatmul算子的Tiling结构体。

    通信和Matmul融合得到的通算融合算子的Tiling结构体一般包括如下三个部分:

    • 。定义Mc2InitTiling和Mc2CcTiling参数。Mc2InitTiling参数用于初始化通信任务配置,必须定义为算子Tiling结构体的第一个参数。Mc2CcTiling为具体每个通信任务的参数配置,由于AllGatherMatmul算子中只有AllGather一个通信任务,因此仅需定义一个Mc2CcTiling参数。
    • 。一般而言,主块、尾块和本卡数据的shape是不同的,由于TCubeTiling只能存储对一个输入形状进行Tiling计算得到的结果,因此需要分别定义主块、尾块和本卡数据块的Tiling结构体,来存放它们的多核切分和核内切分策略。
    • AllGatherMatmul算子额外需要的自定义结构体AllGatherMatmulTiling。

    AllGatherMatmul算子的完整Tiling结构体定义如下:

    [object Object]
  2. 获取AllGatherMatmul算子的Tiling结构体对象指针。

    [object Object]

    context为TilingContext的对象指针,该指针由框架自动从注册的Tiling入口函数AllGatherMatmulCustomTilingFunc传入,用于保存算子Tiling计算的上下文。在AllGatherMatmul算子的Tiling实现中,通过该上下文context获取计算Tiling所需要的输入输出shape、输入属性等参数,然后将Tiling结果(例如TilingKey、TilingData)保存至上下文中,供后续算子执行时使用。

  3. 设置算子自定义的Tiling结构体参数。

    [object Object]
  4. 设置 Tiling结构体。

    通过matmul_tiling::MultiCoreMatmulTiling获取TCubeTiling结构体,首先创建多核Tiling对象mmTiling,然后设置A、B、C的参数类型信息,M、N、K形状信息等,最后调用接口,获取Tiling信息,具体方法可详见。AllGatherMatmul算子中将上述逻辑封装为matmulTilingFunc函数,再分别根据主块、尾块和本卡数据的形状大小,调用matmulTilingFunc函数,得到对应的TCubeTiling参数。

    [object Object]
  5. 设置Hccl高阶API Tiling结构体。

    根据通信任务类型、算法配置等,创建一个Mc2CcTilingConfig类对象,通过向方法传入算子Tiling结构体中mc2InitTiling和mc2CcTiling成员的引用,完成需要传递给Kernel侧的Mc2InitTiling参数和Mc2CcTiling参数的获取。Hccl高阶API Tiling结构体的具体使用方法详见

    [object Object]
[object Object]

在AllGatherMatmul算子的Kernel实现中,需要对本卡数据、通信主块、通信尾块共三种形状的左矩阵进行Matmul运算,为避免重复代码,有必要抽象出一个通用的适用于不同输入形状的Matmul计算函数。设计该Matmul计算函数前,需要考虑Matmul计算需要的基本信息,罗列如下:

  • 输入A、B矩阵和输出C矩阵的地址。
  • TCubeTiling结构体:包含矩阵A、B、C的形状、数据类型等信息,以及A、B矩阵进行Matmul运算时在核间和核内的切分策略。

除了上述Matmul运算所需的信息外,为了快速实现Matmul矩阵乘法,可以使用Matmul高阶API中的Matmul对象来执行计算。如果Matmul对象在Matmul计算函数中定义,每次调用该函数时都会实例化Matmul对象并释放资源,这将导致较大的运行时开销。因此,将该对象也作为Matmul计算函数的参数,以实现对象的复用。

综上所述,在Kernel实现中定义的适用于不同输入形状的Matmul计算函数如下。其中Matmul计算函数函数名定义为MatmulKernel,入参aGM、bGM、cGM表示需要运算的原始输入输出矩阵的地址,入参tiling表示TCubeTiling结构体,入参mm对应Matmul高阶API的实现类。MATMUL_TYPE是特化了MatmulType模板的类型别名。

[object Object]

MatmulKernel函数的实现步骤如下。

  1. TCubeTiling结构体存储了Matmul计算所需的核数,在无需计算的核上直接返回,结束计算。

    [object Object]
  2. Matmul高阶API要求使用GlobalTensor作为输入输出矩阵,因此,根据函数输入的A、B、C矩阵在Global Memory的地址,分别定义aGlobal、bGlobal、cGlobal三个GlobalTensor。

    [object Object]
  3. 为了实现多核并行,提升计算效率,将矩阵数据进行切分,切分后的数据分配到不同的核上进行处理。这里采用了不切分K轴、仅切分M、N轴的切分策略,示意图如下。在这种场景下,每个核需要计算待处理的矩阵数据相对于原始矩阵的偏移量,并将偏移后的矩阵作为传入A、B、C矩阵时的入参。同时,为支持分核后的尾块数据的处理,每个核需要计算实际处理的singleCoreM、singleCoreN大小,并在下一步中通过调用Matmul高阶API进行设置。

    图 7 Matmul计算分核示意图[object Object][object Object]

    [object Object]
  4. 调用Matmul高阶API设置Matmul计算的原始完整的形状、当前核处理的输入输出矩阵的地址和计算的实际singleCoreM、singleCoreN的大小,并完成矩阵乘运算。

    [object Object]

AllGatherMatmul算子的核函数定义如下,aGM、bGM、cGM、gatherOutGM参数含义如中所述,workspaceGM和tilingGM分别表示wrokspace空间和tiling数据在Global Memory的地址。

[object Object]

下面介绍AllGatherMatmul算子主流程实现的具体步骤。

  1. Matmul计算依赖AIC核,因此控制算子逻辑仅运行于AIC中。通过ASCEND_IS_AIV宏,判断如果当前核为AIV核,直接返回,结束当前核的运行。

    [object Object]
  2. 注册算子Tiling结构体、获取Tiling,并初始化TPipe。

    [object Object]
  3. 定义并赋值后续计算所需变量。

    [object Object]
  4. 初始化hccl对象并下发AllGather通信任务。

    [object Object]
  5. 初始化Matmul对象,对本卡数据进行Matmul计算。

    [object Object]
  6. 逐轮等待主块的通信完成,并对其进行Matmul计算。

    [object Object]
  7. 等待尾块的通信完成,并对其进行Matmul计算。

    [object Object]
  8. 释放资源。

    [object Object]

整合前述代码 ,完整Kernel代码如下。

[object Object]
[object Object]

下面从编译、安装、运行三个步骤对AllGatherMatmul样例作简要介绍。

  1. 编译

    参考中生成自定义算子工程、编译算子的命令,运行install.sh脚本完成编译。

    样例目录结构如下,AllGatherMatmulCustom目录为必要的算子实现,install.sh脚本使用msOpGen在21_all_gather_matmul_custom目录下创建一个CustomOp目录,并将算子实现文件复制到对应目录下,再调用msOpGen生成的编译入口脚本build.sh编译算子。

    [object Object]

    msOpGen生成的CustomOp目录结构如下。

    [object Object]
  2. 安装

    部署自定义算子包前,请确保环境中存在自定义算子包默认部署路径的环境变量ASCEND_OPP_PATH。

    [object Object]

    然后执行如下命令,切换目录为编译出的自定义算子安装包所在目录,并安装自定义算子包。

    [object Object]

    命令执行成功后,自定义算子包中的相关文件将部署至环境变量ASCEND_OPP_PATH指向的的vendors/customize目录中。

  3. 运行

    切换目录为AclNNInvocation目录,执行run.sh脚本运行单算子样例。

    [object Object]

    样例中的AclNNInvocation目录提供了完整的单算子API调用的示例代码。完成前两个步骤自定义算子的编译部署后,会自动生成单算子API,该API可以直接在应用程序中调用。算子API的形式一般为“两段式接口”,形如:

    [object Object]

    其中aclnnAllGatherMatmulCustomGetWorkspaceSize为第一段接口,主要用于计算本次API调用计算过程中需要的workspace内存大小。按照该workspaceSize大小申请Device侧内存,然后调用第二段接口aclnnAllGatherMatmulCustom执行计算。详细内容请参考章节。

    在通算融合场景,单算子API调用的程序中需要调用中的接口创建通信域,并在多线程上执行AllGatherMatmul算子。以下给出main函数和线程调用函数中关键步骤的代码示例,仅供参考。

    [object Object]

    在main函数中,通过HcclCommInitAll接口在当前进程统一创建了RANK_DIM张卡的通信域,一张卡对应后续创建的一个线程。每个线程都调用RunOp函数,该函数负责卡上运行时资源申请和单算子API的二阶段接口调用。RunOp函数的代码示例如下。

    [object Object]