ch10:构建计算图的图关系

173 阅读3分钟

作者这节原文是“再探Tensor类并构建计算图的图关系”,但我为了主题明确本文只关注如何构建计算图的图关系。
ch06我们根据PNNX计算图得到了我们的计算图,包括操作数和操作符,但我们没有将其关联起来,构建成完整的计算图。本文注如何构建计算图的图关系。
项目地址
课程地址
个人完成的作业地址
作者原文

回顾

ch06中解析思路为

1. 加载pnnx计算图
2. 获取pnnx计算图的运算符operators
3. 遍历pnnx计算图中的运算符,构建kuiperinfer计算图
   在遍历每一个运算符时候,初始化其输入输出参数权重
   4. 初始化RuntimeOperator的输入节点
   5. 初始化RuntimeOperator的输出节点
   6. 初始化参数
   7. 初始化权重 

可以看到,确实没有得到完整的图关系。

构建图关系

两层循环  
第一次遍历所有节点 A  
    第二次遍历除自身外所有节点 B
         判断B是不是A的子节点
// 构建图关系 (第十章新增)
for (const auto &current_op : this->operators_) {
    const std::vector<std::string> &output_names = current_op->output_names;
    for (const auto &next_op : this->operators_) {
        if (next_op == current_op) {
            continue;
        }
        if (std::find(output_names.begin(), output_names.end(), next_op->name) !=
            output_names.end()) {
            current_op->output_operators.insert({next_op->name, next_op});
        }
    }
}

解析思路变为

1. 加载pnnx计算图
2. 获取pnnx计算图的运算符operators
3. 遍历pnnx计算图中的运算符,构建kuiperinfer计算图
   在遍历每一个运算符时候,初始化其输入输出参数权重
   4. 初始化RuntimeOperator的输入节点
   5. 初始化RuntimeOperator的输出节点
   6. 初始化参数
   7. 初始化权重 
8. 两轮循环构建图关系

初始化计算图输入与输出节点

一个图一定有一个输入和输出

std::map<std::string, std::shared_ptr<RuntimeOperator>> input_operators_maps_;  // 输入节点
std::map<std::string, std::shared_ptr<RuntimeOperator>> output_operators_maps_;  // 输出节点

build函数

 this->input_operators_maps_.clear();
this->output_operators_maps_.clear();

for (const auto &kOperator : this->operators_) {
    if (kOperator->type == "pnnx.Input") {
        this->input_operators_maps_.insert({kOperator->name, kOperator});
    } else if (kOperator->type == "pnnx.Output") {
        this->output_operators_maps_.insert({kOperator->name, kOperator});
    } else {
        // 以后的课中加layer的
        // std::shared_ptr<Layer> layer = RuntimeGraph::CreateLayer(kOperator);
        // CHECK(layer != nullptr) << "Layer create failed!";
        // if (layer) {
        //     kOperator->layer = layer;
        // }
    }
}

初始化所有节点的输入与输出操作数

根据上述,在遍历每一个运算符时候,已经初始化其输入输出参数权重。
这里初始化输入与输出操作数是指具体为操作符的操作数开辟内存空间,分配指针。
同一个操作数,是父节点的输出结果,也是多个子节点的输入,此时开辟几块空间?
依然在build函数。

RuntimeGraphShape::InitOperatorInputTensor(operators_);
RuntimeGraphShape::InitOperatorOutputTensor(graph_->ops, operators_);

InitOperatorInputTensor函数

对于每一个操作符
先得到他的输入操作数字典
此时可以得到每一个操作数的shapes与datas
datas肯定是空(如果未被初始化) shapes可能是各种各样,arma::fcube底层只支持3维度,所以需要我们初始化时处理。

// 先得到他的输入操作数字典  遍历每一个输入操作数
const std::map<std::string, std::shared_ptr<RuntimeOperand>> & input_operands_map = op->input_operands;
for (const auto &input_operand_iter : input_operands_map) {
    const auto &input_operand = input_operand_iter.second;
    
// 得到input_operands中记录的数据应有大小input_operand_shape和存储数据的变量input_datas
const auto &input_operand_shape = input_operand->shapes;
auto &input_datas = input_operand->datas;

// 只支持 123维度+bacth
CHECK(input_operand_shape.size() == 2 ||
input_operand_shape.size() == 4 ||
input_operand_shape.size() == 3)
    << "Unsupported tensor shape sizes: " << input_operand_shape.size();

如果未初始化
 // datas为空, 分配内存
input_datas.resize(batch);
for (int32_t i = 0; i < batch; ++i) {
    if (input_operand_shape.size() == 4) {
    input_datas.at(i) = std::make_shared<Tensor<float>>(
        input_operand_shape.at(1), input_operand_shape.at(2),
        input_operand_shape.at(3));
    } else if (input_operand_shape.size() == 2) {
    input_datas.at(i) = std::make_shared<Tensor<float>>(
        1, input_operand_shape.at(1), 1);
    } else {
    // current shape is 3
    input_datas.at(i) = std::make_shared<Tensor<float>>(
        1, input_operand_shape.at(1), input_operand_shape.at(2));
    }
}

// 已经初始化,只检查
for (int32_t i = 0; i < batch; ++i) {
    const std::vector<uint32_t> &input_data_shape =
        input_datas.at(i)->shapes();
    CHECK(input_data_shape.size() == 3)
            << "THe origin shape size of operator input data do not equals "
            "to three";
    if (input_operand_shape.size() == 4) {
        CHECK(input_data_shape.at(0) == input_operand_shape.at(1) &&
            input_data_shape.at(1) == input_operand_shape.at(2) &&
            input_data_shape.at(2) == input_operand_shape.at(3));
    } else if (input_operand_shape.size() == 2) {
        CHECK(input_data_shape.at(1) == input_operand_shape.at(1) &&
            input_data_shape.at(0) == 1 && input_data_shape.at(2) == 1);
    } else {
        // current shape size = 3
        CHECK(input_data_shape.at(1) == input_operand_shape.at(1) &&
            input_data_shape.at(0) == 1 &&
            input_data_shape.at(2) == input_operand_shape.at(2));
    }
}

InitOperatorOutputTensor函数

InitOperatorInputTensor函数基本相同,区别在于,在ch06的初始化中,初始化输入时候存了名字,shape;但初始化输出只存了名字。所以此时开辟内存,也要重新遍历pnnx::graph的操作符,得到各个操作符输出的shape。

TEST

解析输入 10.1.png 目前总共通过23个test 10.2.png