10.1 算子工程结构

结构大致情况。
在这里插入图片描述
定义变量
在这里插入图片描述
具体操作:
在这里插入图片描述

10.1.1 目录结构

AddCustom算子工程采用标准的CANN算子工程结构,包含三个主要目录:

AddCustom/
├── op_kernel/              // Kernel侧实现
│   └── add_custom.cpp      // 核函数实现
├── op_host/                // Host侧实现
│   ├── add_custom.cpp      // Tiling函数、形状推导、算子注册
│   └── add_custom_tiling.h // Tiling数据结构定义
└── framework/              // 框架集成
    └── tf_plugin/          // TensorFlow插件
        └── tensorflow_add_custom_plugin.cc

op_kernel目录:存放核函数实现,这些代码会在AI Core上执行。核函数是算子的核心计算逻辑,使用Ascend C编写。

op_host目录:存放Host端代码,包括Tiling策略、形状推导、数据类型推导、算子注册等。这些代码在CPU上执行,负责为核函数准备参数。

framework目录:存放框架集成代码,用于将自定义算子集成到第三方框架(如TensorFlow、PyTorch)中。

10.1.2 算子描述文件

AddCustom.json是算子的描述文件,定义了算子的输入输出规格:

[
    {
        "op": "AddCustom",
        "input_desc": [
            {
                "name": "x",
                "param_type": "required",
                "format": ["ND"],
                "type": ["float16"]
            },
            {
                "name": "y",
                "param_type": "required",
                "format": ["ND"],
                "type": ["float16"]
            }
        ],
        "output_desc": [
            {
                "name": "z",
                "param_type": "required",
                "format": ["ND"],
                "type": ["float16"]
            }
        ]
    }
]

这个文件描述了AddCustom算子有两个输入(x和y),一个输出(z),都是float16类型,ND格式。msOpGen工具会根据这个文件生成算子工程的框架代码。


10.2 Kernel实现详解

10.2.1 KernelAdd类设计

op_kernel/add_custom.cpp实现了KernelAdd类,这是算子的核心计算逻辑:

class KernelAdd {
public:
    __aicore__ inline KernelAdd() {}
    
    // 初始化函数:设置GlobalTensor、初始化队列
    __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z, 
                                uint32_t totalLength, uint32_t tileNum)
    {
        // 计算每个Core处理的数据长度
        this->blockLength = totalLength / AscendC::GetBlockNum();
        this->tileNum = tileNum;
        // 计算每个tile的长度(考虑双缓冲)
        this->tileLength = this->blockLength / tileNum / BUFFER_NUM;
        
        // 设置GlobalTensor,每个Core处理不同的数据块
        xGm.SetGlobalBuffer((__gm__ DTYPE_X *)x + 
                           this->blockLength * AscendC::GetBlockIdx(), 
                           this->blockLength);
        yGm.SetGlobalBuffer((__gm__ DTYPE_Y *)y + 
                           this->blockLength * AscendC::GetBlockIdx(), 
                           this->blockLength);
        zGm.SetGlobalBuffer((__gm__ DTYPE_Z *)z + 
                           this->blockLength * AscendC::GetBlockIdx(), 
                           this->blockLength);
        
        // 初始化队列,每个队列有BUFFER_NUM个缓冲区(双缓冲)
        pipe.InitBuffer(inQueueX, BUFFER_NUM, 
                       this->tileLength * sizeof(DTYPE_X));
        pipe.InitBuffer(inQueueY, BUFFER_NUM, 
                       this->tileLength * sizeof(DTYPE_Y));
        pipe.InitBuffer(outQueueZ, BUFFER_NUM, 
                       this->tileLength * sizeof(DTYPE_Z));
    }
    
    // 处理函数:执行流水线处理
    __aicore__ inline void Process()
    {
        int32_t loopCount = this->tileNum * BUFFER_NUM;
        for (int32_t i = 0; i < loopCount; i++) {
            CopyIn(i);   // 数据加载
            Compute(i);  // 计算
            CopyOut(i);  // 结果写回
        }
    }

Init函数:负责初始化,包括:

  • 计算数据分片:根据总长度和Core数量,计算每个Core处理的数据长度
  • 设置GlobalTensor:每个Core处理不同的数据块,通过GetBlockIdx()获取当前Core的索引
  • 初始化队列:为每个队列分配缓冲区,BUFFER_NUM=2实现双缓冲

Process函数:执行流水线处理,循环调用CopyIn、Compute、CopyOut三个阶段。

10.2.2 数据加载(CopyIn)

__aicore__ inline void CopyIn(int32_t progress)
{
    // 从队列分配LocalTensor
    AscendC::LocalTensor<DTYPE_X> xLocal = inQueueX.AllocTensor<DTYPE_X>();
    AscendC::LocalTensor<DTYPE_Y> yLocal = inQueueY.AllocTensor<DTYPE_Y>();
    
    // 从Global Memory拷贝数据到Local Memory
    AscendC::DataCopy(xLocal, xGm[progress * this->tileLength], 
                      this->tileLength);
    AscendC::DataCopy(yLocal, yGm[progress * this->tileLength], 
                      this->tileLength);
    
    // 将LocalTensor入队
    inQueueX.EnQue(xLocal);
    inQueueY.EnQue(yLocal);
}

CopyIn阶段负责从Global Memory加载数据到Local Memory:

  • 从队列分配LocalTensor:AllocTensor从队列的缓冲区池中分配一个LocalTensor
  • 数据拷贝:使用DataCopy API从Global Memory拷贝到Local Memory
  • 入队:将LocalTensor放入队列,供后续计算使用

10.2.3 计算(Compute)

__aicore__ inline void Compute(int32_t progress)
{
    // 从队列取出LocalTensor
    AscendC::LocalTensor<DTYPE_X> xLocal = inQueueX.DeQue<DTYPE_X>();
    AscendC::LocalTensor<DTYPE_Y> yLocal = inQueueY.DeQue<DTYPE_Y>();
    
    // 分配输出LocalTensor
    AscendC::LocalTensor<DTYPE_Z> zLocal = outQueueZ.AllocTensor<DTYPE_Z>();
    
    // 执行Add计算
    AscendC::Add(zLocal, xLocal, yLocal, this->tileLength);
    
    // 将结果入队
    outQueueZ.EnQue<DTYPE_Z>(zLocal);
    
    // 释放输入LocalTensor
    inQueueX.FreeTensor(xLocal);
    inQueueY.FreeTensor(yLocal);
}

Compute阶段执行实际的计算:

  • 出队:从输入队列取出LocalTensor
  • 分配输出:为输出分配LocalTensor
  • 执行计算:使用Add API进行向量加法
  • 入队:将结果放入输出队列
  • 释放:释放输入LocalTensor,归还到缓冲区池

10.2.4 结果写回(CopyOut)

__aicore__ inline void CopyOut(int32_t progress)
{
    // 从队列取出结果
    AscendC::LocalTensor<DTYPE_Z> zLocal = outQueueZ.DeQue<DTYPE_Z>();
    
    // 从Local Memory拷贝结果到Global Memory
    AscendC::DataCopy(zGm[progress * this->tileLength], zLocal, 
                      this->tileLength);
    
    // 释放LocalTensor
    outQueueZ.FreeTensor(zLocal);
}

CopyOut阶段将结果写回Global Memory:

  • 出队:从输出队列取出结果LocalTensor
  • 数据拷贝:使用DataCopy API从Local Memory拷贝到Global Memory
  • 释放:释放LocalTensor,归还到缓冲区池

10.2.5 流水线执行

三个阶段的流水线执行实现了计算和数据传输的重叠:

时间轴:
Core 0: [CopyIn0] [Compute0] [CopyOut0] [CopyIn1] [Compute1] [CopyOut1] ...
Core 1: [CopyIn0] [Compute0] [CopyOut0] [CopyIn1] [Compute1] [CopyOut1] ...
...

双缓冲优化:
tile0: [CopyIn] -> [Compute] -> [CopyOut]
tile1:         [CopyIn] -> [Compute] -> [CopyOut]

当Compute在执行tile0的计算时,CopyIn可以同时加载tile1的数据,实现流水线并行。

10.2.6 Kernel函数

extern "C" __global__ __aicore__ void add_custom(
    GM_ADDR x, GM_ADDR y, GM_ADDR z, 
    GM_ADDR workspace, GM_ADDR tiling)
{
    // 获取Tiling数据
    GET_TILING_DATA(tiling_data, tiling);
    
    // 创建KernelAdd对象
    KernelAdd op;
    
    // 初始化
    op.Init(x, y, z, tiling_data.totalLength, tiling_data.tileNum);
    
    // 执行处理
    op.Process();
}

Kernel函数是核函数的入口点:

  • __global__:表示这是核函数
  • __aicore__:表示在AI Core上执行
  • 参数:包括输入输出地址、workspace、tiling数据
  • GET_TILING_DATA:宏,用于从tiling参数中提取TilingData

10.3 Host端实现详解

10.3.1 Tiling数据结构

op_host/add_custom_tiling.h定义了Tiling数据结构:

namespace optiling {
BEGIN_TILING_DATA_DEF(TilingData)
TILING_DATA_FIELD_DEF(uint32_t, totalLength);  // 总数据长度
TILING_DATA_FIELD_DEF(uint32_t, tileNum);      // Tile数量
END_TILING_DATA_DEF;

REGISTER_TILING_DATA_CLASS(AddCustom, TilingData)
} // namespace optiling

Tiling数据结构用于在Host和Kernel之间传递参数:

  • totalLength:输入数据的总元素数
  • tileNum:将数据分成多少个tile处理
  • BEGIN_TILING_DATA_DEF/END_TILING_DATA_DEF:宏,用于定义Tiling数据结构
  • REGISTER_TILING_DATA_CLASS:注册Tiling数据类

10.3.2 Tiling函数

op_host/add_custom.cpp中的TilingFunc函数负责计算Tiling参数:

namespace optiling {
const uint32_t BLOCK_DIM = 8;  // 使用8个Core
const uint32_t TILE_NUM = 8;   // 每个Core处理8个tile

static ge::graphStatus TilingFunc(gert::TilingContext *context)
{
    // 创建TilingData对象
    TilingData tiling;
    
    // 获取输入形状的总元素数
    uint32_t totalLength = context->GetInputShape(0)
                          ->GetOriginShape()
                          .GetShapeSize();
    
    // 设置Block维度(Core数量)
    context->SetBlockDim(BLOCK_DIM);
    
    // 设置Tiling参数
    tiling.set_totalLength(totalLength);
    tiling.set_tileNum(TILE_NUM);
    
    // 保存Tiling数据到缓冲区
    tiling.SaveToBuffer(context->GetRawTilingData()->GetData(), 
                       context->GetRawTilingData()->GetCapacity());
    
    // 设置实际使用的数据大小
    context->GetRawTilingData()->SetDataSize(tiling.GetDataSize());
    
    // 设置workspace大小(Add算子不需要workspace,设为0)
    size_t *currentWorkspace = context->GetWorkspaceSizes(1);
    currentWorkspace[0] = 0;
    
    return ge::GRAPH_SUCCESS;
}
} // namespace optiling

Tiling函数的作用:

  • 获取输入形状:从context中获取输入tensor的形状信息
  • 计算Tiling参数:根据输入大小和策略,计算totalLength和tileNum
  • 设置Block维度:告诉框架使用多少个Core并行执行
  • 保存Tiling数据:将Tiling参数保存到缓冲区,传递给Kernel
  • 设置workspace:如果算子需要临时内存,在这里设置大小

10.3.3 形状推导

namespace ge {
static graphStatus InferShape(gert::InferShapeContext *context)
{
    // 获取输入形状
    const gert::Shape *x1_shape = context->GetInputShape(0);
    
    // 获取输出形状
    gert::Shape *y_shape = context->GetOutputShape(0);
    
    // 输出形状等于输入形状(Add算子的输出形状和输入相同)
    *y_shape = *x1_shape;
    
    return GRAPH_SUCCESS;
}
} // namespace ge

InferShape函数用于推导输出tensor的形状。对于Add算子,输出形状和输入形状相同。框架在编译图时会调用这个函数,用于验证图的正确性和分配内存。

10.3.4 数据类型推导

namespace ge {
static graphStatus InferDataType(gert::InferDataTypeContext *context)
{
    // 获取输入数据类型
    const auto inputDataType = context->GetInputDataType(0);
    
    // 输出数据类型等于输入数据类型
    context->SetOutputDataType(0, inputDataType);
    
    return GRAPH_SUCCESS;
}
} // namespace ge

InferDataType函数用于推导输出tensor的数据类型。对于Add算子,输出数据类型和输入相同。

10.3.5 算子注册

namespace ops {
class AddCustom : public OpDef {
public:
    explicit AddCustom(const char *name) : OpDef(name)
    {
        // 定义输入x
        this->Input("x")
            .ParamType(REQUIRED)           // 必需参数
            .DataType({ge::DT_FLOAT16})    // 支持float16
            .Format({ge::FORMAT_ND});       // 支持ND格式
        
        // 定义输入y
        this->Input("y")
            .ParamType(REQUIRED)
            .DataType({ge::DT_FLOAT16})
            .Format({ge::FORMAT_ND});
        
        // 定义输出z
        this->Output("z")
            .ParamType(REQUIRED)
            .DataType({ge::DT_FLOAT16})
            .Format({ge::FORMAT_ND});
        
        // 设置形状和数据类型推导函数
        this->SetInferShape(ge::InferShape)
            .SetInferDataType(ge::InferDataType);
        
        // 配置AI Core执行
        this->AICore()
            .SetTiling(optiling::TilingFunc)  // 设置Tiling函数
            .AddConfig("ascend910")           // 支持ascend910
            .AddConfig("ascend310p")          // 支持ascend310p
            .AddConfig("ascend310b")           // 支持ascend310b
            .AddConfig("ascend910b");         // 支持ascend910b
    }
};

// 注册算子
OP_ADD(AddCustom);
} // namespace ops

算子注册是算子开发的关键步骤:

  • OpDef类:所有自定义算子都继承自OpDef
  • Input/Output:定义算子的输入输出规格
  • ParamType:参数类型,REQUIRED表示必需,OPTIONAL表示可选
  • DataType:支持的数据类型列表
  • Format:支持的数据格式列表
  • SetInferShape/SetInferDataType:设置推导函数
  • AICore配置:设置Tiling函数和支持的芯片型号
  • OP_ADD宏:注册算子到框架

10.4 框架集成

10.4.1 TensorFlow插件

framework/tf_plugin/tensorflow_add_custom_plugin.cc实现了TensorFlow框架集成:

#include "register/register.h"

namespace domi {
// 注册算子信息到GE(Graph Engine)
REGISTER_CUSTOM_OP("AddCustom")
    .FrameworkType(TENSORFLOW)        // 框架类型:TensorFlow
    .OriginOpType("AddCustom")        // TensorFlow中的算子名称
    .ParseParamsByOperatorFn(AutoMappingByOpFn);  // 参数映射函数
}  // namespace domi

这个文件将AddCustom算子注册到TensorFlow框架:

  • REGISTER_CUSTOM_OP:注册自定义算子
  • FrameworkType:指定框架类型
  • OriginOpType:TensorFlow中使用的算子名称
  • ParseParamsByOperatorFn:参数映射函数,AutoMappingByOpFn会自动映射同名参数

10.4.2 框架集成流程

当TensorFlow调用AddCustom算子时:

  1. TensorFlow识别到AddCustom算子
  2. 通过插件找到对应的CANN算子
  3. GE(Graph Engine)调用Tiling函数计算参数
  4. 调用InferShape和InferDataType推导形状和类型
  5. 编译生成执行图
  6. 运行时调用Kernel函数执行计算

10.5 编译和部署流程

10.5.1 使用msOpGen生成工程

msOpGen是CANN提供的算子工程生成工具,可以根据JSON描述文件生成算子工程框架:

# 使用AddCustom.json生成算子工程
msOpGen -i AddCustom.json -o CustomOp

生成的工程包含:

  • 目录结构:op_kernel、op_host、framework等
  • 框架代码:基础的Kernel函数、Tiling函数框架
  • 编译脚本:CMakeLists.txt等

10.5.2 实现算子逻辑

生成工程后,需要实现:

  1. Kernel逻辑:在op_kernel/add_custom.cpp中实现KernelAdd类
  2. Tiling策略:在op_host/add_custom.cpp中实现TilingFunc
  3. 形状推导:实现InferShape和InferDataType
  4. 算子注册:定义AddCustom类并注册

10.5.3 编译算子

# 进入算子工程目录
cd CustomOp

# 创建构建目录
mkdir build && cd build

# 配置CMake
cmake .. -DCMAKE_INSTALL_PREFIX=/path/to/install

# 编译
make -j8

# 安装
make install

编译会生成:

  • 算子库:libcust_opapi.so
  • 头文件:aclnn_add_custom.h等
  • 安装包:custom_opp_*.run

10.5.4 部署算子

# 安装算子包
./custom_opp_ubuntu_x86_64.run

# 验证安装
ls $ASCEND_OPP_PATH/vendors/customize/op_api/

安装后,算子会被部署到ASCEND_OPP_PATH/vendors/customize/op_api/目录,可以在应用中使用。


10.6 关键设计要点

10.6.1 数据分片策略

AddCustom使用简单的均匀分片策略:

  • 总数据长度除以Core数量,得到每个Core处理的数据长度
  • 每个Core再分成多个tile,实现流水线处理
  • 双缓冲(BUFFER_NUM=2)实现计算和数据传输重叠

对于更复杂的算子,可能需要:

  • 考虑数据对齐
  • 处理边界情况
  • 优化内存访问模式

10.6.2 流水线设计

三个阶段的流水线设计:

  • CopyIn:数据加载,可以和其他阶段并行
  • Compute:计算,是性能瓶颈
  • CopyOut:结果写回,可以和其他阶段并行

双缓冲确保每个阶段都有数据可处理,最大化硬件利用率。

10.6.3 多核并行

通过GetBlockIdx()和GetBlockNum()实现多核并行:

  • 每个Core处理不同的数据块
  • Tiling函数设置BLOCK_DIM=8,使用8个Core
  • 数据自动分配到不同Core

10.6.4 内存管理

使用TPipe和TQue管理Local Memory:

  • TPipe:管理整个流水线的内存
  • TQue:管理队列的缓冲区池
  • AllocTensor/FreeTensor:从缓冲区池分配和释放

这种设计避免了手动内存管理,减少了内存碎片。


10.7 与简化版的对比

10.7.1 AddCustom vs AddCustomTiny

AddCustom是完整的算子工程,包含:

  • 完整的Host端实现(Tiling、形状推导、算子注册)
  • 框架集成(TensorFlow插件)
  • 完整的编译和部署流程

AddCustomTiny是极简版本,只包含:

  • 基本的Kernel实现
  • 简单的Tiling函数
  • 算子注册

适合快速验证和学习。

10.7.2 适用场景

使用AddCustom

  • 生产环境
  • 需要框架集成
  • 需要完整的错误处理
  • 需要支持多种芯片

使用AddCustomTiny

  • 学习理解
  • 快速原型
  • 简单算子

学习检查点

学完这一篇,你应该能做到这些:

理解算子工程的完整结构,知道每个目录和文件的作用。掌握Kernel实现的三个阶段(CopyIn、Compute、CopyOut),理解流水线设计。理解Tiling函数的作用,能够根据输入计算Tiling参数。掌握形状和数据类型推导,能够实现InferShape和InferDataType。理解算子注册机制,能够使用OpDef注册自定义算子。了解框架集成方式,知道如何将算子集成到TensorFlow等框架。掌握编译和部署流程,能够独立完成算子的编译和安装。

实践练习

阅读AddCustom代码:仔细阅读AddCustom的完整代码,理解每个部分的作用。

修改Tiling策略:修改TILE_NUM和BLOCK_DIM,观察对性能的影响。

添加数据类型支持:修改算子注册,支持float32类型。

实现其他算子:参考AddCustom,实现Mul、Sub等其他基础算子。

集成到框架:尝试将AddCustom集成到PyTorch或其他框架。


下一步:掌握了完整算子开发流程后,你已经具备了开发自定义算子的能力。可以继续学习更复杂的算子实现,如MatMul、Convolution等,或者学习性能优化技巧,提升算子的执行效率。

2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机,平板、开发板等大奖。

报名链接:https://www.hiascend.com/developer/activities/cann20252

社区地址:https://www.hiascend.com/developer

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐