自定义算子
使用背景
针对部分算子的性能问题,如果无法通过PASS,优化周围的计算结构规避时,就需要针对这部分算子进行手动优化。但手写算子,如果要保证其泛化性,往往会需要针对不同shape实现不同tiling分支,代码量非常大。在针对模型优化时,优化时间过长往往不能满足用户需求。所以,如果需要手写自定义算子,最优的策略是基于一些特定网络结构,基于模型上带来的先验知识,去实现针对特定网络结构的优化分支。
具体案例
案例一:带有效数据长度的TopK
TopK算子在推荐推理如TWINS模型中使用,该算子即使通过1.5.4中的cast方案优化到vector core上,对于序列长度很长的topK,性能还是很差。同时,基于硬件原因,TOPK算子的算法在NPU上很难实现和GPU相同的性能。所以,针对自定义TOPK算子的优化需要上升到模型层面进行分析。从模型上看,TOPK入参的序列源自于一段padding过后的序列,实际上不需要所有数据参与topK,且其中有效数据占比平均只有10%左右。基于此先验条件,只需要实现一个自定义topK算子,新增一个有效长度入参,就能让算子耗时平均下降90%。
案例二:自定义floormod算子
模型中,针对部分特征,采取了如下图实现的通过floormod进行分桶的方式处理,这里实现逻辑为0索引作为0,其余索引会针对一个数取余后+1作为分桶后的索引传递给Gather算子。当前,因为NPU实际上底层不直接支持int64的计算,floormod算子针对int64类型实际上是使用的scalar指令做运算,当入参数量多时,性能很差。从数学上,如果要将int64转换为两个int32计算,其计算逻辑和符号位的处理都很复杂。但基于这个结构,可以得到索引从一定>0的约束(否则Gather报错),同时floormod用于Gather索引分桶,所以除数(桶数)并不需要int64表达。因为底层fmod/除法指令只支持fp32/fp16的计算,分析除数如果转到fp32,在2^24-1(1677万)内不会出现精度损失,这个范围已经满足用户需求(实际上考虑算法需要桶数在2^21-1以内保证性能,也在大部分场景够用)。故设计算子可以设计为int64/fp32, 在vec范围内有实现,可以避免scalar的问题。

案例三:自定义Gather算子
对于部分带有缺省值的特征,通过如下结构实现将<0的索引转为0向量,>0的索引正常作为索引去Embedding. 该结构中Where算子作为AICPU算子,同时这个结构会被作为动态子图执行,性能很差。这里通过将该逻辑融入Gather算子中,在算子内部对每个索引值进行判断,然后将0向量或者从embedding值搬到目标位置实现这一套逻辑,整体耗时减少3个数量级。
