# Parser的编写指南

  Parser是一种网络框架转换工具，将其他框架如Caffe、TensorFlow的网络结构转换为Anakin网络结构图，然后对转换后的Anakin图进行预测处理

  本文主要介绍Parser功能的框架结构和根据已有的网络框架改写Parser，以解析得到Anakin框架图，进行Anakin预测

  下文称Anakin为AK，运算操作为OP,本文参考TensorFlow的Parser编写,参考代码目录为tools/external_converter_v2/parser/tensorflow

## Parser的功能和执行流程

  Parser功能是将其他深度学习框架(如Caffe，TensorFlow，ONNX)的模型转换为AK的模型

  对AK的作用是屏蔽不同框架间的差异，这种差异包括模型存储、OP的定义、图差异

  因此Parser的执行流程是：

  - 将源框架的模型载入Parser
  - 将原框架的图解析为AK中的OP节点和OP节点的连接关系
  - 进行OP定义的转换和图优化
  - 将符合AK标准的图写入protobuf

## Parser的目录结构

  Parser工具在tools/external_converter_v2/parser目录下

  Parser的目录主要包含3部分:

  - Parser的运行配置文件包括 config.py, config.yaml, converter.py, 用户只用执行converter.py，Parser就会按照config.yaml中的声明去解析模型
  - Parser的公共定义，包括operations,pbs,proto三个目录。Parser的公共工具函数 graph*.py logger.py utils.py
  - 各个框架对应的Parser，其目录的命名方式为框架名,如Caffe, TensorFlow

## Parser的编写流程

### 1、声明你的Parser

  - 在config.yaml中填写你的Parser运行的必要信息，包括ProtoPath和SavePath等。OPTIONS/Framework改为你的Parser的类型，TARGET下填写对应的参数列表
  - 添加你的Parser目录，如TensorFlow，导出你的Parser符号。注意，Parser的框架默认调用你的Parser类中的__call__方法来执行解析，这个方法需要返回填写完毕的GraphProtoIO对象
  - 在config.py中Configuration下__init__函数中增加对你的Parser的调用，将yaml中读取的配置信息传给你的Parser，此处调用你的Parser中的__init__方法

### 2、添加你的Parser主体

  可以参考parser_tf.py

  - 你需要在Parser主体构造时获取模型路径，input，ouput名字等解析必须的信息
  - 在__call__中返回填写好的GraphProtoIO对象，该对象为填写protobuf的辅助工具
  - 建议Parser的解析过程分成三部分，先将原框架的模型载入并转换为一种便于修改的中间的图形式；对中间图修改使得图满足AK的要求；将满足要求的中间图利用NodeProtoIO和GraphProtoIO这两个辅助类填入protobuf，具体细节可以参考parser_tf

### 3、读取原始模型，并将模型转换为中间类型

  可以参考parse_tf_2_med.py

  - 这一步与原始框架结合紧密，你可能需要import原始框架的工具函数来完成模型的裁剪、固定、加载等操作
  - 大部分的框架都是使用tensor来连接OP的，但AK中是OP直接相连，这点需要注意
  - AK的shape默认是4维的，有的参数的shape不足4维，需要Parser补全

### 4、对中间类型的图进行优化

  可以参考med_graph.py

  - 由于AK不支持普通OP多输出的情况，需要在多输出的OP后面补上Splite类型的OP节点
  - 对于Convlution后接Batchnorm这种可以合并又不会导致OP定义改变的情况，需要Parser在这一步做掉
  - AK规定所有的输入类型OP的名字必须是input_x这种命名方式，其中x为从0开始的数字

### 5、将中间类型的图以GraphProtoIO的方式保存

  可以参考parse_med_2_ak.py 和 parser_tf.py

  - 你首先需要构造Node节点，Node节点的名字是OP的名字(如conv2d_1_a_0)，Node节点中OP成员变量的名字是Node节点的类型(如Convlution)
  - Node节点需要按照输入的顺序用Node的add_in方法填写输入Node的名字，add_out方法按顺序填写输出Node的名字
  - 通过调用GraphProtoIO的add_node方法将构造好的Node的__call__方法的返回值作为参数，将Node节点加入AK的graph中
  - 调用GraphProtoIO的add_in_edge和add_out_edge完成AK图中OP间关系的构建。如果Node中的in和out填写正确，你也可以通过调用GraphProtoIO的format_edge_from_nodes方法完成这个工作
  - AK的模型需要Parser给出输出Node的名字，使用GraphProtoIO的add_out方法填写输出Node的名字

### 6、检查模型解析的正确性

  - 默认的config.yaml配置会在解析结束后启动一个web服务器展示解析后的AK模型图，你需要对比原框架的模型图进行验证。这里最容易出现的错误是边关系的错误，表现为图非常乱，你需要逐条边地检查错误；第二个容易出错的地方是参数漏填，需要你检查OP中的属性
  - 将解析后的模型放入AK中执行，使用相同的输入，原框架与AK有相同的输出。若果输出不一致可以开启AK的DEBUG模式，在net.cpp中将没层的输出打印；如果AK在解析阶段陷入死循环，大概率是边的关系出错

## 如何添加新OP

  - 需要在AK代码中加入该OP的实现，包括对应设备Saber的OP，Saber单测和Framework中的OP
  - 根据Framework的OP在ops.py中添加Parser公共的OP定义
  - 从原框架的模型中解析出该OP的节点，并在AK的graph中填入该OP节点

## AK模型与其他框架模型的不同之处

  + AK模型与caffe的模型相似，因此与其他模型有很多不同的地方，需要Parser在解析过程中处理掉
  + 最大的不同是与PaddlePaddle或TensorFlow的模型中OP粒度很细，而AK的模型中OP的粒度很粗（目的是为了节省访存开销）。这会导致解析这些框架的模型时存在大量的合并操作
  + 其次是OP的行为不同,如TensorFlow中Pooling默认都是exclusive的，而AK中是inclusive的。TensorFlow的Padding，如果是奇数pad，则在右方和下方多pad，而AK是在左方和上方多Pad
  + AK默认的布局是NCHW，如果其他框架的OP是其他形式的，需要在Parser中做weights的布局转换，并处理reshape的问题
  + AK中有的weights是需要预先做布局转换的(如GRU，LSTM)，AK中也支持同一OP的不同算法，如(GRU，Pooling)

