KuiperInfer跟学——第三讲 计算图的定义

KuiperInfer跟学——第三讲 计算图的定义

一、PNNX

pnnx是一种类似于onnx的模型中间表示结构。KuiperInfer计算图的设计参照了pnnx。

pnnx介绍:https://zhuanlan.zhihu.com/p/427620428

一个计算图中包含了操作符、操作数。

下面是计算图这个类的定义

class Graph
{
public:
    Graph();
    ~Graph();

    int load(const std::string& parampath, const std::string& binpath);
    int save(const std::string& parampath, const std::string& binpath);

    int python(const std::string& pypath, const std::string& binpath);

    int parse(const std::string& param);

    Operator* new_operator(const std::string& type, const std::string& name);

    Operator* new_operator_before(const std::string& type, const std::string& name, const Operator* cur);

    Operator* new_operator_after(const std::string& type, const std::string& name, const Operator* cur);

#if BUILD_PNNX
    Operand* new_operand(const torch::jit::Value* v);
#endif

    Operand* new_operand(const std::string& name);

    Operand* get_operand(const std::string& name);
    const Operand* get_operand(const std::string& name) const;

    std::vector<Operator*> ops;
    std::vector<Operand*> operands;

private:
    Graph(const Graph& rhs);
    Graph& operator=(const Graph& rhs);
};

首先说一下load函数

int Graph::load(const std::string& parampath, const std::string& binpath)
{
    std::ifstream is(parampath, std::ios::in | std::ios::binary);
    if (!is.good())
    {
        fprintf(stderr, "open failed\n");
        return -1;
    }

    StoreZipReader szr;
    if (szr.open(binpath) != 0)
    {
        fprintf(stderr, "open failed\n");
        return -1;
    }

    int magic = 0;
    {
        std::string line;
        std::getline(is, line);
        std::istringstream iss(line);

        iss >> magic;
    }

    int operator_count = 0;
    int operand_count = 0;
    {
        std::string line;
        std::getline(is, line);
        std::istringstream iss(line);

        iss >> operator_count >> operand_count;
    }

    for (int i = 0; i < operator_count; i++)
    {
        std::string line;
        std::getline(is, line);
        std::istringstream iss(line);

        std::string type;
        std::string name;
        int input_count = 0;
        int output_count = 0;

        iss >> type >> name >> input_count >> output_count;

        Operator* op = new_operator(type, name);

        for (int j = 0; j < input_count; j++)
        {
            std::string operand_name;
            iss >> operand_name;

            Operand* r = get_operand(operand_name);
            r->consumers.push_back(op);
            op->inputs.push_back(r);
        }

        for (int j = 0; j < output_count; j++)
        {
            std::string operand_name;
            iss >> operand_name;

            Operand* r = new_operand(operand_name);
            r->producer = op;// 操作符的生产者只有一个,而消费者可以有多个
            op->outputs.push_back(r);
        }

        // key=value
        while (!iss.eof())
        {
            std::string param;
            iss >> param;

            std::string key;
            std::string value;
            std::istringstream pss(param);
            std::getline(pss, key, '=');
            std::getline(pss, value);

            if (key[0] == '@')
            {
                // attribute
                load_attribute(op, key.substr(1), value, szr);
            }
            else if (key[0] == '$')
            {
                // operand input key
                load_input_key(op, key.substr(1), value);
            }
            else if (key[0] == '#')
            {
                // operand shape
                load_shape(op, key.substr(1), value);
            }
            else
            {
                // parameter
                load_parameter(op, key, value);
            }
        }
    }

    return 0;
}

load函数的整体思路是:首先从.param文件中读取操作符和操作数,这个文件中包含了majic数,用于文件或者版本标识;包含了操作符和操作数的总个数,其中输入和输出算作操作符;然后就是每个操作数的类型、名称、输入和输出的相关信息,这些信息是为了构建操作符而服务,param文件中只包含了搭建模型的相关信息,不包含具体的权重值,具体权重值在bin文件中。load函数根据这两个文件的信息,构建一个完整的模型计算图;


Operator类,运算符类,也是就算子类;

class Operator
{
public:
    std::vector<Operand*> inputs;
    std::vector<Operand*> outputs;

    // keep std::string typed member the last for cross cxxabi compatibility
    std::string type;
    std::string name;

    std::vector<std::string> inputnames;
    std::map<std::string, Parameter> params;
    std::map<std::string, Attribute> attrs;
};

inputs:算子的输入,是一个Operand类型的数组,因为可能有多个输入

outputs:算子的输出,也是一个Operand类型的数组,因为算子也有可能有多个输出;

typename:类型是操作符的类型,比如卷积conv、全连接linear;名字是算子的唯一标识符,比如conv1、linear1。二者可以类比成类和对象的关系;

inputnames:这个参数目前存疑,代码中似乎没有用到

params:params是指操作符的相关信息,而不是具体的权重数值,用map类型来存储。比如linear算子中会指出in_features:32;这里key=in_features,对应的value=32;

attrs:是指算子的权重值,用一个map类型来存储,比如一个linear算子的in_features=32,out_features=128,那么它的attrs中会包含key=weights,value等于一个32*128的矩阵;


Parameter类,参数类,用于操作符和操作数中,来说明其中参数的值;比如卷积核的尺寸等;一般和map结合使用,std::map<std::string, Parameter> params;

class Parameter
{
public:
    Parameter()
        : type(0)
    {
    }
    Parameter(bool _b)
        : type(1), b(_b)
    {
    }
    Parameter(int _i)
        : type(2), i(_i)
    {
    }
    Parameter(long _l)
        : type(2), i(_l)
    {
    }
    Parameter(long long _l)
        : type(2), i(_l)
    {
    }
    Parameter(float _f)
        : type(3), f(_f)
    {
    }
    Parameter(double _d)
        : type(3), f(_d)
    {
    }
    Parameter(const char* _s)
        : type(4), s(_s)
    {
    }
    Parameter(const std::string& _s)
        : type(4), s(_s)
    {
    }
    Parameter(const std::initializer_list<int>& _ai)
        : type(5), ai(_ai)
    {
    }
    Parameter(const std::initializer_list<int64_t>& _ai)
        : type(5)
    {
        for (const auto& x : _ai)
            ai.push_back((int)x);
    }
    Parameter(const std::vector<int>& _ai)
        : type(5), ai(_ai)
    {
    }
    Parameter(const std::initializer_list<float>& _af)
        : type(6), af(_af)
    {
    }
    Parameter(const std::initializer_list<double>& _af)
        : type(6)
    {
        for (const auto& x : _af)
            af.push_back((float)x);
    }
    Parameter(const std::vector<float>& _af)
        : type(6), af(_af)
    {
    }
    Parameter(const std::initializer_list<const char*>& _as)
        : type(7)
    {
        for (const auto& x : _as)
            as.push_back(std::string(x));
    }
    Parameter(const std::initializer_list<std::string>& _as)
        : type(7), as(_as)
    {
    }
    Parameter(const std::vector<std::string>& _as)
        : type(7), as(_as)
    {
    }

#if BUILD_PNNX
    Parameter(const torch::jit::Node* value_node);
    Parameter(const torch::jit::Value* value);
#endif // BUILD_PNNX

    static Parameter parse_from_string(const std::string& value);

    // 0=null 1=b 2=i 3=f 4=s 5=ai 6=af 7=as 8=others
    int type;

    // value
    bool b;
    int i;
    float f;
    std::vector<int> ai;
    std::vector<float> af;

    // keep std::string typed member the last for cross cxxabi compatibility
    std::string s;
    std::vector<std::string> as;
};

这里面并非所有的数据成员都有意义,只有相应类型的数据成员才有意义。


Attribute类,权重类,用于存储操作符中的权重值,比如卷积权重,偏置权重。使用也是通常和map结合,std::map<std::string, Attribute> attrs;

class Attribute
{
public:
    Attribute()
        : type(0)
    {
    }

#if BUILD_PNNX
    Attribute(const at::Tensor& t);
#endif // BUILD_PNNX

    Attribute(const std::initializer_list<int>& shape, const std::vector<float>& t);

    // 0=null 1=f32 2=f64 3=f16 4=i32 5=i64 6=i16 7=i8 8=u8 9=bool
    int type;
    std::vector<int> shape;

    std::vector<char> data;
};

值得注意的是,Attribute类的data成员采用char类型的列表,也就是采用字节序列来存储,这样保证了所有类型的数据都采用统一的格式进行存储,后续使用权重的时候,根据type和shape来组织数据。


Operand类,操作数类

class Operand
{
public:
    void remove_consumer(const Operator* c);

    Operator* producer;
    std::vector<Operator*> consumers;

    // 0=null 1=f32 2=f64 3=f16 4=i32 5=i64 6=i16 7=i8 8=u8 9=bool 10=cp64 11=cp128 12=cp32
    int type;
    std::vector<int> shape;

    // keep std::string typed member the last for cross cxxabi compatibility
    std::string name;

    std::map<std::string, Parameter> params;

};

题外话:keep std::string typed member the last for cross cxxabi compatibility

把string类型放在最后,可以提高兼容性;

producer:该操作数的生产者,也就是该操作数是producer的输出,一个操作数明显只能有一个生产者;

consumer: 操作数的消费者,也就是该操作数是consumer的输入,一个操作数可以有多个消费者;

type:该操作数的数据类型;

shape:该操作数的数据形状;

name:该操作数的名字,用作唯一标识符;

params:用于标记该操作数的一个额外信息,由于目前还没遇到具体的过程,所以这里先不举例说明;


二、Kuiperinfer对PNNX计算图的封装

核心是要保持KuiperInfer::Runtime Graph和pnnx::Graph的完全一致,包括结构和参数权重。

1、UML结构图

在这里插入图片描述

注:虚线加空心箭头表示实现,是一种类与接口的关系,箭头指向接口;但实际代码中,RuntimeParameter和RuntimeParameterInt、RuntimeParameterString、RuntimeParameterFloat是继承关系。

实线加实心箭头表示关联关系,即has-a关系;

2、关键类讲解说明

2.1 RuntimeOperator
struct RuntimeOperator {
  virtual ~RuntimeOperator();

  bool has_forward = false;
  std::string name;      /// 计算节点的名称
  std::string type;      /// 计算节点的类型
  std::shared_ptr<Layer> layer;  /// 节点对应的计算Layer

  std::vector<std::string> output_names;  /// 节点的输出节点名称
  std::shared_ptr<RuntimeOperand> output_operands;  /// 节点的输出操作数

  std::map<std::string, std::shared_ptr<RuntimeOperand>>
      input_operands;  /// 节点的输入操作数
  std::vector<std::shared_ptr<RuntimeOperand>>
      input_operands_seq;  /// 节点的输入操作数,顺序排列
  std::map<std::string, std::shared_ptr<RuntimeOperator>>
      output_operators;  /// 输出节点的名字和节点对应

  std::map<std::string, RuntimeParameter*> params;  /// 算子的参数信息
  std::map<std::string, std::shared_ptr<RuntimeAttribute>>
      attribute;  /// 算子的属性信息,内含权重信息
};

RuntimeOperator中的一些具体设计,比如设计输出节点,里面不是能完全理解,估计得继续往下面学才能懂为何这样设计。

name: 该计算节点的名称,也是其在计算图中的唯一标识;比如我们conv_1就是一个计算节点的名称;

type: 该运算节点的类型,比如Convolution、Linear;

layer: 负责实施计算的组件,目前课程还没将layer的实现;

其他的一些成员的含义,参考注释即可;


2.2 PNNX中的operator是如何到KuiperInfer::RuntimeOperator中的

pnnx可以看作类似于onnx的一种中间模型表示,训练好的模型保存为pnnx格式,然后推理框架从pnnx模型文件中读取信息,进行推理,所以就涉及到pnnx中的operator是如何传递到KuiperInfer::RuntimeOperator中的,也就是RuntimeOperator是如何读取pnnx中operator的信息并化为己用的;

因为推理都是直接读取整个pnnx文件,然后转为推理计算图的,所以这里有必要看一下RuntimeGraph的内容

/// 计算图结构,由多个计算节点和节点之间的数据流图组成
class RuntimeGraph {
public:
  /**
   * 初始化计算图
   * @param param_path 计算图的结构文件
   * @param bin_path 计算图中的权重文件
   */
  RuntimeGraph(std::string param_path, std::string bin_path);

  /**
   * 设置权重文件
   * @param bin_path 权重文件路径
   */
  void set_bin_path(const std::string &bin_path);

  /**
   * 设置结构文件
   * @param param_path  结构文件路径
   */
  void set_param_path(const std::string &param_path);

  /**
   * 返回结构文件
   * @return 返回结构文件
   */
  const std::string &param_path() const;

  /**
   * 返回权重文件
   * @return 返回权重文件
   */
  const std::string &bin_path() const;

  /**
   * 计算图的初始化
   * @return 是否初始化成功
   */
  bool Init();

  const std::vector<std::shared_ptr<RuntimeOperator>> &operators() const;

private:
  /**
   * 初始化kuiper infer计算图节点中的输入操作数
   * @param inputs pnnx中的输入操作数
   * @param runtime_operator 计算图节点
   */
  static void InitGraphOperatorsInput(
      const std::vector<pnnx::Operand *> &inputs,
      const std::shared_ptr<RuntimeOperator> &runtime_operator);

  /**
   * 初始化kuiper infer计算图节点中的输出操作数
   * @param outputs pnnx中的输出操作数
   * @param runtime_operator 计算图节点
   */
  static void InitGraphOperatorsOutput(
      const std::vector<pnnx::Operand *> &outputs,
      const std::shared_ptr<RuntimeOperator> &runtime_operator);

  /**
   * 初始化kuiper infer计算图中的节点属性
   * @param attrs pnnx中的节点属性
   * @param runtime_operator 计算图节点
   */
  static void
  InitGraphAttrs(const std::map<std::string, pnnx::Attribute> &attrs,
                 const std::shared_ptr<RuntimeOperator> &runtime_operator);

  /**
   * 初始化kuiper infer计算图中的节点参数
   * @param params pnnx中的参数属性
   * @param runtime_operator 计算图节点
   */
  static void
  InitGraphParams(const std::map<std::string, pnnx::Parameter> &params,
                  const std::shared_ptr<RuntimeOperator> &runtime_operator);

private:
  std::string input_name_;  /// 计算图输入节点的名称
  std::string output_name_; /// 计算图输出节点的名称
  std::string param_path_;  /// 计算图的结构文件
  std::string bin_path_;    /// 计算图的权重文件

  std::vector<std::shared_ptr<RuntimeOperator>> operators_;
  std::map<std::string, std::shared_ptr<RuntimeOperator>> operators_maps_;

  std::unique_ptr<pnnx::Graph> graph_; /// pnnx的graph
};

input_name_:计算图的输入节点名称;

output_name_:计算图的输入出节点名称;

param_path_:计算图的结构文件路径,这个文件告诉我们计算图是如何搭建起来的,数据是如何在计算图中进行流转的。

bin_path_:计算图的权重文件,这个文件告诉我们计算图中各个节点的权重值;

这里面的很多方法其实都是在pnnx::Graph的基础上进一步封装,就是能调用pnnx::Graph的函数就调用,不要自己再去造轮子;


RuntimeGraph::InitGraphAttrs

void RuntimeGraph::InitGraphAttrs(
            const std::map<std::string, pnnx::Attribute> &attrs,
            const std::shared_ptr<RuntimeOperator> &runtime_operator) {
        for (const auto &[name, attr]: attrs) {
            switch (attr.type) {
                case 1: {
                    std::shared_ptr<RuntimeAttribute> runtime_attribute =
                            std::make_shared<RuntimeAttribute>();
                    runtime_attribute->type = RuntimeDataType::kTypeFloat32;
                    runtime_attribute->weight_data = attr.data;
                    runtime_attribute->shape = attr.shape;
                    runtime_operator->attribute.insert({name, runtime_attribute});
                    break;
                }
                default: {
                    LOG(FATAL) << "Unknown attribute type: " << attr.type;
                }
            }
        }
    }

把每个计算节点的权重信息给赋予给运行时计算节点(RuntimeOperator)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值