# 前言

本篇通流程脑图和代码介绍 Tengine 推理引擎的推理流程。本篇是第二篇。第一篇地址Tengine 工程地址

作为初学者,错误在所难免,还望不吝赐教。

# 介绍

Tengine

Tengine 由 OPEN AI LAB 主导开发,该项目实现了深度学习神经网络模型在嵌入式设备上的快速、高效部署需求。为实现在众多 AIoT 应用中的跨平台部署,该项目使用 C 语言进行核心模块开发,针对嵌入式设备资源有限的特点进行了深度框架裁剪。同时采用了完全分离的前后端设计,有利于 CPU、GPU、NPU 等异构计算单元的快速移植和部署,降低评估、迁移成本。

# 总流程图

总流程图:

Tengine推理流程图

init_tengine()create_graph() 流程位于第一篇地址中。

# prerun_graph_multithread()

prerun_graph_multithread推理流程图

该函数是神经网络模型运行前的与运行流程。从流程图中看,其主要调用了三个函数。

# 1.infer_ir_graph_shape()

推理引擎要根据输入 tensor 的维度来推理输出 tensor 的维度。例如某个卷积节点的输入维度是【1,32,128,128】,依据 kernel 大小计算出输出维度【1,64,128,128】。所以该函数会按顺序执行所有节点,并调用每个节点的 op->infer_shape 方法(前述提到过),完成整个模型的 tensor 维度推理。

# 2.optimizer->split_graph()

切分子图。这是一个为了实现多设备运行而实现的方法,即通过切分子图实现。

当前已有一个总图,包含模型所有的节点。而设备可能包括 CPU、GPU、NPU、DNNL 等等,而这些设备可能仅支持一部分算子,这就要求将总图切分成设备支持的子图,和设备不支持的子图。设备支持的子图交给设备运行,不支持的交给 CPU 执行。

经过这个函数之后,子图就变成一各执行单元。

# 3.schedule->prerun()

预运行,其调用的是 interface->prerun() ,即前述的,和设备相关的接口中的 prerun() 函数。其又调用了三个函数:

create_exec_graph() :为每个子图创建执行子图。

通过 find_node_ops() 函数,找到节点的所有执行方式。前述卷积算法,就注册了三种执行方式。

ret = register_conv_ref_op();
    ret = register_conv_dw_hcl_x86_op();
    ret = register_conv_hcl_x86_op();

找到所有执行方式之后,调用每种执行方式的 node_ops->score() 函数,获得一个得分。例如 conv_hcl_x86_op 这种实现方式,其 score() 函数如下。它根据该节点的参数返回一个得分。

static int score(struct node_ops* node_ops, struct exec_graph* exec_graph, struct node* exec_node)
{
    struct node* ir_node = exec_node;
    struct graph* ir_graph = ir_node->graph;
    struct tensor* input_tensor = get_ir_graph_tensor(ir_graph, ir_node->input_tensors[0]);
    struct tensor* output_tensor = get_ir_graph_tensor(ir_graph, ir_node->output_tensors[0]);
    struct conv_param* param = (struct conv_param*)exec_node->op.param_mem;
    int group = param->group;
    int kernel_h = param->kernel_h;
    int kernel_w = param->kernel_w;
    int in_c = input_tensor->dims[1] / group;
    int out_c = output_tensor->dims[1] / group;
    if (input_tensor->data_type != TENGINE_DT_FP32 && input_tensor->data_type != TENGINE_DT_UINT8 && input_tensor->data_type != TENGINE_DT_INT8)
        return 0;  // 返回 0 得分,意思是无法使用
    if (group != 1)
        return 0;  // 返回 0 得分,意思是无法使用
    return OPS_SCORE_PREFER;   // 返回 OPS_SCORE_PREFER 得分,意思是推荐使用
}

得分分为以下几档:

#define OPS_SCORE_STATIC 10000
#define OPS_SCORE_BEST   8000
#define OPS_SCORE_PREFER 6000
#define OPS_SCORE_CANDO  4000
#define OPS_SCORE_NOTSUP 2000

推理引擎将选择得分最高的作为执行方法,而得分低于 4000 的方法不可使用。

下一步是调用 node_ops->init_node() 函数,是初始化节点预留的接口。

通过 prerun_exec_graph() 函数调用 node_ops->prerun() ,这个是节点预运行而预留的接口。

# run_graph()

run_graph推理流程图

到这里才开始实际执行图的每个节点。

和前述一样,还是调用和设备相关的接口 interface->run() ,先调用每个算子的 node_ops->reshape() 函数,从工程中来看该函数仍然是在做维度推理的工作,与前述的 infer_shape() 相同。似乎是重复的做法,不过不同之处在于前述的 infer_shape() 只在模型构建完成之后运行一次,而这个 reshape() 函数在算子每次执行之前都要调用。

可能是为了防止模型的每次运行,输入 tensor 大小都不一样?毕竟神经网络模型通常会不断输入数据,重复运行。(当开发者确定每次输入输入维度一致时,这里的运算就有些浪费时间了。)

然后调用算子的具体执行: node_ops->run()

# postrun_graph()

postrun_graoh推理流程图

图的后处理。

调用每个节点的后运行 node_ops->postrun()

调用子图的释放函数 release_exec_graph() ,调用每个节点的释放函数 node_ops->release_node()

# destroy_graph()

destroy_graph推理流程图

调用整个图的释放函数 interface->release_graph()

一张脑图就帮我们整体把握 Tengine 推理引擎。

# 后记

本博客目前以及可预期的将来都不会支持评论功能。各位大侠如若有指教和问题,可以在我的 github 项目 或随便一个项目下提出 issue,或者知乎 私信,并指明哪一篇博客,我看到一定及时回复!

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

XianMu WeChat Pay

WeChat Pay

XianMu Alipay

Alipay