华为昇腾CANN算子Tiling策略实战:大小核动态负载均衡的减法算子实现
华为昇腾CANN算子Tiling策略实战:大小核动态负载均衡的减法算子实现
本文通过一个完整的减法算子(
z = x - y)代码,深入讲解基于华为昇腾CANN的Tiling策略设计。从Host侧的动态负载均衡算法,到Device侧的多核并行处理,带你一步步理解昇腾AI芯片的Tiling策略和内存管理。
1. 背景与目标
在昇腾AI处理器的开发中,Tiling策略是算子性能优化的关键。当输入数据量不能被核心数整除时,如何合理分配数据给各个核心,避免核心空闲,提升整体计算效率,是Tiling策略要解决的核心问题。本文的目标是:
- 实现一个减法算子
SubCustomTemplate,支持z[i] = x[i] - y[i]。 - 采用大小核动态负载均衡策略,解决数据量不能被核心数整除的问题。
- 深入理解Unified Buffer、Tile、Block等昇腾AI芯片的核心概念。
2. Tiling策略核心思想
2.1 大小核概念
在昇腾AI处理器中,”大核”和”小核”并非指核心能力差异,而是指处理数据量的多少:
- 大核:处理较多的数据,用于处理尾数据(数据量不能被核心数整除时的剩余部分)
- 小核:处理较少的数据,用于处理主体数据
这种设计确保了所有核心都能充分利用,避免部分核心空闲。
2.2 核心概念
| 概念 | 说明 |
|---|---|
| BLOCK_SIZE | 块大小,每次搬运/计算的最小数据量,通常为32字节对齐 |
| Tile | 每次搬运的数据量,由Unified Buffer大小决定 |
| Unified Buffer | 核内快速缓存,计算时能使用的空间大小 |
| 尾数据 | 数据量不能被Tile整除时的剩余数据 |
3. Host侧Tiling策略实现
3.1 数据对齐与核心数确定
uint32_t inputNum = context->GetInputShape(0)->GetStorageShape().GetShapeSize();
uint32_t typeLength = 0;
ge::TypeUtils::GetDataTypeLength(context->GetInputDesc(0)->GetDataType(), typeLength);
uint32_t inputLength = inputNum * typeLength;
const uint32_t BLOCK_SIZE = 32;
uint32_t inputLengthAlgin32 = (((inputLength + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE);
关键点:
- 获取输入数据的元素数量和数据类型长度
- 计算输入数据的字节长度
- 按照32字节对齐,保证每个核心至少处理一个块的数据
3.2 核心数动态分配
auto min_Use_CoreNum = std::min(coreNum, inputLengthAlgin32 / BLOCK_SIZE);
coreNum = std::max(min_Use_CoreNum, static_cast<uint32_t>(1));
context->SetBlockDim(coreNum);
核心逻辑:
- 每个核至少处理一个块的数据量
- 如果对齐后的输入数据块数小于核心数,只使用block数的核心数
- 至少使用一个核心,避免0个核心处理数据异常
3.3 数据分配策略
uint32_t everyCoreInputBlockNum = inputLengthAlgin32 / BLOCK_SIZE / coreNum;
uint32_t tailBlockNum = (inputLengthAlgin32 / BLOCK_SIZE) % coreNum;
分配策略:
everyCoreInputBlockNum:每个核心处理的块数量(平均分配部分)tailBlockNum:尾块数量,如果输入数据块数不能被核心数整除,前tailBlockNum个核心需要多处理一个块
3.4 Tile计算
uint64_t ubSize;
ascendcPlatform.GetCoreMemSize(platform_ascendc::CoreMemType::UB, ubSize);
uint32_t ubDataNumber = 3;
uint32_t tileBlockNum = (ubSize / BLOCK_SIZE) / ubDataNumber;
uint32_t tileDataNum = (tileBlockNum * BLOCK_SIZE) / typeLength;
Tile计算逻辑:
- 获取Unified Buffer的大小
ubDataNumber = 3:减法涉及两个输入一个输出,需要同时在UB中存放tileBlockNum:每次搬运的Block数量(核每次最大的计算量)tileDataNum:每次搬运的元素数量
3.5 大小核数据分配
uint32_t smallCoreDataNum = everyCoreInputBlockNum * BLOCK_SIZE / typeLength;
uint32_t smallTileNum = everyCoreInputBlockNum / tileBlockNum;
uint32_t finalSmallTileNum = (everyCoreInputBlockNum % tileBlockNum) == 0 ? smallTileNum : smallTileNum + 1;
uint32_t smallTailDataNum = smallCoreDataNum - (tileDataNum * smallTileNum);
smallTailDataNum = smallTailDataNum == 0 ? tileDataNum : smallTailDataNum;
everyCoreInputBlockNum += 1;
uint32_t bigCoreDataNum = everyCoreInputBlockNum * BLOCK_SIZE / typeLength;
uint32_t bigTileNum = everyCoreInputBlockNum / tileBlockNum;
uint32_t finalBigTileNum = (everyCoreInputBlockNum % tileBlockNum) == 0 ? bigTileNum : bigTileNum + 1;
uint32_t bigTailDataNum = bigCoreDataNum - (tileDataNum * bigTileNum);
bigTailDataNum = bigTailDataNum == 0 ? tileDataNum : bigTailDataNum;
大小核分配策略:
- 小核:处理
everyCoreInputBlockNum个Block的数据 - 大核:处理
everyCoreInputBlockNum + 1个Block的数据(多处理一个Block) - 尾数据处理:如果数据量不能被Tile整除,最后一次搬运处理剩余数据
4. Device侧多核并行实现
4.1 核函数初始化
uint32_t coreNum = AscendC::GetBlockIdx();
uint32_t globalBufferIndex = bigCoreDataNum * AscendC::GetBlockIdx();
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);
}
核心识别与数据分配:
- 通过
GetBlockIdx()获取当前核的编号 - 前
tailBlockNum个核作为大核,处理更多数据 - 后续核作为小核,处理较少数据
- 小核需要调整偏移量,确保从正确的位置开始处理数据
4.2 流水线并行处理
__aicore__ inline void Process()
{
int32_t loopCount = this->tileNum;
this->processDataNum = this->tileDataNum;
for (int32_t i = 0; i < loopCount; i++) {
if (i == this->tileNum - 1) {
this->processDataNum = this->tailDataNum;
}
CopyIn(i);
Compute(i);
CopyOut(i);
}
}
处理流程:
- 循环处理每个Tile
- 最后一个Tile处理尾数据
- 采用CopyIn → Compute → CopyOut的流水线模式
4.3 数据搬运与计算
__aicore__ inline void CopyIn(int32_t progress)
{
AscendC::LocalTensor<TYPE_X> xLocal = inQueueX.AllocTensor<TYPE_X>();
AscendC::LocalTensor<TYPE_Y> yLocal = inQueueY.AllocTensor<TYPE_Y>();
AscendC::DataCopy(xLocal, xGm[progress * this->tileDataNum], this->processDataNum);
AscendC::DataCopy(yLocal, yGm[progress * this->tileDataNum], this->processDataNum);
inQueueX.EnQue(xLocal);
inQueueY.EnQue(yLocal);
}
__aicore__ inline void Compute(int32_t progress)
{
AscendC::LocalTensor<TYPE_X> xLocal = inQueueX.DeQue<TYPE_X>();
AscendC::LocalTensor<TYPE_Y> yLocal = inQueueY.DeQue<TYPE_Y>();
AscendC::LocalTensor<TYPE_Z> zLocal = outQueueZ.AllocTensor<TYPE_Z>();
AscendC::Sub(zLocal, xLocal, yLocal, this->processDataNum);
outQueueZ.EnQue<TYPE_Z>(zLocal);
inQueueX.FreeTensor(xLocal);
inQueueY.FreeTensor(yLocal);
}
__aicore__ inline void CopyOut(int32_t progress)
{
AscendC::LocalTensor<TYPE_Z> zLocal = outQueueZ.DeQue<TYPE_Z>();
AscendC::DataCopy(zGm[progress * this->tileDataNum], zLocal, this->processDataNum);
outQueueZ.FreeTensor(zLocal);
}
流水线设计:
- CopyIn:从GlobalMemory搬运数据到LocalTensor
- Compute:执行减法计算,结果放入输出队列
- CopyOut:将结果写回GlobalMemory
5. Tiling数据结构
struct SubCustomTemplateTilingData {
uint32_t smallCoreDataNum;
uint32_t bigCoreDataNum;
uint32_t finalBigTileNum;
uint32_t finalSmallTileNum;
uint32_t tileDataNum;
uint32_t smallTailDataNum;
uint32_t bigTailDataNum;
uint32_t tailBlockNum;
};
结构体字段说明:
smallCoreDataNum:每个小核处理的数据个数bigCoreDataNum:每个大核处理的数据个数finalBigTileNum:大核搬运Tile的次数finalSmallTileNum:小核搬运Tile的次数tileDataNum:每个Tile处理的数据个数smallTailDataNum:小核最后一轮搬运Tile的数据个数bigTailDataNum:大核最后一轮搬运Tile的数据个数tailBlockNum:处理尾数据的大核数量
6. 关键知识点总结
| 概念 | 说明 |
|---|---|
| 大小核策略 | 通过让部分核心处理更多数据,解决数据量不能被核心数整除的问题 |
| BLOCK_SIZE | 32字节对齐的块大小,昇腾AI处理器的最小数据搬运单位 |
| Unified Buffer | 核内快速缓存,决定了一次最多能计算的数据量 |
| Tile | 每次搬运的数据量,由Unified Buffer大小和算法需求决定 |
| 尾数据处理 | 当数据量不能被Tile整除时,最后一次搬运处理剩余数据 |
| 流水线并行 | CopyIn、Compute、CopyOut重叠进行,提升硬件利用率 |
7. 算子注册
class SubCustomTemplate : public OpDef {
public:
explicit SubCustomTemplate(const char* name) : OpDef(name)
{
this->Input("x")
.ParamType(REQUIRED)
.DataType({ge::DT_FLOAT16, ge::DT_FLOAT})
.Format({ge::FORMAT_ND, ge::FORMAT_ND})
.UnknownShapeFormat({ge::FORMAT_ND, ge::FORMAT_ND});
this->Input("y")
.ParamType(REQUIRED)
.DataType({ge::DT_FLOAT16, ge::DT_FLOAT})
.Format({ge::FORMAT_ND, ge::FORMAT_ND})
.UnknownShapeFormat({ge::FORMAT_ND, ge::FORMAT_ND});
this->Output("z")
.ParamType(REQUIRED)
.DataType({ge::DT_FLOAT16, ge::DT_FLOAT})
.Format({ge::FORMAT_ND, ge::FORMAT_ND})
.UnknownShapeFormat({ge::FORMAT_ND, ge::FORMAT_ND});
this->SetInferShape(ge::InferShape).SetInferDataType(ge::InferDataType);
this->AICore().SetTiling(optiling::TilingFunc);
this->AICore().AddConfig("ascend910b");
}
};
算子注册要点:
- 定义输入输出张量的数据类型和格式
- 设置形状推理和数据类型推理函数
- 绑定Tiling函数
- 指定支持的硬件平台(ascend910b)
8. 性能优化建议
- 合理设置Tile大小:根据Unified Buffer大小和算法需求,平衡数据搬运和计算效率
- 避免核心空闲:通过大小核策略,确保所有核心都有数据可处理
- 内存对齐:按照32字节对齐,提升数据搬运效率
- 流水线并行:充分利用CopyIn、Compute、CopyOut的重叠,提升吞吐量
参考文献:
- 华为昇腾 CANN 官方文档 (v6.3.0+)
- 《Ascend C 编程指南》
- 《AscendCL 接口参考》
完整源码
Sub_template_tiling.h
#ifndef SUB_CUSTOM_TEMPLATE_TILING_H
#define SUB_CUSTOM_TEMPLATE_TILING_H
#include <cstdint>
struct SubCustomTemplateTilingData {
uint32_t smallCoreDataNum;
uint32_t bigCoreDataNum;
uint32_t finalBigTileNum;
uint32_t finalSmallTileNum;
uint32_t tileDataNum;
uint32_t smallTailDataNum;
uint32_t bigTailDataNum;
uint32_t tailBlockNum;
};
#endif
Sub_template_host.cpp
#include "register/op_def_registry.h"
#include "../op_kernel/sub_custom_template_tiling.h"
#include "graph/utils/type_utils.h"
#include "tiling/platform/platform_ascendc.h"
namespace optiling {
static ge::graphStatus TilingFunc(gert::TilingContext* context)
{
auto ascendcPlatform = platform_ascendc::PlatformAscendC(context->GetPlatformInfo());
auto coreNum = ascendcPlatform.GetCoreNum();
uint32_t inputNum = context->GetInputShape(0)->GetStorageShape().GetShapeSize();
uint32_t typeLength = 0;
ge::TypeUtils::GetDataTypeLength(context->GetInputDesc(0)->GetDataType(), typeLength);
uint32_t inputLength = inputNum * typeLength;
const uint32_t BLOCK_SIZE = 32;
uint32_t inputLengthAlgin32 = (((inputLength + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE);
auto min_Use_CoreNum = std::min(coreNum, inputLengthAlgin32 / BLOCK_SIZE);
coreNum = std::max(min_Use_CoreNum, static_cast<uint32_t>(1));
context->SetBlockDim(coreNum);
uint32_t everyCoreInputBlockNum = inputLengthAlgin32 / BLOCK_SIZE / coreNum;
uint32_t tailBlockNum = (inputLengthAlgin32 / BLOCK_SIZE) % coreNum;
uint64_t ubSize;
ascendcPlatform.GetCoreMemSize(platform_ascendc::CoreMemType::UB, ubSize);
uint32_t ubDataNumber = 3;
uint32_t tileBlockNum = (ubSize / BLOCK_SIZE ) / ubDataNumber;
uint32_t tileDataNum = (tileBlockNum * BLOCK_SIZE) / typeLength;
uint32_t smallCoreDataNum = everyCoreInputBlockNum * BLOCK_SIZE / typeLength;
uint32_t smallTileNum = everyCoreInputBlockNum / tileBlockNum;
uint32_t finalSmallTileNum = (everyCoreInputBlockNum % tileBlockNum) == 0 ? smallTileNum : smallTileNum + 1;
uint32_t smallTailDataNum = smallCoreDataNum - (tileDataNum * smallTileNum);
smallTailDataNum = smallTailDataNum == 0 ? tileDataNum : smallTailDataNum;
everyCoreInputBlockNum += 1;
uint32_t bigCoreDataNum = everyCoreInputBlockNum * BLOCK_SIZE / typeLength;
uint32_t bigTileNum = everyCoreInputBlockNum / tileBlockNum;
uint32_t finalBigTileNum = (everyCoreInputBlockNum % tileBlockNum) == 0 ? bigTileNum : bigTileNum + 1;
uint32_t bigTailDataNum = bigCoreDataNum - (tileDataNum * bigTileNum);
bigTailDataNum = bigTailDataNum == 0 ? tileDataNum : bigTailDataNum;
SubCustomTemplateTilingData *tiling = context->GetTilingData<SubCustomTemplateTilingData>();
tiling->smallCoreDataNum = smallCoreDataNum;
tiling->bigCoreDataNum = bigCoreDataNum;
tiling->tileDataNum = tileDataNum;
tiling->smallTailDataNum = smallTailDataNum;
tiling->bigTailDataNum = bigTailDataNum;
tiling->finalSmallTileNum = finalSmallTileNum;
tiling->finalBigTileNum = finalBigTileNum;
tiling->tailBlockNum = tailBlockNum;
return ge::GRAPH_SUCCESS;
}
namespace ge {
static ge::graphStatus InferShape(gert::InferShapeContext* context)
{
const gert::Shape* x1_shape = context->GetInputShape(0);
gert::Shape* y_shape = context->GetOutputShape(0);
*y_shape = *x1_shape;
return GRAPH_SUCCESS;
}
static ge::graphStatus InferDataType(gert::InferDataTypeContext *context)
{
const auto inputDataType = context->GetInputDataType(0);
context->SetOutputDataType(0, inputDataType);
return ge::GRAPH_SUCCESS;
}
}
namespace ops {
class SubCustomTemplate : public OpDef {
public:
explicit SubCustomTemplate(const char* name) : OpDef(name)
{
this->Input("x")
.ParamType(REQUIRED)
.DataType({ge::DT_FLOAT16, ge::DT_FLOAT})
.Format({ge::FORMAT_ND, ge::FORMAT_ND})
.UnknownShapeFormat({ge::FORMAT_ND, ge::FORMAT_ND});
this->Input("y")
.ParamType(REQUIRED)
.DataType({ge::DT_FLOAT16, ge::DT_FLOAT})
.Format({ge::FORMAT_ND, ge::FORMAT_ND})
.UnknownShapeFormat({ge::FORMAT_ND, ge::FORMAT_ND});
this->Output("z")
.ParamType(REQUIRED)
.DataType({ge::DT_FLOAT16, ge::DT_FLOAT})
.Format({ge::FORMAT_ND, ge::FORMAT_ND})
.UnknownShapeFormat({ge::FORMAT_ND, ge::FORMAT_ND});
this->SetInferShape(ge::InferShape).SetInferDataType(ge::InferDataType);
this->AICore()
.SetTiling(optiling::TilingFunc);
this->AICore().AddConfig("ascend910b");
}
};
OP_ADD(SubCustomTemplate);
}
}
Sub_template_kernel.cpp
#include "kernel_operator.h"
#include "sub_custom_template_tiling.h"
#include "kernel_operator_dump_tensor_intf_impl.h"
constexpr int32_t BUFFER_NUM = 2;
template<typename TYPE_X, typename TYPE_Y, typename TYPE_Z>
class KernelSub {
public:
__aicore__ inline KernelSub() {}
__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);
}
this->xGm.SetGlobalBuffer((__gm__ TYPE_X*)x + globalBufferIndex, this->coreDataNum);
this->yGm.SetGlobalBuffer((__gm__ TYPE_Y*)y + globalBufferIndex, this->coreDataNum);
this->zGm.SetGlobalBuffer((__gm__ TYPE_Z*)z + globalBufferIndex, this->coreDataNum);
this->pipe.InitBuffer(inQueueX, BUFFER_NUM, this->tileDataNum * sizeof(TYPE_X));
this->pipe.InitBuffer(inQueueY, BUFFER_NUM, this->tileDataNum * sizeof(TYPE_Y));
this->pipe.InitBuffer(outQueueZ, BUFFER_NUM, this->tileDataNum * sizeof(TYPE_Z));
}
__aicore__ inline void Process()
{
int32_t loopCount = this->tileNum;
this->processDataNum = this->tileDataNum;
for (int32_t i = 0; i < loopCount; i++) {
if (i == this->tileNum - 1) {
this->processDataNum = this->tailDataNum;
}
CopyIn(i);
Compute(i);
CopyOut(i);
}
}
private:
__aicore__ inline void CopyIn(int32_t progress)
{
AscendC::LocalTensor<TYPE_X> xLocal = inQueueX.AllocTensor<TYPE_X>();
AscendC::LocalTensor<TYPE_Y> yLocal = inQueueY.AllocTensor<TYPE_Y>();
AscendC::DataCopy(xLocal, xGm[progress * this->tileDataNum], this->processDataNum);
AscendC::DataCopy(yLocal, yGm[progress * this->tileDataNum], this->processDataNum);
inQueueX.EnQue(xLocal);
inQueueY.EnQue(yLocal);
}
__aicore__ inline void Compute(int32_t progress)
{
AscendC::LocalTensor<TYPE_X> xLocal = inQueueX.DeQue<TYPE_X>();
AscendC::LocalTensor<TYPE_Y> yLocal = inQueueY.DeQue<TYPE_Y>();
AscendC::LocalTensor<TYPE_Z> zLocal = outQueueZ.AllocTensor<TYPE_Z>();
AscendC::Sub(zLocal, xLocal, yLocal, this->processDataNum);
outQueueZ.EnQue<TYPE_Z>(zLocal);
inQueueX.FreeTensor(xLocal);
inQueueY.FreeTensor(yLocal);
}
__aicore__ inline void CopyOut(int32_t progress)
{
AscendC::LocalTensor<TYPE_Z> zLocal = outQueueZ.DeQue<TYPE_Z>();
AscendC::DataCopy(zGm[progress * this->tileDataNum], zLocal, this->processDataNum);
outQueueZ.FreeTensor(zLocal);
}
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;
};
__global__ __aicore__ void sub_custom_template(GM_ADDR x, GM_ADDR y, GM_ADDR z, GM_ADDR workspace, GM_ADDR tiling)
{
REGISTER_TILING_DEFAULT(TilingDataTemplate);
GET_TILING_DATA_WITH_STRUCT(TilingDataTemplate, tiling_data, tiling);
KernelSub op;
op.Init(x, y, z, tiling_data.smallCoreDataNum,
tiling_data.bigCoreDataNum, tiling_data.finalBigTileNum,
tiling_data.finalSmallTileNum, tiling_data.tileDataNum,
tiling_data.smallTailDataNum, tiling_data.bigTailDataNum,
tiling_data.tailBlockNum);
op.Process();
}
Sub.json
[{
"op": "SubCustomTemplate",
"input_desc": [{
"name": "x",
"param_type": "required",
"format": ["ND", "ND"],
"type": ["float16", "float"]
},
{
"name": "y",
"param_type": "required",
"format": ["ND", "ND"],
"type": ["float16", "float"]
}
],
"output_desc": [{
"name": "z",
"param_type": "required",
"format": ["ND", "ND"],
"type": ["float16", "float"]
}]
}]