算子适配说明
【免费下载链接】asc-devkit本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C++标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。项目地址: https://gitcode.com/cann/asc-devkit
自定义算子要支持融合进SuperKernel,开发流程上与普通算子并无显著差异,但需要遵循以下一系列特定约束。这些约束与具体的开启路径(GE或aclgraph)无关,大部分约束与算子启动核数(block num)以及SuperKernel启动核数之间的关系相关,需要开发者在算子设计阶段就予以关注。
[!NOTE]说明 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的
GetValue和SetValue接口对GM进行标量读写,在GE入图及aclnn调用场景,SuperKernel编译时会自动在两个接口内部插入DataCacheCleanAndInvalid指令刷新单个Cache Line,保证一定的数据缓存一致性。不会在子Kernel调用前后插入DataCacheCleanAndInvalid。如果开发者通过GlobalTensor的
()运算符接口来获取值(读值时会插入DataCacheCleanAndInvalid指令刷新单个Cache Line保证数据缓存一致性),但通过该接口直接改写了GlobalTensor对应位置的值时,则不会自动插入DataCacheCleanAndInvalid指令,开发者需要自行保证数据缓存一致性。例如:AscendC::GlobalTensor<float> xGm; xGm.SetGlobalBuffer((__gm__ float *)(addr), length); // addr为GM地址,length为对应GM长度 xGm(0) = (float)(1.0); // 在获取值时,SuperKernel能够保证自动刷新cache line,但是写值时,相当于普通变量直接赋值,SuperKernel无法插入DataCacheCleanAndInvalid,需要用户保证数据缓存的一致性。
对于频繁调用GetValue和SetValue的算子,建议使用dcci_before_kernel_start、dcci_after_kernel_end关闭指定算子内GetValue/SetValue中自动插入的缓存刷新指令,而改为算子调用前后插入整个DCache刷新,避免性能劣化。具体option说明,请参考各自pytorch图模式SuperKernel文档option说明章节:GE入图参考TorchAir用户指南“图内标定SuperKernel范围”,npugraph_ex后端参考torchair仓库docs/zh/npugraph_ex/advanced/superkernel.md中super_kernel_optimize_options参数说明。
Cache刷新机制示意图如下图所示:
核数获取接口约束
在GE入图及aclnn调用场景,子Kernel中调用GetBlockNum接口获取核数时,无论是否融合SuperKernel,获取的核数保持不变,不受SuperKernel启动核数的影响。因此,在使用该接口时,开发者无需特别关注SuperKernel的启动核数,使用方法和开发普通算子时一样。
在SuperKernel场景下,如下Ascend C API在算子编译过程中适配了SuperKernel,子算子需要严格按照Ascend C提供的API进行编程,从而无需感知是否开启SuperKernel。例如在获取核数和索引时,不能调用block_idx、block_num等底层变量和相关API,必须使用下表中的Ascend C API:
| API列表 | 功能描述 |
|---|---|
| AscendC::GetBlockIdx() | 获取当前核的index |
| AscendC::GetBlockNum() | 获取当前任务配置的核数 |
TPipe析构约束
针对Atlas A3 训练系列产品/Atlas A3 推理系列产品,在GE入图及aclnn调用场景,子算子TPipe对象析构(TPipe::Destroy)接口内部的AscendC::PipeBarrier<PIPE_ALL>()指令会被去除,如果算子内部使用了多个TPipe对象或手动调用Destroy函数时开发者需自行保障TPipe对象间流水的同步。
样例:
场景一:TPipe对象析构时同步
当算子中存在多个TPipe对象,且通过作用域(大括号或算子结束)触发析构时,需在第一个TPipe析构后手动插入
PipeBarrier<PIPE_ALL>(),确保流水同步。// 场景一:两个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>(),确保流水同步。// 场景二:两个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侧编程时,可以通过调用SetNextTaskStart和WaitPreTaskEnd两个任务间接口,进一步提升性能。
- 调用
SetNextTaskStart后的指令可以和后续其他的子Kernel实现并行,提升整体性能。如图1所示,SuperKernel按序调用子Kernel,为保证子Kernel之间数据互不干扰,会在子Kernel间插入算子间同步进行保序,子KernelN-1调用该接口后,之后的指令会和后续子KernelN实现并行。
图1通过SetNextTaskStart实现并行示意图
- 调用
WaitPreTaskEnd前的指令可以和前序其他的子Kernel实现并行,提升整体性能。如图2所示,SuperKernel按序调用子Kernel,为保证子Kernel之间数据互不干扰,会在子Kernel间插入算子间同步进行保序,子KernelN+1调用该接口之前的指令会和前序子KernelN实现并行。
图2通过WaitPreTaskEnd实现并行示意图
- 调用
二进制复用优化
在GE入图Tiling下沉场景下,可以通过
--op_relocatable_kernel_binary编译选项,开启二进制复用优化,提升编译性能,具体可参考算子工程编译。
核函数直调算子的额外适配
对于使用<<<>>>方式开发的Ascend C算子,除了遵循上述通用约束外,还需要在算子kernel入口侧增加SuperKernel入口函数。具体方法请参考核函数直调算子额外适配说明。
【免费下载链接】asc-devkit本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C++标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。项目地址: https://gitcode.com/cann/asc-devkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考