下载
中文
注册

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列表中。

计算逻辑实现

完成入参校验后,根据算子输入支持的数据类型分别进行计算,伪代码如下,其中OpCompute函数为算子计算过程实现函数。
// 获取第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);

    Sample仓中的UniqueCust算子为动态Shape算子,您可以参考UniqueCust算子的代码实现。

  • 算子计算过程可分块并行执行,这样可以有效地使用昇腾AI处理器的硬件资源,使性能达到最优。
    • 分块并行为性能提升的一个手段,开发者可根据对算子的性能要求选择是否需要进行并行计算。
    • 分块并行执行的功能,仅支持输入参数间独立运算的场景,若输入参数间存在数据依赖,则无法进行分块并行计算。
    • 使用此功能时,需要在算子信息库定义中设置“opInfo.flagSupportBlockDim”为“True”,并设置“opInfo.functionName”为“RunCpuKernelWithBlock”。
    1. 首先使用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”,用户可自行进行一些基本校验。

    2. 计算本次计算(当前分块的计算)的偏移量及数据量。

      例如,若“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));
      }
    3. 进行算子的计算逻辑的实现。
      以Add算子为例,代码示例如下所示:
      for (int i = startpos; i < startpos + len; i++) {
        y[i] = x0[i] + x1[i];
      } 

    完整的分块并行计算的算子样例可参见开源Ascend Sample仓,更多样例可参见自定义算子模板

日志Dump功能使用

日志Dump功能用于记录AI CPU算子执行过程中的日志信息,方便进行AI CPU算子的功能调测。使用步骤如下:

  1. 根据需要在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);
        ...
    }
  2. (可选)通过AI CPU 算子信息库中opInfo.workspaceSize参数配置用于记录AICPU算子日志信息的内存大小。默认值为2KB。具体参数说明参见AI CPU算子信息库
  3. 算子运行之前开启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}表示时间戳。

  4. 使用开发套件包中的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