昇腾算子挑战赛S5
昇腾算子挑战赛S5的WriteUp。本次是抽空参加,就只重点完成了Lcm,因为可以直接在上一次决赛的GCD算子上面修改,这一题也获得了本次的单题第一,其他两题只是在前十,做的一般。
1 BitwiseLeftShift
- 对每个输入input[i],左移other[i]位。
输入Tensor Input和Other可能维度不对应,需要广播。
最多三个维度
1.1 任意维度的广播支持
广播实际上是因为other在某个维度上只有1个单位,因此只能取该单位中的值的场景。例如对于输入数据input(14,10,30,9),other(14,1,30,0),other的维度小于输入数据,最终输出维度与input的维度相同,则需要操作的元素数是input的每一个元素,并且在other中缺失的维度只能取0,例如input的(3,3,3,3),对应other的数据是(3,0,3,0)。由此可以根据输入数据的坐标以及总体维度信息,得到other的坐标,取值进行计算。
对于BitwiseLeftShift,input的维度一定大于等于other的维度,所以输出形状与input相对应,可以直接根据input的元素数量进行多核数据划分。但这样会将所有数据展开至一维划分,失去维度上的含义,每个核在初始化阶段只知道当前元素在所有元素中的偏移,所以在计算过程中,需要根据偏移计算每个操作的元素在input中的坐标,其每一维坐标为:
其中Dim存储的是每一个维度的大小,而S[i]是包含$i$维度在内的维度大小后缀和。根据上式可计算出每一维在原input Tensor的坐标,然后可以根据此坐标得到Other中的对应元素坐标,取出该元素计算即可,就实现了广播。
1.2 单维度广播优化
如果只有一个维度需要广播,可以将other的数据也通过向量化搬运的方式拷入进行batch计算。例如对于input(256,30,99),other数据(256,1,99),可以按照batch进行划分,即将256*30个组计算分配给40个核,每个核内则需要拷入分配到的batch数量组input进行计算,每轮input的size是99,而other的每轮数据则需要根据当前计算的batchID来拷入。(更严谨来说,应该考虑other数据是否能够一次拷入UB Buffer,如果不能,还需要再进行划分,多次拷入处理)。通过这样的优化,可以将单维度广播这种特殊情况的访存开销和索引计算开销降低。
为了进一步优化搬运,可以一次搬运多个batch来进行计算。对于单维度广播,维度总是可以抽象为三部分,以输入数据为input(256,30,99),other(256,1,99),中间维度需要广播为例:
- batch部分,是截至广播维度的维度大小乘积,例如对于上述提到的(256,30,99),共有256*30个batch
- broadCastDim,需要广播的维度大小,这个大小表示每连续的broadCastDim组数据,使用相同的数据进行计算,例如上述提到的(256,30,99),broadCastDim为30,因为other的中间维度是1,所以对于输入的每连续30组数据,使用的是相同的99个元素
- batchDataSize,每组计算的数据量,对于输入张量(256,30,99)为99
每个核可以知道自己的起始batchID,最后一个batchID,而对应的otherBatchID为batchIdx / broadCastDim,由于起始batchID可能不是broadCastDim的整数倍,所以对于一组other数据,要重用的次数是(otherBatchId + 1) * broadCastDim - batchIdx,再与剩余batch数量取较小的那一个。
为了减少搬运次数,增加搬运量,可以一次搬多行。
1 | __aicore__ inline void Process1() { |
1.3 向量化计算
对于不需要广播的情况,首先仍然可以将所有维度展至一维进行数据的划分。输入、输出全部都可以通过DataCopy直接搬入,只需要按照UB buffer的大小,逐个迭代完成单核内所有分配到的数据的计算。
在计算方面,ai core支持int16,int32类型的左移指令,因此对于这两种数据类型,可以实现向量化计算,而另外两种类型只能标量计算。以int32为例,移位数依次为32、16、8、4、2、1,最多6次移位就可以完成所有输入元素的移位,每次移位前后要进行Compare,判断当前移位位数是否小于剩余待移位位数,通过Compare后的bitmask选择移位结果或之前的结果。
由于Compare指令操作类型只有half/float,所以需要预先将Other输入Cast到half或float,而Select指令也只支持half和float,所以要将临时结果和移位结果的Tensor Reinterpret得到一个half或float的指针进行操作,由于Select操作只进行数据选择,所以int16和int32与half或float进行的操作是完全相同的。由于向量化需要进行Cast,指针重解释等操作,模板推导可能会有问题,所以最好不使用模板,对int16和int32的数据编写两个kernel处理。
2 CopySign
与BitwiseLeftShift基本相同。
3 Lcm
- 求两个输入的最小公倍数
输入Tensor Input和Other可能维度不对应,需要广播。
最多三个维度
3.1 形状与数据类型
需要广播的场景与题目一BitwiseLeftShift完全一致,类似题目一:
- 任意维度广播,全标量处理
- 单维度广播,向量数据搬运
- 不需要广播
而根据不同的数据类型,计算又可以分三种情况:
- int8 int16:向量数据搬运(展开至一维进行数据划分)
- int32:cast至float进行向量化计算
- int64:stein+gcd结合的标量+向量化计算得到公因数,再标量计算lcm。
3.2 向量化计算优化
对于int32类型的数据,可以cast至float进行向量化计算,通过CompareScalar、Select、Fmod进行向量化的Gcd计算。
对于int64类型的数据,可以先采用stein算法将数据降至int32范围,然后再使用向量化的gcd来完成计算。在stein算法过程中,需要把降低了数值范围的两个值存入一个tmpBuffer中,后续gcd使用。经过一些测试,当a<(1<<24),gcd迭代次数24的时候,是可以保证计算的正确性的。可以将stein的a降低到更小,让gcd迭代次数更多,来确保在全数据范围都能计算正确。
赛题的时间最长的case刚好是单一维度广播的int64类型,对于单一维度广播,相较于BitwiseLeftShift,还更改了循环的模式,一次读取other数据,处理多个相应的input数据,减少other tensor的标量读取,以及所有的标量计算部分,都可以通过循环展开来提升性能。经实际测试,展开4次的性能是最好的。


