介绍

本教程端到端教会用户完成:将Pytorch训好的模型转化为昇腾计算图表达,然后自定义规则对计算图做融合优化,最后将优化后的计算图做编译运行,从而针对性的深度优化用户自定义模型的推理性能。

环境准备

  1. 安装CANN开发套件包
    CANN开发套件软件包请从link获取,支持的安装方式及操作系统请参见配套版本的用户手册
使用默认路径安装: ./Ascend-cann-toolkit_<cann_version>_linux-<arch>.run --install
#若使用root用户安装,安装完成后相关软件存储在/usr/local/Ascend/ascend-toolkit/latest路径下。
#若使用非root用户安装,安装完成后相关软件存储在$HOME/Ascend/ascend-toolkit/latest路径下。

指定路径安装: ./Ascend-cann-toolkit_<cann_version>_linux-<arch>.run --install --install-path=${install_path}
#安装完成后,相关软件存储在${install_path}指定路径下。
  1. 安装Pytorch
    参见Pytorch官网获取详细安装教程。

如何将训练好的Pytorch模型(以Resnet50为例)转化为昇腾计算图,然后调用昇腾内置的图融合算法进行编译优化,最后执行计算图推理?

  1. Pytorch模型转化为onnx格式
import torch
import torch.nn as  nn
import torch.nn.functional as F
import torch.onnx
import torchvision
from torchvision import models

def convert_onnx():
    model = models.resnet50()
    resnet50_model = torch.load('resnet50.pth', map_location='cpu')    #根据实际文件路径名称修改
    model.load_state_dict(resnet50_model) 

    batch_size = 1  #批处理大小
    input_shape = (3, 224, 224)   #输入数据,改成自己的输入shape

    # 模型设置为推理模式
    model.eval()

    dummy_input = torch.randn(batch_size, *input_shape) #  定义输入shape
    torch.onnx.export(model, 
                      dummy_input, 
                      "model.onnx",  #onnx模型文件输出路径
                      input_names = ["input"],   # 构造输入名
                      output_names = ["output"],    # 构造输出名
                      opset_version=11,   
                      dynamic_axes={"input":{0:"batch_size"}, "output":{0:"batch_size"}})  #支持输出动态轴

if __name__ == "__main__":
    convert_onnx()
  1. 将onnx模型转化为昇腾计算图并执行图编译优化和推理
#include "ge_api.h"
#include "onnx_parser.h"


// 二进制格式读取onnx模型文件
FILE *pFile = fopen("model.onnx", "rb" );
if(pFile==NULL)
{
    fputs("File error",stderr);
    exit(1);
}

// 获取文件size
fseek(pFile, 0, SEEK_END);
long lSize = ftell(pFile);
rewind(pFile);

// 分配相应size的内存buffer
char *buffer =(char*) malloc(sizeof(char)*lSize);
if(buffer == NULL)
{
    fputs("Memory error", stderr); 
    exit(2);
}

// copy 二进制文件到内存buffer
size_t result = fread(buffer, 1, lSize, pFile);
if(result != lSize)
{
    fputs("Reading error", stderr);
    exit(3);
}

//将内存buffer里的数据解析为GE计算图对象
std::map<ge::AscendString, ge::AscendString> parser_params= {
            {ge::AscendString(ge::ir_option::INPUT_FP16_NODES), ge::AscendString("input1;input2")},
            {ge::AscendString(ge::ir_option::OUTPUT), ge::AscendString("newIssue")}};
ge::Graph compute_graph;
auto onnxStatus = ge::aclgrphParseONNXFromMem(buffer, result, parser_params, compute_graph);


//计算图编译和运行
std::map<AscendString, AscendString>config = {{"ge.exec.deviceId", "0"},  //可以通过config配置传入ge运行的初始化信息,配置参数ge.exec.deviceId和ge.graphRunMode,
                                              {"ge.graphRunMode", "1"}};  //分别用于指定GE实例运行设备,图执行模式(在线推理请配置为0,训练请配置为1)
                                                                         
Status ret = ge::GEInitialize(config);  //初始化
std::map <AscendString, AscendString> options;
ge::Session *session = new Session(options); //创建运行实例
if(session == nullptr) {
  std::cout << "Create session failed." << std::endl;
  ge::GEFinalize();  //释放资源
  return FAILED;
}
uint32_t graph_id = 0;
Status ret = session->AddGraph(graph_id, compute_graph); //运行实例添加计算图
if(ret != SUCCESS) {
  ge::GEFinalize(); //释放资源
  delete session;
  return FAILED;
}
std::vector<ge::Tensor> input; //定义输入tensor
std::vector<ge::Tensor> output; //定义输出tensor
ret = session->RunGraph(graph_id, input, output); //执行计算图推理,结果保存在输出tensor
if(ret != SUCCESS) {
  ge::GEFinalize(); //释放资源
  delete session;
  return FAILED;
}

如何自定义计算图融合规则,并作用于图编译优化过程中,从而提升计算图运行效率?

  1. 参照下面的示例代码,实现自己设计的图融合规则
#include <iostream>
//自定义Pass接口头文件
#include "register_custom_pass.h"
//新增算子头文件
#include "all_ops.h"

/*
如果使用Ascend C自定义了算子,需要包含如下头文件:
#include "CANN软件安装目录/latest/opp/vendors/customize/op_proto/inc/op_proto.h"
*/

namespace {
constexpr const char *kOpNameAdd = "add";
constexpr const char *kOpNameMatMul = "matmul";
constexpr const char *kOpNameGEMM = "gemm";
constexpr const char *kOpNameAlpha = "alpha";
constexpr const char *kOpNameBeta = "beta";
constexpr const char *kAttrNameTransposeA = "transpose_a";
constexpr const char *kAttrNameTransposeB = "transpose_b";
constexpr int32_t kIndex0 = 0;
constexpr int32_t kIndex1 = 1;
constexpr int32_t kIndex2 = 2;
constexpr int32_t kIndex3 = 3;
constexpr int32_t kIndex4 = 4;

// 1.遍历所有节点,寻找MatMul和Add节点
bool FindNodes(GraphPtr &graph, GNode &src_node, GNode &dst_node) {
    auto all_nodes = graph->GetAllNodes();
    bool find_src_node = false;
    bool find_dst_node = false;
    for (auto &node: all_nodes) {
        AscendString node_name;
        auto ret = node.GetName(node_name);
        if (node_name == kOpNameMatMul) {
            src_node = node;
            find_src_node = true;
            cout << "Find src node: MatMul." << endl;
        } else if (node_name == kOpNameAdd) {
            dst_node = node;
            find_dst_node = true;
            cout << "Find dst node: Add." << endl;
        }
    }
    return (find_src_node && find_dst_node);
}
// 2.判断MatMul和Add节点是否有连边关系
bool CheckNodesHaveEdge(GraphPtr &graph, const GNode &src_node, const GNode &dst_node) {
    for (auto &[out_node, _]: src_node.GetOutDataNodesAndPortIndexs(kIndex0)) {
        AscendString node_name;
        auto ret = out_node->GetName(node_name);
        if (node_name == kOpNameAdd) {
            return true;
        }
    }
    return false;
}
// 3.创建和添加GEMM节点
void CreateGEMMNode(GraphPtr &graph, const GNode &src_node, GNode &node_gemm) {
    bool transpose_a = false;
    bool transpose_b = false;
    src_node.GetAttr(kAttrNameTransposeA, transpose_a);
    src_node.GetAttr(kAttrNameTransposeB, transpose_b);
    constexpr float kValue1 = 1;
    TensorDesc alpha_desc(ge::Shape({1}), FORMAT_ND, DT_FLOAT);
    Tensor alpha_tensor(alpha_desc, reinterpret_cast<const uint8_t *>(&kValue1), sizeof(float));
    auto alpha = op::Const(kOpNameAlpha).set_attr_value(alpha_tensor);
    TensorDesc beta_desc(ge::Shape({1}), FORMAT_ND, DT_FLOAT);
    Tensor beta_tensor(beta_desc, reinterpret_cast<const uint8_t *>(&kValue1), sizeof(float));
    auto beta = op::Const(kOpNameBeta).set_attr_value(beta_tensor);

    auto gemm = op::GEMM(kOpNameGEMM);
    gemm.set_attr_transpose_a(transpose_a)
        .set_attr_transpose_b(transpose_b);
    gemm.update_input_desc_alpha(alpha_desc);
    gemm.update_input_desc_beta(beta_desc);

    auto node_alpha = graph->AddNodeByOp(alpha);
    auto node_beta = graph->AddNodeByOp(beta);
    node_gemm = graph->AddNodeByOp(gemm);

    auto ret = graph->AddDataEdge(node_alpha, kIndex0, node_gemm, kIndex3);
    ret = graph->AddDataEdge(node_beta, kIndex0, node_gemm, kIndex4);
}
// 4.添加新节点的输入输出
bool AddInputsAndOutputs(GraphPtr &graph, const GNode &src_node, const GNode &dst_node, GNode &node_gemm) {
    auto [a, a_output_index] = src_node.GetInDataNodesAndPortIndexs(kIndex0);
    auto [b, b_output_index] = src_node.GetInDataNodesAndPortIndexs(kIndex1);
    int32_t add_node_c_input_index = -1;
    for (size_t i = 0; i < dst_node.GetInputsSize(); ++i) {
        auto [in_node, _] = dst_node.GetInDataNodesAndPortIndexs(i);
        AscendString node_name;
        auto ret = in_node->GetName(node_name);
        if (node_name != kOpNameMatMul) {
            add_node_c_input_index = i;
            break;
        }
    }
    if (add_node_c_input_index == -1) {
        return false;
    }
    auto [c, c_output_index] = dst_node.GetInDataNodesAndPortIndexs(add_node_c_input_index);
    auto ret = graph->AddDataEdge(*a, a_output_index, node_gemm, kIndex0);
    if (ret != GRAPH_SUCCESS) {
        return false;
    }
    ret = graph->AddDataEdge(*b, b_output_index, node_gemm, kIndex1);
    ret = graph->AddDataEdge(*c, c_output_index, node_gemm, kIndex2);

    TensorDesc input_desc_a;
    ret = src_node.GetInputDesc(kIndex0, input_desc_a);
    ret = node_gemm.UpdateInputDesc(kIndex0, input_desc_a);

    TensorDesc input_desc_b;
    ret = src_node.GetInputDesc(kIndex1, input_desc_b);
    ret = node_gemm.UpdateInputDesc(kIndex1, input_desc_b);

    TensorDesc input_desc_c;
    ret = dst_node.GetInputDesc(add_node_c_input_index, input_desc_c);
    ret = node_gemm.UpdateInputDesc(kIndex2, input_desc_c);

    TensorDesc output_desc_y;
    ret = dst_node.GetOutputDesc(kIndex0, output_desc_y);
    ret = node_gemm.UpdateOutputDesc(kIndex0, output_desc_y);
    return true;
}
// 5.删除旧节点和其连边关系,连接新GEMM节点和输出节点
void RemoveOldNodesEdgesAndAddGemmOutput(GraphPtr &graph, GNode &src_node, GNode &dst_node, GNode &node_gemm) {
    vector<GNode> node_vec{src_node, dst_node};
    for (auto &node: node_vec) {
        for (size_t i = 0; i < node.GetInputsSize(); ++i) {
            auto [in_node, in_id] = node.GetInDataNodesAndPortIndexs(i);
            if (in_node != nullptr) {
                auto ret = graph->RemoveEdge(*in_node, in_id, node, i);
            }
        }
    }

    for (auto &[out_node, out_id]: dst_node.GetOutDataNodesAndPortIndexs(kIndex0)) {
        if (out_node != nullptr) {
            auto ret = graph->RemoveEdge(dst_node, kIndex0, *out_node, out_id);
            ret = graph->AddDataEdge(node_gemm, kIndex0, *out_node, out_id);
        }
    }

    for (auto &node: node_vec) {
        auto ret = graph->RemoveNode(node);
    }
}
} // namespace

// |o>-----------------------------------
// |o>    a  b
// |o>    \ /              a   b    c
// |o>   MatMul  c   ==>   \   |   /
// |o>     \    /            GEMM
// |o>      Add
// |o>-----------------------------------
// 融合说明:本例识别上图中左边的MatMul+Add结构并通过图修改接口替换为右边的单个GEMM节点
graphStatus FuseMatMulAndAddPass(GraphPtr &graph, CustomPassContext &custom_context) {
    cout << "FuseMatMulAndAddPass begin." << endl;
    GNode src_node;
    GNode dst_node;
    // 1.遍历所有节点,寻找MatMul和Add节点
    if (!FindNodes(graph, src_node, dst_node)) {
        cout << "Do not find MatMul or Add node." << endl;
        return GRAPH_SUCCESS;
    }

    // 2.判断MatMul和Add节点是否有连边关系
    if (!CheckNodesHaveEdge(graph, src_node, dst_node)) {
        cout << "There is no edge between src and dst node." << endl;
        return GRAPH_SUCCESS;
    }

    // 3.创建和添加GEMM节点
    GNode node_gemm;
    CreateGEMMNode(graph, src_node, node_gemm);

    // 4.添加新节点的输入输出
    if (!AddInputsAndOutputs(graph, src_node, dst_node, node_gemm)) {
        custom_context.SetErrorMessage("Add inputs and outputs failed.");
        return -1;
    }

    // 5.删除旧节点和其连边关系,连接新GEMM节点和输出节点
    RemoveOldNodesEdgesAndAddGemmOutput(graph, src_node, dst_node, node_gemm);

    cout << "FuseMatMulAndAddPass end." << endl;
    return GRAPH_SUCCESS;
}

REGISTER_CUSTOM_PASS("FuseMatMulAndAddPass").CustomPassFn(FuseMatMulAndAddPass);
  1. 把自己实现的图融合c++代码编译成以".so"结尾的动态库文件
  2. 把上述".so"动态库文件复制到${CANN安装后软件存储路径}/opp/vendors/xxx/custom_fusion_passes/ 目录下 (xxx为自定义目录,且为必选项,custom_fusion_passes目录下不能有子目录)
注意:如果自定义图融合规则中需要用到新算子,请使用Ascend C工程化算子开发方式完成自定义算子实现和编译部署。
Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐