CANN学习资源开源仓的中级算子开发三泛化Tiling
如果开发者想支持“一类” 算子,能适合任何合法的数据类型、形状,甚至多种昇腾AI处理器型号,这种场景,称之为算子的泛化。其中,泛化tiling开发的原则有三:内存对齐(32字节是最小粒度单位)、访存优化(单次多搬,减少搬运次数)、多核均衡。
基本概念回顾:每次搬运的那一部分数据块,叫做Tiling块;根据不同输入形状确定搬入基本块大小的相关算法,叫做Tiling算法(或Tiling策略)。根据每个核计算的数据量是否相同、核内每个数据块的数据量是否相同,切分策略可有:
- 核间均分,核内均分。
- 核间均分,核内不均分:通过尾块Tiling处理尾块数据的计算。
- 核间不均分,核内均分:通过尾核Tiling的处理解决数据无法在各核间均匀分配的问题。
- 核间不均分,核内不均分:需要同时考虑尾核&尾块。
以状为(1,660)的half类型输入数据,要分配到4个aicore上完成Add计算为例。660×2=1320B无法被32整除,向上补齐至32B对齐,为1344B。将补齐后的 42 个 32B 数据块 分配到 4 个 核,则每核10块,余2块由前 2 个核各多处理 1 块。
假设UB(Unified Buffer)容量限制为768B,单核单次处理数据量受此约束,需按UB大小对核内数据进行批次拆分。
为存储 Tiling 相关参数,基于前述的核间拆分、UB 批次拆分逻辑,需要设计以下字段:
| 结构体字段 | 对应数值 | 计算逻辑/业务含义 |
smallCoreDataNum |
320B | 小核(后2个核)总数据量:10个32B块 × 32B/块 = 320B |
bigCoreDataNum |
352B | 大核(前2个核)总数据量:11个32B块 × 32B/块 = 352B |
finalBigTileNum |
2 | 大核批次次数:单批次最多8块(256B),11块需分2批(8块+3块) |
finalSmallTileNum |
2 | 小核批次次数:单批次最多8块(256B),10块需分2批(8块+2块) |
tileDataNum |
256B | 单核单次最大搬运量:UB 768B ÷ 3(2输入+1输出)= 256B(8个32B块) |
smallTailDataNum |
64B | 小核最后一批数据量:2个32B块 × 32B/块 = 64B |
bigTailDataNum |
96B | 大核最后一批数据量:3个32B块 × 32B/块 = 96B |
tailBlockNum |
2 | 大核个数:42个32B块 ÷ 4核,余数为2,前2个核为大核 |
在计算以上字段的过程中,会涉及到以下中间计算
- GetCoreNum:获取当前环境的核数
- GetShapeSize:获取输入的元素数量
- GetDataTypeLength: 获取输入数据类型所占内存大小
- everyCoreInputBlockNum: 每个核处理的32B数据块个数(舍去余数),然后
- tailBlockNum: 前tailBlockNum个核每个核多计算一个32B数据块(余数)
- tileBlockNum:和tileDataNum表达同一东西,只是单位不一样,此处单位是Block(32B数据块)
- GetCoreMemSize: 获取硬件平台存储空间的内存大小
- smallTileNum: 每个核分到数据不能一次运算完成时需要循环计算的次数(舍去余数),然后前面加final就看有没有余数了,有余数final就加一,无余数则相等。
最后在算大核的时候,对于大核,因需多处理1个32B数据块,其实际处理的32B数据块总数为everyCoreInputBlockNum + 1,所以代码里直接everyCoreInputBlockNum += 1; 这个是不是有点粗鲁了,如果核间均分,就没有大小核之分,那这里怎么说呢?
最后说明,此tiling方案可全面覆盖4种不同的Tiling场景(未考虑开启Double Buffer模式)。
然后看device侧,算子类通过模板接收输入和输出的数据类型,Init函数入参包含输入输出张量GM地址、Tiling切分数据。
- 先基于大核处理元素数,计算当前核的GlobalMemory地址偏移。
- 通过当前核索引与tailBlockNum的对比结果判断核类型
- 若为大核,直接配置算子类的大核规格参数(元素数、切分次数、尾块元素数);
- 若为小核,则配置小核规格参数,修正GlobalMemory地址偏移(因初始偏移量基于大核计算)。
- 最后根据已配置的核规格参数,完成GlobalTensor的初始化与LocalMemory的内存分配。
代码:
__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z, uint32_t smallCoreDataNum,
uint32_t bigCoreDataNum, uint32_t finalBigTileNum,
uint32_t finalSmallTileNum, uint32_t tileDataNum,
uint32_t smallTailDataNum, uint32_t bigTailDataNum,
uint32_t tailBlockNum)
{
uint32_t coreNum = AscendC::GetBlockIdx();
uint32_t globalBufferIndex = bigCoreDataNum * AscendC::GetBlockIdx();
this->tileDataNum = tileDataNum;
if (coreNum < tailBlockNum) {
this->coreDataNum = bigCoreDataNum;
this->tileNum = finalBigTileNum;
this->tailDataNum = bigTailDataNum;
}
else {
this->coreDataNum = smallCoreDataNum;
this->tileNum = finalSmallTileNum;
this->tailDataNum = smallTailDataNum;
globalBufferIndex -= (bigCoreDataNum - smallCoreDataNum) * (AscendC::GetBlockIdx() - tailBlockNum);
}
xGm.SetGlobalBuffer((__gm__ TYPE_X*)x + globalBufferIndex, this->coreDataNum);
yGm.SetGlobalBuffer((__gm__ TYPE_Y*)y + globalBufferIndex, this->coreDataNum);
zGm.SetGlobalBuffer((__gm__ TYPE_Z*)z + globalBufferIndex, this->coreDataNum);
pipe.InitBuffer(inQueueX, BUFFER_NUM, this->tileDataNum * sizeof(TYPE_X));
pipe.InitBuffer(inQueueY, BUFFER_NUM, this->tileDataNum * sizeof(TYPE_Y));
pipe.InitBuffer(outQueueZ, BUFFER_NUM, this->tileDataNum * sizeof(TYPE_Z));
}
//核函数中搬运和运算会使用到的变量,和tiling结构体相比已大大减少
private:
AscendC::TPipe pipe;
AscendC::TQue<AscendC::QuePosition::VECIN, BUFFER_NUM> inQueueX, inQueueY;
AscendC::TQue<AscendC::QuePosition::VECOUT, BUFFER_NUM> outQueueZ;
AscendC::GlobalTensor<TYPE_X> xGm;
AscendC::GlobalTensor<TYPE_Y> yGm;
AscendC::GlobalTensor<TYPE_Z> zGm;
uint32_t coreDataNum;
uint32_t tileNum;
uint32_t tileDataNum;
uint32_t tailDataNum;
uint32_t processDataNum;
至于考入运算考出的循环,以及算子调用,不在赘述
- 点赞
- 收藏
- 关注作者
评论(0)