ch06:构建自己的计算图理解

468 阅读3分钟

本节主要探讨如何读取pnnx的计算图, 并在其之上进行封装, 构建自己的计算图。 在参考作者原文的基础上加上部分自己的理解。
项目地址
课程地址
个人完成的作业地址
作者原文

pnnx介绍

PNNX为PyTorch提供了一种开源的模型格式, 它定义了与PyTorch相匹配的数据流图和运算操作, 我们的框架在PNNX之上封装了一层更加易用和简单的计算图格式. PyTorch训练好一个模型之后, 然后模型需要转换到PNNX格式, 然后PNNX格式我们再去读取, 形成计算图。 PNNX由operand(运算数)operator(运算符号)PNNX::Graph用来管理和操作这两者.

operand

github.com/Tencent/ncn…

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;

private:
    friend class Graph;
    Operand()
    {
    }
};

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;  // 权重
    private: 
        friend class Graph; 
        Operator() { } 
};

我们对PNNX的封装

对Operands(运算数)的封装

struct RuntimeOperand{
    std::string name; // 操作数名称
    std::vector<int32_t> shapes;  // 操作数形状
    std::vector<std::shared_ptr<Tensor<float>>> datas; // 存储操作数
    RuntimeDataType type = RuntimeDataType::kTypeUnknown; // 操作数类型, 一般是float
};

对Operator(运算符)的封装

struct RuntimeOperator{
    int32_t meet_num = 0; // 计算节点被相连接节点访问到的次数
    ~RuntimeOperator(){
        for(const auto &param:this->params){
            delete param.second;
        }
    }
    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; // 算子属性信息, 内含权重信息
    };

从PNNX计算图到KuiperInfer计算图

RuntimeGraph graph(param_path, bin_path);
graph.Init();

Init函数负责从pnnx计算图到kuiperInfer计算图转换

1. 加载pnnx计算图

load函数由pnnx项目ir.cpp提供

int load_result = this->graph_->load(param_path_, bin_path_);

2. 获取pnnx计算图的运算符operators

std::vector<pnnx::Operator*> operators = this->graph_->ops;
if (operators.empty()) {
    LOG(ERROR) << "Can not read the layers' define";
    return false;
}

3. 遍历pnnx计算图中的运算符,构建kuiperinfer计算图

this->operators_.clear();
for(const pnnx::Operator *op:operators){
    std::shared_ptr<RuntimeOperator> runtime_operator = std::make_shared<RuntimeOperator>();
    // 初始化算子的名称
    runtime_operator->name = op->name;
    runtime_operator->type = op->type;
    
    // 对应后续第四小结 根据pnnx算子的输入数初始化我们的算子的输入数。  
    const std::vector<pnnx::Operand *> &inputs = op->inputs;
    InitInputOperators(inputs, runtime_operator);
    
    InitOutputOperators(outputs, runtime_operator);
    
    InitGraphAttrs(attrs, runtime_operator);
    
    InitGraphParams(params, runtime_operator);
    
    this->operators_.push_back(runtime_operator);
}

4. 初始化RuntimeOperator的输入

InitInputOperators(inputs, runtime_operator);
RuntimeOperator有两个属性

std::map<std::string, std::shared_ptr<RuntimeOperand>> input_operands; //节点输入操作数
std::vector<std::shared_ptr<RuntimeOperand>> input_operands_seq; // 节点输入操作数, 顺序排列
void RuntimeGraph::InitInputOperators(const std::vector<pnnx::Operand *> &inputs,
                                      const std::shared_ptr<RuntimeOperator> &runtime_operator) {
    // 遍历输入pnnx的操作数类型(operands), 去初始化KuiperInfer中的操作符(RuntimeOperator)的输入.
    for(const pnnx::Operand *input: inputs){
        if(!input){
            continue;
        }
        // 得到pnnx操作数对应的生产者(类型是pnnx::operator)
        const pnnx::Operator *producer = input->producer;
        // 初始化RuntimeOperator的输入runtime_operand
        std::shared_ptr<RuntimeOperand> runtime_operand = std::make_shared<RuntimeOperand>();
        // 赋值runtime_operand的名称和形状
        runtime_operand->name = producer->name;
        runtime_operand->shapes = input->shape;
        switch(input->type){
            case 1:{
                runtime_operand->type = RuntimeDataType::kTypeFloat32;
                break;
            }
            case 0:{
                runtime_operand->type = RuntimeDataType::kTypeUnknown;
                break;
            }default: {
                LOG(FATAL) << "Unknown input operand type: " << input->type;
            }
        }
        // runtime_operand放入到KuiperInfer的运算符中
        runtime_operator->input_operands.insert({producer->name, runtime_operand});
        runtime_operator->input_operands_seq.push_back(runtime_operand);
    }
}

5. 初始化RuntimeOperator的输出

void RuntimeGraph::InitOutputOperators(const std::vector<pnnx::Operand *> &outputs,
                                   const std::shared_ptr<RuntimeOperator> &runtime_operator) {
    for (const pnnx::Operand *output : outputs) {
        if (!output) {
        continue;
        }
        const auto &consumers = output->consumers;
        for (const auto &c : consumers) {
        runtime_operator->output_names.push_back(c->name);
        }
    }
}

6. 初始化参数

几乎同四五

7. 初始化权重

同上

TEST

输出pnnx的算子类型与名称 6.1.png 目前总共通过12个test 6.10.png