Compute函数实现
Compute函数实现示例如下,主要包括合法性校验和计算逻辑实现两部分,可根据需要在合适的地方调用Dump日志接口,打印相关调试信息。示例中使用到的API接口介绍可参见AI CPU API。
// 从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) { // 根据需要调用Dump日志接口,打印相关调试信息 CUST_KERNEL_LOG_DEBUG(ctx, "dim[%d] size:%ld.", i, inputShape->GetDimSize(i)); } // 获取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];
合法性校验
- 对获取到的Input进行空指针校验,此校验必选。
- 对输入输出个数进行校验。
- 对算子输入的内在逻辑进行校验。
例如,对于多输入算子,多个tensor的dtype需要保持一致,此时需要校验多个输入的dtype是否一致。若多输入的内在逻辑要求已经在算子原型定义的Verify函数中进行实现,则compute函数中的此校验可不再实现。
- 对dtype的校验。
可根据算子实际情况来选择是否进行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; }
算子计算逻辑实现时,有以下几个注意点:
- 由于C++自身不支持半精度浮点类型,对于半精度数据类型的计算实现,可使用第三方库Eigen来表示(建议使用3.3.9版本),具体可参考资料:LINK。
例如,对于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();
- 对于动态shape算子,无法根据算子原型定义的InferShape推导得到输出tensor的shape,所以需要在Compute函数中完成输出shape的计算与更新。
std::vector<int64_t> dims = {inputData[0],inputData[1],3,4} outputShape ->SetDimSizes(dims);
- 算子计算过程可分块并行执行,这样可以有效地使用昇腾AI处理器的硬件资源,使性能达到最优。
- 分块并行为性能提升的一个手段,开发者可根据对算子的性能要求选择是否需要进行并行计算。
- 分块并行执行的功能,仅支持输入参数间独立运算的场景,若输入参数间存在数据依赖,则无法进行分块并行计算。
- 使用此功能时,需要在算子信息库定义中设置“opInfo.flagSupportBlockDim”为“True”,并设置“opInfo.functionName”为“RunCpuKernelWithBlock”。
- 首先使用GetAttr接口获取分块数目以及本次计算的分块ID,示例如下:
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仓,更多样例可参见自定义算子模板。
日志Dump功能使用
日志Dump功能用于记录AI CPU算子执行过程中的日志信息,方便进行AI CPU算子的功能调测。使用步骤如下:
- 根据需要在Compute函数中调用日志dump接口输出相应级别的日志信息,接口的具体介绍请参见Dump日志接口。代码示例如下:
uint32_t UniqueCpuKernel::Compute(CpuKernelContext &ctx) { Tensor *param_tensor = ctx.Input(0); if (param_tensor == nullptr) { return 1; } auto param_shape = param_tensor->GetTensorShape(); if (param_shape == nullptr) { return 1; } int64_t p_size = 1; for (int i = 0; i < param_shape->GetDims(); ++i) { p_size *= param_shape->GetDimSize(i); } CUST_KERNEL_LOG_DEBUG(ctx, "Cust UniqueCpuKernel Compute, p_size is %ld.", p_size); ... }
- (可选)通过AI CPU 算子信息库中opInfo.workspaceSize参数配置用于记录AICPU算子日志信息的内存大小。默认值为2KB。具体参数说明参见AI CPU算子信息库。
- 算子运行之前开启Dump功能,使得日志Dump功能生效。如何开启Dump功能依赖于具体的网络运行方式。以TensorFlow在线推理为例,sess.run模式下,通过session配置enable_dump、dump_path、dump_mode配置Dump参数,示例如下:
import tensorflow as tf import numpy as np sess_config = tf.compat.v1.ConfigProto() from npu_bridge.estimator import npu_ops custom_op = sess_config.graph_options.rewrite_options.custom_optimizers.add() custom_op.name = "NpuOptimizer" custom_op.parameter_map["enable_data_pre_proc"].b = True custom_op.parameter_map["use_off_line"].b = True custom_op.parameter_map["min_group_size"].b = 1 # enable_dump:是否开启dump功能 custom_op.parameter_map["enable_dump"].b = True # dump_path:dump数据存放路径 custom_op.parameter_map["dump_path"].s = tf.compat.as_bytes("/test/log") # dump模式配置为all时,可以Dump AI CPU日志信息 custom_op.parameter_map["dump_mode"].s = tf.compat.as_bytes("all") tensor = tf.constant([1,2,3,4,5,6,7,8,9], dtype=tf.float32) output, idx = tf.raw_ops.Unique(x=tensor) with tf.compat.v1.Session(config=sess_config) as sess: print("data = ", sess.run([output,idx])) print("end proc")
算子运行完成后,在Dump数据存放路径下会有日志Dump文件生成,文件名命名规则格式为{op_type}.{op_name}.{taskid}.{stream_id}.{timestamp},其中{op_type}表示算子类型,{op_name}表示算子名称,{taskid}表示调用算子计算接口的taskId,{stream_id}表示算子具体执行的流Id,{timestamp}表示时间戳。
- 使用开发套件包中的Dump文件解析工具解析日志Dump文件。开发套件包Ascend-cann-toolkit安装请参考。安装完成后,执行{install_path}/tools/operator_cmp/compare路径下的dump_parser.py脚本({install_path}为套件包安装目录),即可实现对日志Dump文件的解析。
命令示例和参数说明如下:
python3 dump_parser.py save_log -d dump_file [-out output]
表1 参数说明表 参数名
描述
是否必选
-d
--dump_file
待解析的日志Dump文件。
是
-out
--output
解析后落盘日志的存放目录,默认为当前路径。
否
解析后落盘日志的命名规则为{dump_file_name}.{index}.log,其中{dump_file_name}为日志Dump文件的名称,{index}为日志文件索引号。例如, 执行如下命令,会在当前目录生成文件名为Unique.Unique.3.2.1671845532156774.0.log的日志文件。
python3 dump_parser.py save_log –d Unique.Unique.3.2.1671845532156774
网络运行时,使能exception_dump开关,AICPU算子执行异常时,会生成异常算子的日志Dump信息,解析方法同步骤4。