本节主要探讨如何读取pnnx的计算图, 并在其之上进行封装, 构建自己的计算图。 在参考作者原文的基础上加上部分自己的理解。
项目地址
课程地址
个人完成的作业地址
作者原文
pnnx介绍
PNNX为PyTorch提供了一种开源的模型格式, 它定义了与PyTorch相匹配的数据流图和运算操作, 我们的框架在PNNX之上封装了一层更加易用和简单的计算图格式. PyTorch训练好一个模型之后, 然后模型需要转换到PNNX格式, 然后PNNX格式我们再去读取, 形成计算图。
PNNX由operand(运算数)和operator(运算符号), PNNX::Graph用来管理和操作这两者.
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;
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 ¶m: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的算子类型与名称
目前总共通过12个test