Skip to content

算子适配说明

自定义算子要支持融合进SuperKernel,开发流程上与普通算子并无显著差异,但需要遵循以下一系列特定约束。这些约束与具体的开启方式(npugraph_ex后端或GE图模式)无关,大部分约束与算子启动核数(block num)以及SuperKernel启动核数之间的关系相关,需要开发者在算子设计阶段就予以关注。

说明

SuperKernel启动核数为其中所有子核函数(Kernel)的最大启动核数。例如,SuperKernel包含算子a(启动核数为4)和算子b(启动核数为2),则SuperKernel启动核数为4。

全核同步约束

自定义算子若进行全核同步,需注意该子核函数(Kernel)启动的核数需要与SuperKernel的核数一致,通常需要启动当前硬件满核。若子核函数(Kernel)启动的核数少于SuperKernel的核数,全核同步会等待所有核完成,导致卡住超时。

MIX 1:1算子核数比例约束

若自定义算子的核函数(Kernel)类型设置为KERNEL_TYPE_MIX_AIC_1_1,因为SuperKernel会根据启动核数等信息调整SuperKernel的启动比例,此时需特别注意该算子也可以适应SuperKernel的1:2启动比例,确保AIC与AIV之间的硬同步操作正确执行。比如:

  • 算子内部使用了AIC与AIV之间的硬同步接口(CrossCoreSetFlag和CrossCoreWaitFlag),不要单独指定某些AIV核调用硬同步接口,要使所有AIV核均调用硬同步接口,防止因为硬同步数量不匹配而导致卡死超时。
  • 使用Matmul高阶API时,算子逻辑应保证仅有一个AIV0核调用Matmul接口,防止启动两个AIV核之后出现AIV1核消息无法接收导致卡死超时。

标量读写与Cache一致性

在开发自定义算子时,开发者必须确保所有对GM的标量读写操作都按需正确插入DataCacheCleanAndInvalid指令。

  • 在单算子编译场景下,毕昇编译器自动在算子末尾添加DataCacheCleanAndInvalid指令,刷新整个DCache(数据缓存)。
  • 在SuperKernel中,子核函数(Kernel)被当作普通函数处理,编译器不会自动插入该指令来确保数据缓存一致性,开发者需要自行保证,避免因容错机制改变而导致错误。

出于性能考虑,SuperKernel场景下Cache刷新机制如下:

  • 如果开发者调用GlobalTensor的GetValueSetValue接口对GM进行标量读写,在GE入图及npugraph_ex后端aclnn调用场景,SuperKernel编译时会自动在两个接口内部插入DataCacheCleanAndInvalid指令刷新单个Cache Line,保证一定的数据缓存一致性。不会在子核函数(Kernel)调用前后插入DataCacheCleanAndInvalid。

  • 如果开发者通过GlobalTensor的()运算符接口来获取值(读值时会插入DataCacheCleanAndInvalid指令刷新单个Cache Line保证数据缓存一致性),但通过该接口直接改写了GlobalTensor对应位置的值时,则不会自动插入DataCacheCleanAndInvalid指令,开发者需要自行保证数据缓存一致性。例如:

    C++
    AscendC::GlobalTensor<float> xGm;
    xGm.SetGlobalBuffer((__gm__ float *)(addr), length); // addr为GM地址,length为对应GM长度
    xGm(0) = (float)(1.0); // 在获取值时,SuperKernel能够保证自动刷新cache line,但是写值时,相当于普通变量直接赋值,SuperKernel无法插入DataCacheCleanAndInvalid,需要用户保证数据缓存的一致性。
    

对于频繁调用GetValueSetValue的算子,建议使用dcci_before_kernel_startdcci_after_kernel_end关闭指定算子内GetValue/SetValue中自动插入的缓存刷新指令,而改为算子调用前后插入整个DCache刷新,避免性能劣化。具体option说明,请参考各自PyTorch图模式SuperKernel文档option说明章节:GE入图参考图内标定SuperKernel范围,npugraph_ex后端参考SuperKernel功能super_kernel_optimize_options参数说明。

Cache刷新机制示意图如下图所示:

核数获取接口约束

在GE入图及npugraph_ex后端aclnn调用场景,子核函数(Kernel)中调用GetBlockNum接口获取核数时,无论是否融合SuperKernel,获取的核数保持不变,不受SuperKernel启动核数的影响。因此,在使用该接口时,开发者无需特别关注SuperKernel的启动核数,使用方法和开发普通算子时一样。

在SuperKernel场景下,如下Ascend C API在算子编译过程中适配了SuperKernel,子算子需要严格按照Ascend C提供的API进行编程,从而无需感知是否开启SuperKernel。例如在获取核数和索引时,不能调用block_idxblock_num等底层变量和相关API,必须使用下表中的Ascend C API:

API列表功能描述
AscendC::GetBlockIdx()获取当前核的index
AscendC::GetBlockNum()获取当前任务配置的核数

TPipe析构约束

针对Atlas A2 训练系列产品/Atlas A2 推理系列产品、Atlas A3 训练系列产品/Atlas A3 推理系列产品,在GE入图及npugraph_ex后端aclnn调用场景,子算子TPipe对象析构(TPipe::Destroy)接口内部的AscendC::PipeBarrier<PIPE_ALL>()指令会被去除,如果算子内部使用了多个TPipe对象或手动调用Destroy函数时开发者需自行保障TPipe对象间流水的同步。

样例:

  • 场景一:TPipe对象析构时同步

    当算子中存在多个TPipe对象,且通过作用域(大括号或算子结束)触发析构时,需在第一个TPipe析构后手动插入PipeBarrier<PIPE_ALL>(),确保流水同步。

    C++
    // 场景一:两个TPipe对象通过作用域析构,需在第一个析构后插入PipeAll
    {
        AscendC::TPipe pipe1;
        AscendC::TQue<AscendC::TPosition::VECOUT, 2> que1;
        uint8_t num1 = 2;
        uint32_t len1 = 128;
        pipe1.InitBuffer(que1, num1, len1);
        // ... 使用pipe1进行算子计算 ...
    } // pipe1析构,但Destroy中的PipeBarrier已被去除
    
    // 必须手动插入PipeAll,保证pipe1流水完成后再使用pipe2
    AscendC::PipeBarrier<AscendC::PIPE_ALL>();
    
    {
        AscendC::TPipe pipe2;
        AscendC::TQue<AscendC::TPosition::VECOUT, 2> que2;
        uint8_t num2 = 2;
        uint32_t len2 = 128;
        pipe2.InitBuffer(que2, num2, len2);
        // ... 使用pipe2进行算子计算 ...
    } // pipe2析构
    
  • 场景二:手动调用Destroy时同步

    当算子中存在多个TPipe对象,且手动调用Destroy接口时,需在第一个TPipe的Destroy调用后手动插入PipeBarrier<PIPE_ALL>(),确保流水同步。

    C++
    // 场景二:两个TPipe对象手动调用Destroy,需在第一个Destroy后插入PipeAll
    AscendC::TPipe pipe1;
    AscendC::TQue<AscendC::TPosition::VECOUT, 2> que1;
    uint8_t num1 = 2;
    uint32_t len1 = 128;
    pipe1.InitBuffer(que1, num1, len1);
    // ... 使用pipe1进行算子计算 ...
    pipe1.Destroy(); // Destroy中的PipeBarrier已被去除
    
    // 必须手动插入PipeAll,保证pipe1流水完成后再使用pipe2
    AscendC::PipeBarrier<AscendC::PIPE_ALL>();
    
    AscendC::TPipe pipe2;
    AscendC::TQue<AscendC::TPosition::VECOUT, 2> que2;
    uint8_t num2 = 2;
    uint32_t len2 = 128;
    pipe2.InitBuffer(que2, num2, len2);
    // ... 使用pipe2进行算子计算 ...
    pipe2.Destroy();
    

性能优化建议

  • 任务间同步

    开发者在进行核函数(Kernel)侧编程时,可以通过调用SetNextTaskStartWaitPreTaskEnd两个任务间接口,进一步提升性能。

    • 调用SetNextTaskStart后的指令可以和后续其他的子核函数(Kernel)实现并行,提升整体性能。如图1所示,SuperKernel按序调用子核函数(Kernel),为保证子核函数(Kernel)之间数据互不干扰,会在子核函数(Kernel)间插入算子间同步进行保序,子核函数(Kernel)N-1调用该接口后,之后的指令会和后续子核函数(Kernel)N实现并行。

    图1 通过SetNextTaskStart实现并行示意图

    • 调用WaitPreTaskEnd前的指令可以和前序其他的子核函数(Kernel)实现并行,提升整体性能。如图2所示,SuperKernel按序调用子核函数(Kernel),为保证子核函数(Kernel)之间数据互不干扰,会在子核函数(Kernel)间插入算子间同步进行保序,子核函数(Kernel)N+1调用该接口之前的指令会和前序子核函数(Kernel)N实现并行。

    图2 通过WaitPreTaskEnd实现并行示意图

  • 二进制复用优化

    在GE入图Tiling下沉场景下,可以通过--op_relocatable_kernel_binary编译选项,开启二进制复用优化,提升编译性能,具体可参考算子工程编译

核函数(Kernel)直调算子的额外适配

对于使用<<<>>>方式开发的Ascend C算子,目前仅支持在npugraph_ex后端融合进SuperKernel。此类算子除需遵循上述通用约束外,还需要在算子kernel入口侧增加SuperKernel入口函数。具体方法请参考核函数(Kernel)直调算子额外适配说明

免责声明:本站内容由 asc-devkit 仓 master 分支自动编译生成,属于持续开发版本,可能存在缺陷,仅供预览与参考。如需稳定及商用资料,请查阅官方 昇腾社区