昇腾计算图融合算法开发教程
本教程端到端教会用户完成:将Pytorch训好的模型转化为昇腾计算图表达,然后自定义规则对计算图做融合优化,最后将优化后的计算图做编译运行,从而针对性的深度优化用户自定义模型的推理性能。Pytorch模型转化为onnx格式return xreturn xreturn xif stride!resnet50_model = torch.load('resnet50.pth', map_locatio
·
介绍
本教程端到端教会用户完成:将Pytorch训好的模型转化为昇腾计算图表达,然后自定义规则对计算图做融合优化,最后将优化后的计算图做编译运行,从而针对性的深度优化用户自定义模型的推理性能。
环境准备
使用默认路径安装: ./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}指定路径下。
- 安装Pytorch
参见Pytorch官网获取详细安装教程。
如何将训练好的Pytorch模型(以Resnet50为例)转化为昇腾计算图,然后调用昇腾内置的图融合算法进行编译优化,最后执行计算图推理?
- 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()
- 将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;
}
如何自定义计算图融合规则,并作用于图编译优化过程中,从而提升计算图运行效率?
- 参照下面的示例代码,实现自己设计的图融合规则
#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);
- 把自己实现的图融合c++代码编译成以".so"结尾的动态库文件
- 把上述".so"动态库文件复制到${CANN安装后软件存储路径}/opp/vendors/xxx/custom_fusion_passes/ 目录下 (xxx为自定义目录,且为必选项,custom_fusion_passes目录下不能有子目录)
注意:如果自定义图融合规则中需要用到新算子,请使用Ascend C工程化算子开发方式完成自定义算子实现和编译部署。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)