docs/develop_guides/add_hardware.md
如下图所示, Paddle Lite 整个推理的过程,可以简单分成分析( Analysis phase )和执行( Execution phase )两个阶段,分析阶段包括 Paddle 模型文件的加载和解析、计算图的转化、图分析和优化、运行时程序的生成和执行等步骤。具体地,
模型文件的加载和解析 Paddle 模型由程序( Program )、块( Block )、算子( Operator )和变量( Variable )组成(如下图所示,程序由若干块组成,块由若干算子和变量组成,变量包括中间变量和持久化变量,如卷积的权值),经序列化保存后形成 Combined 和 Non-combined 两种形式的模型文件, Non-combined 形式的模型由一个网络拓扑结构文件 model 和一系列以变量名命名的参数文件组成, Combined 形式的模型由一个网络拓扑结构文件 model 和一个合并后的参数文件 params 组成,其中网络拓扑结构文件是基于 Protocol Buffers 格式以 Paddle proto 文件规则序列化后的文件。现在以 Non-combined 格式的 Paddle 模型为例,将网络拓扑结构文件(要求文件名必须是 model )拖入到 Netron 工具即可图形化显示整个网络拓扑结构。
了解更多细节,可以访问具体的代码实现
计算图的转化 将每个块按照如下规则生成对应的计算图的过程:每个算子或变量都对应计算图的一个节点,节点间的有向边由算子的输入、输出决定(依赖关系确定边的方向),算子节点与变量节点相邻。为了方便调试,分析阶段的各个步骤都会将计算图的拓扑结构以 DOT 格式的文本随 log 打印,可以将 DOT 文本复制、粘贴到 webgraphviz 进行可视化,如下图所示,黄色矩形节点为算子,椭圆形节点为变量。
了解更多细节,可以访问具体代码实现
图分析和优化 将一系列 pass (优化器,用于描述一个计算图优化生成另一个计算图的算法过程)按照一定的顺序依次应用到每个块对应的计算图的过程,包括量化信息处理、算子融合、 Kernel 选择、类型转化、上下文创建、内存复用优化和子图检测等,实现不同设备的适配、高效的计算和更少的内存占用。其中,算子融合作为一种行之有效的优化策略,普遍存在于各种推理框架中,它通过相邻算子间的融合,减少访存和计算量,有效提高模型的整体性能,例如前一步骤的计算图中, conv_bn_fuse_pass 、 conv_activation_fuse_pass 分别以 conv2d + batch_norm 和 conv2d + relu 为 pattern ,先后搜索整个计算图并完成融合,如下图所示, conv2d + batch_norm + relu 结构,经过前面的 pass 处理后只保留了 1 个 conv2d 算子。
了解更多细节,可以访问具体代码实现
diff --git a/lite/core/optimizer/optimizer.cc b/lite/core/optimizer/optimizer.cc
--- a/lite/core/optimizer/optimizer.cc
+++ b/lite/core/optimizer/optimizer.cc
class Optimizer {
"weight_quantization_preprocess_pass", //
"lite_conv_elementwise_fuse_pass", // conv-elemwise-bn
"lite_conv_bn_fuse_pass", //
+ "graph_visualize_pass",
"lite_conv_elementwise_fuse_pass", // conv-bn-elemwise
"lite_conv_conv_fuse_pass", //
// TODO(Superjomn) Refine the fusion related design to select fusion
运行时程序的生成和执行 按照拓扑顺序遍历优化后的计算图,生成算子和 Kernel 列表的过程,它基于 generate_program_pass 实现。具体地,只遍历计算图中的算子节点,提取所携带的算子和 Kernel (经过 static_kernel_pick_pass 选取的、适合目标硬件的、最优的 Kernel )对象,以 Instruction 封装后按顺序依次存放到 RuntimeProgram 对象。运行时程序的执行也非常简单,即依次遍历 RuntimeProgram 对象存储的每个 Instruction ,调用其算子对象的 CheckShape 和 InfereShape 方法,最后执行 Kernel 对象的 Launch 方法。
按照层次硬件提供给开发者的接口一般可以分为两类:
通用编程接口( Low Level )
优点是灵活,但缺点也显而易见,性能的好坏取决于负责接入框架的研发同学的能力和经验,更依赖对硬件的熟悉程度;
中间表示层( Intermediate Representation , IR )接口( High Level )
优点是屏蔽硬件细节,模型的优化、生成和执行由运行时库完成,对负责接入框架的研发同学要求较低,性能取决于硬件厂商(或 IP 提供商)的研发能力,相对可控;
提供这两类接口的硬件可分别按照如下两种接入方式接入到框架:
主要涉及 Paddle Lite 架构图中算子、Kernel层的硬件适配工作,具体是在 lite/kernels 下增加待新增硬件的目录,为每个算子实现待新增硬件的 Kernel ,具体可参考新增 OP 中"添加 Argmax Kernel 并绑定"步骤;
为了将硬件细节与 Kernel 的实现剥离,减少冗余代码,建议在 lite/backends 目录下增加待新增硬件的目录,利用硬件提供的编程接口实现诸如 gemm 等通用数学运算,向 Kernel 提供统一的数学运算接口;
其它诸如添加新增硬件的 Target 、 Place 、 Context 等方面的内容可参考即将详细介绍的"子图接入方式"中的相关章节。
硬件要求提供以下接口:
具体可参考华为 HiAI DDK v330 、瑞芯微 rknpu_ddk 和 MTK Neuron Adapter(类似 Android NNAPI )进行接口设计。
什么是子图? 将计算图依据某种规则分割为多个部分,每个部分都被称为一个子图,它包含一个或多个算子和变量,规则一般依据硬件支持能力而定。
框架如何实现子图检测和融合? 在" Paddle Lite 是如何工作的?"章节中的"图分析和优化"步骤曾提到了子图检测 Pass ,它依据硬件对 Paddle 算子的支持情况,将每个块对应的计算图分别进行分割,生成一个或多个子图,如下图所示,具体包括以下三个步骤:
算子标记 按照拓扑顺序依次遍历计算图中每个算子,依据已注册的 Paddle 算子->硬件IR的转换表,标记能够转为硬件 IR 的算子。例如,在上图第一幅图的计算图中,包含 10 个算子 Op1~Op10 ,假设 Op1 、 Op3 和 Op10 不能够转为硬件 IR ,如第二幅图所示,这三个算子会被标记为默认颜色(黄色),代表使用 CPU Kernel 进行计算,而 Op2 、 Op4 、 Op5 、 Op6 、 Op7 、 Op8 和 Op9 则标记为红色,代表这些算子可以被转换成硬件的 IR 。
子图检测 对标记的算子作进一步分析,采用反向 DFS 算法将相邻的算子标记为同一个子图。例如上图第三幅图所示, Op2 被单独分到子图 1 ,而 Op4 、 Op5 、 Op6 、 Op7 、 Op8 和 Op9 则划到子图 2 。
子图融合 为了减少硬件与 Host 端过多的数据拷贝而带来的额外开销,如果某个子图的算子过少,则删除该子图,即它所包含的所有算子都不会放在目标硬件上执行,然后对保留下来的子图进行算子融合,具体是利用一个子图算子代替该子图包含的所有算子,但所有算子信息将以新的块( Block desc )的形式保存在程序( Program desc )中,块索引则以属性的形式保存子图算子中。
由于子图检测代码较为通用,在硬件接入的过程中无需做过多的修改,只需参照着增加对应硬件的 subgraph pass 即可,具体可参考 NNAdapterSubgraphPass 的实现。
框架如何执行子图? 当计算图被分割成若干普通算子和多个子图算子后(如上图的第四幅图所示,包含 4 个普通算子 Op1 、 Op2 、 Op3 和 Op10 , 1 个子图算子 Op1 ),通过"运行时程序的生成和执行"步骤将普通算子( Kernel )和子图算子( Kernel ,参考 NNAdapter subgraph op kernel 保存在运行时程序中,当运行时程序执行时,如果遇到子图算子,则执行如下步骤:
读取并加载子图中的原始算子 通过保存在子图算子中的 sub_block 属性,在程序描述对象( Program desc )中找到相应的块描述对象( Block desc ),然后依次读取算子描述对象( Op desc ),根据算子类型创建算子对象。
加载原始算子的 Kernel ,创建原始运行时程序 通过算子描述对象中保存的 kKernelTypeAttr 属性找到对应的 Kernel ,与上个步骤获得的算子对象,一起封装在 Instruction 对象中,所有算子的 Instruction 对象便组成了子图原始运行时程序。
原始算子转为硬件 IR 、组网生成 Graph 遍历子图中的所有原始算子(已按照拓扑顺序排序),依次将每个原始算子转为硬件 IR ,具体地,通过算子类型查询是否注册对应的转换器( Op converter ),如果已注册,则执行转换器器实现算子到硬件IR的转换,并调用硬件组网 API 生成 Graph 。转换器是子图接入方式最重要的模块,也是工作量最大的部分,为了尽可能将算子放到硬件上执行,应当为每个算子增加相应的转换器,具体实现可参考 NNAdapter unary activation op converter 。
Graph 生成 Model ,设置输入、输出张量:当子图中所有原始算子都转换完成后,调用硬件提供的接口将 Graph 生成 Model 并设置输入、输出张量。
执行 Model ,读取输出张量的数据:将原始输入张量的数据拷贝至(或将指针传递至,以防止重复拷贝实现 ZeroCopy )硬件 Model 的输入张量,然后调用硬件提供 Model 执行接口,待执行结束后将硬件输出张量的数据拷贝至原始输出张量。
了解上述三个步骤的更多细节,可以访问具体代码实现
前四个步骤一般在子图算子 Kernel 第一次运行的时候执行,只有在输入尺寸发生变更且需要重新生成 Model 时,才会回到步骤三重新执行。
为什么需要创建原始运行时程序?在硬件 IR 转换失败、 Graph 或 Model 生成失败的时候,例如不同硬件型号、不同软件版本导致的不兼容,或者运行在不支持该硬件的设备上时,就需要回退到原始运行时程序进行执行,完成推理任务。
硬件接入时需要做哪些代码改动?
# git clone https://github.com/UserName/Paddle-Lite
# cd Paddle-Lite
$ git checkout -b UserName/FeatureName
$ pip install pre-commit
$ pre-commit install
$ git status
On branch hongming/print_ssa_graph
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
(commit or discard the untracked or modified content in submodules)
modified: lite/core/optimizer/optimizer.h
$ git diff
diff --git a/lite/core/optimizer/optimizer.h b/lite/core/optimizer/optimizer.h
index 00e9e07..1b273af 100644
--- a/lite/core/optimizer/optimizer.h
+++ b/lite/core/optimizer/optimizer.h
@@ -55,7 +55,8 @@ class Optimizer {
if (passes.empty()) {
std::vector<std::string> passes_local{
- {"lite_quant_dequant_fuse_pass", //
+ {"graph_visualze",
+ "lite_quant_dequant_fuse_pass", //
"lite_conv_elementwise_fuse_pass", // conv-elemwise-bn
$ git add lite/core/optimizer/optimizer.h
$ git status
On branch hongming/print_ssa_graph
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: lite/core/optimizer/optimizer.h
$ git commit -m "Add graph_visualze pass to output ssa graph
> test=develop"
CRLF end-lines remover...................................................Passed
Check for added large files..............................................Passed
Check for merge conflicts................................................Passed
Check for broken symlinks................................................Passed
Detect Private Key.......................................................Passed
Fix End of Files.........................................................Passed
clang-format.............................................................Passed
cpplint..................................................................Passed
copyright_checker........................................................Passed
[hongming/print_ssa_graph 75ecdce] Add graph_visualze pass to output ssa graph test=develop
1 file changed, 2 insertions(+), 1 deletion(-)
$ git remote -v
origin https://github.com/UserName/Paddle-Lite.git (fetch)
origin https://github.com/UserName/Paddle-Lite.git (push)
$ git remote add upstream https://github.com/PaddlePaddle/Paddle-Lite
$ git remote
origin
upstream
$ git fetch upstream
remote: Enumerating objects: 105, done.
remote: Counting objects: 100% (105/105), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 142 (delta 99), reused 100 (delta 99), pack-reused 37
Receiving objects: 100% (142/142), 52.47 KiB | 2.00 KiB/s, done.
Resolving deltas: 100% (103/103), completed with 45 local objects.
From https://github.com/PaddlePaddle/Paddle-Lite
a1527e8..d6cdb1e develop -> upstream/develop
2136df9..17a58b6 gh-pages -> upstream/gh-pages
1091ab8..55be873 image-sr-v2 -> upstream/image-sr-v2
* [new branch] release/v2.2.0 -> upstream/release/v2.2.0
* [new tag] v2.2.0 -> v2.2.0
$ git branch
develop
* hongming/print_ssa_graph
$ git pull upstream develop
From https://github.com/PaddlePaddle/Paddle-Lite
* branch develop -> FETCH_HEAD
Removing lite/kernels/npu/bridges/transpose_op_test.cc
Removing lite/kernels/npu/bridges/batch_norm_op_test.cc
Merge made by the 'recursive' strategy.
lite/kernels/npu/bridges/batch_norm_op_test.cc | 168 ------------------------------------------------------------------------------------------------
lite/kernels/npu/bridges/transpose_op.cc | 2 +-
lite/kernels/npu/bridges/transpose_op_test.cc | 153 ---------------------------------------------------------------------------------------
lite/tests/kernels/CMakeLists.txt | 4 +--
lite/tests/kernels/batch_norm_compute_test.cc | 2 ++
lite/tests/kernels/transpose_compute_test.cc | 44 ++++++++++++-------------
mobile/test/CMakeLists.txt | 6 ++++
mobile/test/net/test_mobilenet_male2fe.cpp | 66 ++++++++++++++++++++++++++++++++++++++
8 files changed, 99 insertions(+), 346 deletions(-)
delete mode 100644 lite/kernels/npu/bridges/batch_norm_op_test.cc
delete mode 100644 lite/kernels/npu/bridges/transpose_op_test.cc
create mode 100644 mobile/test/net/test_mobilenet_male2fe.cpp
$ git branch
develop
* hongming/print_ssa_graph
$ git push origin hongming/print_ssa_graph
Counting objects: 8, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 868 bytes | 0 bytes/s, done.
Total 8 (delta 6), reused 0 (delta 0)
remote: Resolving deltas: 100% (6/6), completed with 6 local objects.
remote:
remote: Create a pull request for 'hongming/print_ssa_graph' on GitHub by visiting:
remote: https://github.com/UserName/Paddle-Lite/pull/new/hongming/print_ssa_graph
remote:
To https://github.com/UserName/Paddle-Lite.git
* [new branch] hongming/print_ssa_graph -> hongming/print_ssa_graph