AI CPU算子的实现包括两部分:
用户需要在算子工程的“cpukernel/impl/xx.h”文件中进行算子类的声明,如下所示:
// CpuKernel基类以及注册宏定义 #include "cpu_kernel.h" // 定义命名空间aicpu namespace aicpu { // 算子类继承CpuKernel基类 class SampleCpuKernel : public CpuKernel { public: ~SampleCpuKernel() = default; // 声明函数Compute,且Compute函数需要重写 uint32_t Compute(CpuKernelContext &ctx) override; }; } // namespace aicpu
用户需要在算子工程的“cpukernel/impl/xx.cc”文件中进行算子的计算逻辑实现,如下所示:
// 引入声明算子类的头文件 #include "sample_kernels.h" namespace { //sample为算子原型中注册的算子的类型,可查看算子原型定义 const char *SAMPLE = "sample"; } // 定义命名空间aicpu namespace aicpu { // 实现自定义算子类的Compute函数 uint32_t SampleCpuKernel::Compute(CpuKernelContext &ctx) { Tensor *x0 = ctx.Input(0); // 对输入tensor进行基本校验,比如判断是否为空等操作 // 可根据获取到的输入tensor x0获取输入的shape,数据等信息 ... Tensor *x1 = ctx.Input(1); // 对输入tensor进行基本校验,比如判断是否为空等操作 // 可根据获取到的输入tensor x1获取输入的shape,数据等信息 ... Tensor *y0 = ctx.Output(0); // 可根据获取到的输出tensor y0获取输出的shape,数据等信息 ... AttrValue *attr = ctx.GetAttr(attr); //获取属性信息,并对属性进行基本校验,比如判断是否为空等操作 ... // 根据输入信息组织计算逻辑,得到输出结果,并将结果设置到输出的tensor中 ... // 动态shape类算子需要额外更新输出的tensor的shape等信息 ... return 0; } // 注册该算子实现 REGISTER_CPU_KERNEL(SAMPLE, SampleCpuKernel); } // namespace aicpu
头文件sample_kernels.h,头文件定义中声明的头文件。
如下所示:
namespace { //sample为算子的OpType const char *SAMPLE = "sample"; }
其中,sample为算子原型中注册的算子的类型,可查看算子原型定义,SAMPLE为声明的指向算子OpType的常量指针。
命名空间的名称aicpu为固定值,基类及相关定义都在aicpu命名空间中,声明如下所示:
namespace aicpu { uint32_t SampleCpuKernel::Compute(CpuKernelContext &ctx) { ... ... }
SampleCpuKernel为头文件中定义的自定义算子类,形参CpuKernelContext为CPU Kernel的上下文,包括算子的输入输出Tensor以及属性等相关信息。
获取输入/输出Tensor相关信息,并进行合法性校验,然后根据输入信息组织计算逻辑,得出计算结果,并将输出结果设置到输出Tensor中。
例如:
// 从context中获取input tensor Tensor *input = ctx.Input(0); // 对输入tensor进行基本校验 // 例如,对获取到的input进行空指针校验 if (input == nullptr) { return 1; } // 获取input tensor的shape信息 auto inputShape = input->GetTensorShape(); for (int32_t i = 0; i < inputShape->GetDims(); ++i) { std::cout << "dim[" << i << "] size:" << inputShape->GetDimSize(i) << std::endl; } // 获取input tensor的DataType DataType inputType = input->GetDataType(); // 获取input tensor的数据地址 auto inputData = input->GetData(); // 获取输出tensor的数据地址以及shape Tensor *output = ctx.Output(0); auto outputShape = output->GetTensorShape(); auto outputData = output->GetData(); // 保存输出结果 outputData[0] = inputData[0];
以上算子实现时使用到API接口介绍可参见AI CPU API。
例如,对于多输入算子,多个tensor的dtype需要保持一致,此时需要校验多个输入的dtype是否一致。若多输入的内在逻辑要求已经在算子原型定义的Verify函数中进行实现,则compute函数中的此校验可不再实现。
可根据算子实际情况来选择是否进行dtype的校验,若某算子仅支持A、B两种数据类型,其他数据类型都不支持,此时可在实现算子计算逻辑前对dtype进行校验,判断dtype是否在支持的dtype列表中。
// 获取第i个输入的类型 auto data_type = ctx.Input(i)->GetDataType(); switch (data_type) { case DT_FLOAT16: return OpCompute<Eigen::half>(...); case DT_FLOAT: return OpCompute<float>(...); case DT_DOUBLE: return OpCompute<double>(...); case DT_INT8: return OpCompute<int8_t>(...); case DT_INT16: return OpCompute<int16_t>(...); ... ... default: return PARAM_INVAILD; }
其中OpCompute函数为算子的计算过程实现函数。
PARAM_INVAILD的定义如下所示:
const uint32_t PARAM_INVAILD = 1;
算子计算逻辑实现时,有以下几点注意点:
例如,对于Less算子,输入为半精度时,用Eigen进行强转。
auto input = reinterpret_cast<Eigen::half *>(input_0->GetData());
说明:
第三方Eigen库还提供了比较系统的矩阵和向量等线性代数相关的运算操作,若您的算子实现涉及到相关操作,可以借助Eigen库实现。
例如,使用Eigen库进行矩阵的定义、初始化,并求取行列式的代码示例如下所示:
#include "Eigen/Dense" int m,n; Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic> eMatrix(m, n); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { eMatrix(i, j) = i * m + j * n; } } //Using eigen to calculate the Determinant float result = eMatrix.determinant();
std::vector<int64_t> dims = {inputData[0],inputData[1],3,4} outputShape ->SetDimSizes(dims);
Sample仓中的UniqueCust算子为动态Shape算子,您可以参考UniqueCust算子的代码实现。
uint32_t blockdim = ctx.GetAttr("block_num")->GetInt(); uint32_t blockid = ctx.GetAttr("block_id")->GetInt();
说明:分块数目是系统根据用户配置的BlockDim切分原则(算子信息库中配置的opInfo.blockDimByIndex)及CPU核数自动计算的。每一个分块都会分配一个“block_id”,“block_id”的取值范围为blockdim-1。
获取“block_num”及“block_id”,用户可自行进行一些基本校验。
例如,若“opInfo.blockDimByIndex”配置为-1,即按照第一个输入参数的元素个数进行BlockDim的切分,则计算偏移量及数据量的代码示例如下:
// 获取第一个输入参数的元素个数 int64_t total = input0->NumElements(); int64_t startpos = 0; int64_t len = total; if (blockdim != 1) { // 计算每一块的最大数据量 uint32_t per_unit = std::ceil(total / blockdim); // 得出本次计算的偏移量 startpos = blockid * per_unit; // 得出本次计算的数据量。 // blockid的取值范围为:0~blockdim-1,为避免最后一块数据存在拖尾,所以当blockid为最后一块时,len取值为total - per_unit * (blockdim - 1) len = blockid < blockdim - 1 ? per_unit : (total - per_unit * (blockdim - 1)); }
完整的分块并行计算的算子样例可参见开源Ascend Sample仓,更多样例可参见自定义算子模板。
REGISTER_CPU_KERNEL(SAMPLE, SampleCpuKernel);
本节设计实现一个对元素求反余弦值的Acos算子。
首先我们对算子进行分析,明确算子的数学表达式,输入输出信息。
y=acos(x)
计算过程是:对输入参数x求反余弦值,并赋值给输出参数y。
通过以上分析,得到Acos算子的设计规格如下:
算子类型(OpType) |
Acos |
||
---|---|---|---|
算子输入 |
name:x |
data type:double |
format:ND |
算子输出 |
name:y |
data type:double |
format:ND |
样例代码如下:
// 头文件定义 #ifndef _ACOS_KERNELS_H_ #define _ACOS_KERNELS_H_ #include "cpu_kernel.h" namespace aicpu { class AcosCpuKernel : public CpuKernel { public: ~AcosCpuKernel() = default; virtual uint32_t Compute(CpuKernelContext &ctx) override; }; } // namespace aicpu #endif
// 源文件Compute函数实现 #include "acos_kernels.h" #include <cmath> namespace { const char *ACOS = "Acos"; } namespace aicpu { uint32_t AcosCpuKernel::Compute(CpuKernelContext &ctx) { // 从context中获取输入tensor和输出tensor Tensor *input = ctx.Input(0); Tensor *output = ctx.Output(0); // 对输入tensor和输出tensor进行校验 if (input == nullptr || output == nullptr) { return 1; } // 分别获取输入tensor和输出tensor的数据地址 auto inputData = static_cast<double *>(input->GetData()); auto outputData = static_cast<double *>(output->GetData()); // 对输入tensor和输出tensor的数据地址进行校验 if (inputData == nullptr || outputData == nullptr) { return 1; } // 获取输入tensor的DataType DataType inputType = input->GetDataType(); // 对输入tensor的DataType进行校验 switch (inputType) { case DT_DOUBLE: break; default: return 1; } // 调用acos函数进行计算 auto num = input->NumElements(); for(int64_t i = 0; i < num; i++ ){ outputData[i] = std::acos(inputData[i]); } return 0; } REGISTER_CPU_KERNEL(ACOS, AcosCpuKernel); } // namespace aicpu
源文件代码具体解析如下:
#include "acos_kernels.h" #include <cmath>
其中acos_kernels.h是Acos算子类声明头文件,cmath是c++标准库头文件。
namespace { const char *ACOS = "Acos"; }
namespace aicpu { uint32_t AcosCpuKernel::Compute(CpuKernelContext &ctx) { ... }
uint32_t AcosCpuKernel::Compute(CpuKernelContext &ctx) { // 从context中获取输入tensor和输出tensor Tensor *input = ctx.Input(0); Tensor *output = ctx.Output(0); // 对输入tensor和输出tensor进行校验 if (input == nullptr || output == nullptr) { return 1; } // 分别获取输入tensor和输出tensor的数据地址 auto inputData = static_cast<double *>(input->GetData()); auto outputData = static_cast<double *>(output->GetData()); // 对输入tensor和输出tensor的数据地址进行校验 if (inputData == nullptr || outputData == nullptr) { return 1; } // 获取输入tensor的DataType DataType inputType = input->GetDataType(); // 对输入tensor的DataType进行校验 switch (inputType) { case DT_DOUBLE: break; default: return 1; } // 调用acos函数进行计算 auto num = input->NumElements(); for(int64_t i = 0; i < num; i++ ){ outputData[i] = std::acos(inputData[i]); } return 0; }
Compute实现中首先对数据做合法性校验,包括:
完成合法性校验后,调用c++标准库的acos接口实现反余弦的计算逻辑。
REGISTER_CPU_KERNEL(ACOS, AcosCpuKernel);