自制深度学习推理框架之计算图设计

文章目录

    • 一、计算图
      • 1.1 计算图定义
      • 1.2 计算图的生成
        • 1.2.1 **静态计算图(Static Computational Graph)**
        • 1.2.2 **动态计算图(Dynamic Computational Graph)**
      • 1.3 计算图功能
        • 1.3.1 训练阶段
        • 1.3.2 推理部署阶段
      • 1.4 计算图的调度(执行)
    • 二、PNNX计算图
      • 2.1 PNNX介绍
      • 2.2 PNNX计算图结构
      • 2.3 Graph图结构
      • 2.4 Operator运算符
      • 2.5 Operand操作数
      • 2.6 Attribute与Parameter
    • 三、RuntimeGraph
      • 3.1 RuntimeGraph整体介绍
      • 3.2 RuntimeOperator
      • 3.3 RuntimeOperand
      • 3.4 RuntimeAttribute
      • 3.5 RuntimeParam

CLion2023环境搭建配置:

  • CLion配置工程使用外部Linux编译器编译:https://blog.csdn.net/huamu_xingkong/article/details/136944830

  • 在使用Clion通过SSH远程连接Linux,在本地工程上开发远程编译出现有关C++的相关头文件找不到,如下图所示,但是可以编译成功:解决办法

一、计算图

1.1 计算图定义

计算图(Computational Graph)是一种用于表示数学运算和数据流的图结构,在深度学习中,它用于描述神经网络中的操作及其依赖关系。计算图由节点和边组成,其中:

  • 节点:表示操作(如加法、乘法、激活函数等)或变量(如输入、权重、偏置等)。

  • :表示数据的流动,通常是张量(Tensor)在节点间传递。

    ../_images/simpledag.png

如上图所示,将下面的公式转为计算图表示。
Z = R e L U ( X × Y ) Z= ReLU(X \times Y) Z=ReLU(X×Y)

1.2 计算图的生成

在深度学习框架中可以生成静态图动态图两种计算图静态生成可以根据前端语言描述的神经网络拓扑结构以及参数变量等信息构建一份固定的计算图。因此静态图在执行期间可以不依赖前端语言描述,常用于神经网络模型的部署,比如移动端人脸识别场景中的应用等。动态图则需要在每一次执行神经网络模型依据前端语言描述动态生成一份临时的计算图,这意味着计算图的动态生成过程灵活可变,该特性有助于在神经网络结构调整阶段提高效率。

主流机器学习框架TensorFlow、MindSpore均支持动态图和静态图模式;PyTorch则可以通过工具将构建的动态图神经网络模型转化为静态结构,以获得高效的计算执行效率。了解两种计算图生成方式的优缺点及构建执行特点,可以针对待解决的任务需求,选择合适的生成方式调用执行神经网络模型。

1.2.1 静态计算图(Static Computational Graph)

也称为定义-运行(define-and-run)模式,静态计算图在程序开始时一次性构建,然后在执行阶段被多次使用图结构固定,便于优化和加速,适合批处理任务

  • 优点
    • 高效:由于图在构建时就确定,可以进行更深入的图优化,如内存优化、常量折叠等。
    • 易于部署:可以将静态计算图导出为独立文件,用于生产环境中的高效推理
  • 缺点
    • 不灵活:不适合处理动态变化的网络结构,特别是在处理可变长度的输入数据时。
1.2.2 动态计算图(Dynamic Computational Graph)

动态计算图在每次前向传播时动态构建,因此图的结构可以根据输入数据变化。其灵活性高,适合需要动态调整结构的任务,如循环神经网络(RNN)处理变长序列。

  • 优点
    • 灵活:可以处理动态结构和复杂控制流,适合实验和调试。
    • 直观:图的构建与运行是同步的,易于理解和调试。
  • 缺点
    • 性能可能较低:由于图是动态生成的,难以进行高级优化。
    • 部署复杂:动态生成的图不易导出为固定的模型格式,可能需要额外的工作来部署。

1.3 计算图功能

计算图在训练阶段和推理部署阶段的功能与实现存在显著差异。这些差异主要源于两个阶段对计算图的不同需求:训练阶段侧重于学习和优化模型参数,而推理部署阶段则侧重于高效地应用这些参数进行预测

1.3.1 训练阶段
../_images/graph.png

计算图在模型训练阶段主要有以下功能:

  • 前向传播:计算输入数据通过网络的前向传播,生成预测结果。在训练过程中,前向传播不仅生成输出,还保存中间结果(如激活值),为反向传播计算梯度提供基础。

  • 反向传播与梯度计算:计算损失函数相对于每个参数的梯度,以指导模型参数的更新。计算图记录了前向传播过程中每个操作的梯度计算规则,通过链式法则自动计算各个参数的梯度。

  • 参数更新:利用反向传播得到的梯度,通过优化算法(如SGD、Adam)更新模型参数。计算图通常不直接涉及参数更新,但优化器在图之外使用计算得到的梯度来更新参数。

  • 计算图的动态性:支持动态计算图的生成与执行,允许模型结构在训练过程中根据输入数据进行调整。如在处理变长序列或需要动态调整网络结构的任务中,动态计算图能够灵活应对不同的输入数据。

  • 正则化操作:添加正则化操作(如Dropout、L2正则化),防止模型过拟合。这些操作主要用于训练阶段,在推理时通常会被移除或替换。

  • 图优化:在训练过程中,计算图框架可能会进行优化以加速训练过程,如操作融合、内存优化等。虽然优化重点不同,但一些优化(如操作融合)在训练和推理中都会应用

  • 数据增强与预处理:在训练过程中,计算图框架通常支持数据增强和预处理操作(如图像翻转、归一化等),以提高模型的泛化能力。这些操作通常只在训练时进行,不会在推理部署中使用。

1.3.2 推理部署阶段
  • 前向传播:在给定输入的情况下,进行高效的前向传播以生成最终的预测结果。推理阶段只需要进行前向传播,不涉及反向传播和梯度计算,因此执行更加高效

  • 图的冻结与优化推理时使用冻结的计算图,去除训练相关的操作,优化执行路径以提高推理效率。冻结的计算图通常通过各种优化手段,如常量折叠操作融合移除不必要的操作(如Dropout),确保推理的高效性。

  • 硬件适配:根据推理平台的硬件特性(如CPU、GPU、TPU),进行图的调整和优化,以充分利用硬件加速能力。推理阶段的计算图更关注硬件加速的实现,通过图分割与调度、张量分配等技术,最大化硬件资源的利用。

  • 模型量化与压缩:将模型中的浮点数权重和激活量化为低精度整数减少计算量和存储需求,提升推理速度。推理部署阶段通常会进行模型量化和剪枝,以减少模型大小,降低计算成本,适应资源受限的环境。

  • 模型导出与跨平台部署:将训练好的模型导出为特定格式(PNNX或ONNX),以便在不同平台上进行部署。推理部署阶段需要确保模型在不同硬件和操作系统上的兼容性和性能。

训练阶段部署阶段
动态性 vs. 静态性可能需要处理动态计算图,允许网络结构根据输入数据实时变化通常使用静态计算图,以优化后的固定结构进行高效执行
计算复杂度需要进行前向传播、反向传播和梯度计算,计算量大,内存占用高只进行前向传播,无需计算梯度和更新参数,计算量相对较小,内存占用也较低。
优化目标提高模型的收敛速度和准确性,通过梯度计算和参数更新来改进模型性能最大化推理速度和资源利用率,确保模型在各种环境下的高效运行
操作内容涉及反向传播、梯度更新、正则化等训练特有的操作这些训练特有的操作通常被移除,图被简化为只包含必要的前向传播操作
内存与硬件资源使用内存使用量较大,尤其是在处理大规模模型或分布式训练时,框架需要优化内存分配和使用。内存使用相对较低,更多关注硬件加速和延迟优化,以满足实时或大规模并发推理需求

训练阶段关注模型的学习能力和优化过程,而推理阶段则重点在于如何将已经学习到的知识快速、准确地应用到实际数据中。

1.4 计算图的调度(执行)

模型训练就是计算图调度图中算子的执行过程。训练任务是由设定好的训练迭代次数来循环执行计算图,此时需要优化迭代训练计算图过程中数据流载入和训练(推理)执行等多个任务之间的调度策略。单次迭代需要考虑计算图内部的调度执行问题,根据计算图结构、计算依赖关系、计算控制分析算子的执行调度。优化计算图的调度和执行性能,目的是尽可能充分利用计算资源,提高计算效率,缩短模型训练和推理时间。

算子的执行调度包含两个步骤:

  • 根据拓扑排序算法,将计算图进行拓扑排序得到线性的算子调度序列

  • 将序列中的算子分配到指令流进行运算,尽可能将序列中的算子并行执行,提高计算资源的利用率。

计算图是一种由依赖边和算子构成的有向无环图,深度学习框架需要将包含这种依赖关系的算子准确地发送到计算资源,比如CPU、GPU、NPU上执行。针对有向无环图,通常使用拓扑排序来得到一串线性的序列。如下图所示一张有向无环图。

图中包含了a、b、c、d、e五个节点和a->d、b->c、c->d、d->e四条边(a->d表示d依赖于a,称为依赖边)。将图的依赖边表达成节点的入度(图论中通常指有向图中某点作为图中边的终点的次数之和),可以得到各个节点的入度信息(a:0、 b:0、 c:1、 d:2、 e:1)。拓扑排序就是不断循环将入度为0的节点取出放入队列中,直至有向无环图中的全部节点都加入到队列中,循环结束。例如,第一步将入度为0的a、b节点放入到队列中,此时有向无环图中c、d的入度需要减1,得到新的入度信息(c:0、d:1、e:1)。以此类推,将所有的节点都放入到队列中并结束排序。

生成调度序列之后,需要将序列中的算子与数据分发到指定的GPU/NPU上执行运算。根据算子依赖关系和计算设备数量,可以将无相互依赖关系的算子分发到不同的计算设备,同时执行运算,这一过程称之为并行计算,与之相对应的按照序贯顺序在同一设备执行运算被称为串行计算。这里就不过多讲解。

小结:计算图的基本数据结构是张量,基本运算单元是算子。计算图是一个有向无环图,图中算子间可以存在直接依赖和间接依赖关系,或者相互关系独立,但不可以出现循环依赖关系。计算图的生成可以分为静态生成和动态生成两种方式。静态图计算效率高,内存使用效率高,但调试性能较差,可以直接用于模型部署。动态图提供灵活的可编程性和可调试性,可实时得到计算结果,在模型调优与算法改进迭代方面具有优势。利用计算图和算子间依赖关系可以解决模型中的算子执行调度问题。

二、PNNX计算图

2.1 PNNX介绍

不同的深度学习框架,如Tensorflow、PyTorch、MindSpore等,都定义了自己的模型的数据结构(计算图),推理系统需要将它们转换到统一的一种数据结构上。开发神经网络交换协议**(Open Neural Network Exchange,ONNX)正是为此目的而设计的。ONNX支持广泛的深度学习运算符集合,并提供了不同训练框架的转换器,例如TensorFlow模型到ONNX模型的转换器、PyTorch模型到ONNX模型的转换器等。模型转换本质上是将模型这种结构化的数据**,从一种数据结构转换为另一种数据结构的过程。进行模型转换首先要分析两种数据结构的异同点,然后针对结构相同的数据做搬运;对于结构相似的数据做一一映射;对于结构差异较大的数据则需要根据其语义做合理的数据转换;更进一步如果两种数据结构上存在不兼容,则模型转换无法进行。

ONNX具有表达PyTorch模型的能力,并且它是一个开放标准。人们通常使用 ONNX 作为 PyTorch 和推理平台之间的中间表示。然而ONNX仍然存在以下致命问题:

  • ONNX 没有用户可读和可编辑的文件表示形式,这使得用户很难轻松修改计算图或添加自定义运算符

  • ONNX 的算子定义并不完全符合 PyTorch。将训练好的模型导出为ONNX结构之后,模型中的一个复杂算子不仅经常会被拆分成多个细碎的算子,而且为了将这些细碎的算子拼接起来完成原有算子的功能,通常还需要一些称之为“胶水算子”的辅助算子,例如GatherUnsqueeze等。过于细碎的计算图不利于推理的优化。另外,拆分的层次过于细致,也会导致算法工程师难以将导出的模型和原始模型进行结构上的相互对应。在导出一些 PyTorch 算子时,ONNX 往往会被动添加胶水算子,这使得计算图与 PyTorch 不一致,并可能影响推理效率。

  • ONNX 中的运算符定义中有大量附加参数,这些参数增加了硬件和软件推理实现的负担。

为了解决以上问题,我们选用NCNN推理框架的计算图格式之一PNNX(PyTorch Neural Network eXchange),PNNX 为 PyTorch 提供开放模型格式,PNNX 尝试定义一套与 PyTorch 的 python api 完全对接的算子以及简单易用的格式,使得 PyTorch 模型的转换和互操作更加便捷,它定义的计算图以及高级运算符,与 PyTorch 严格匹配。

通常⼀个网络模型文件从PyTorch 先经历了TorchScript(.pt文件)的导出,然后再转换为其它模型(ONNX、PNNX),经过 PNNX 的优化可以得到最终的模型文件,这里不用管最后导出为 NCNN 的部分。

image-20240815181059229

1.PNNX 始终保留 PyTorch提供的算子操作

import torch
import torch.nn as nnclass Model(nn.Module):def __init__(self):super(Model, self).__init__()self.attention = nn.MultiheadAttention(embed_dim=256, num_heads=32)def forward(self, x):x, _ = self.attention(x, x, x)return x

下面是 ONNX、TorchScript 和 PNNX 之间的 netron 可视化比较(TorchScript -->ONNX TorchScript --> PNNX ):

ONNXTorchScriptPNNX
MultiheadAttention.onnxMultiheadAttention.ptimg

PNNX使用模板匹配(pattern matching)的方法将匹配到的子图(一般在TorchScript中)用对应等价的大算子替换掉,例如可以将上图子图中的多个小算子(在TorchScript中被拆分的)重新替换为MultiheadAttention算子,可以看到onnx对算子拆分得更加的细致。

2.PNNX 会保留 PyTorch 所定义的表达式。

import torchdef foo(x, y):return torch.sqrt((2 * x + y) / 12)
ONNXTorchScriptPNNX
math.onnxmath.ptmath.pnnx

PyTorch中定义表达式在转换为PNNX之后,会保留表达式的整体结构,而不会被拆分成多个小的加减乘除算子。例如表达式sqrt(div(add(mul(@0,2),@1,1),12))不会被拆分为两个mul算子、一个add算子、一个div和sqrt算子,而是会生成一个表达式算子Expression

3.PNNX 将 PyTorch提供的 torch 函数和 Tensor 成员函数保存为一个运算符。

import torch
import torch.nn.functional as Fclass Model(nn.Module):def __init__(self):super(Model, self).__init__()def forward(self, x):x = F.normalize(x, eps=1e-3)return x
ONNXTorchScriptPNNX
函数.onnxfunction.ptfunction.pnnx

参考资料:https://zhuanlan.zhihu.com/p/427620428、https://github.com/Tencent/ncnn/tree/master/tools/pnnx#the-pnnxparam-format

2.2 PNNX计算图结构

在 PNNX 中,计算图的核心结构包括 Graph(图结构)、Operator(运算符)、和 Operand(操作数)。这些结构共同作用,构成了 PNNX 用于表示和优化神经网络模型的基础。

  • Graph: Graph 是 PNNX 用于表示整个神经网络模型的计算图,由多个Operator串联得到的有向无环图,规定了各个计算节点(Operator)执行的流程和顺序。它包含了模型中的所有运算符(Operator)和操作数(Operand),并通过这些组件描述模型的计算流程。

  • Operator: Operator 是计算图中的节点,表示模型中的具体操作或层次,其包含 type(表示操作的类型,例如卷积、ReLU等)、name(操作的名称)、params(参数)和 attrs(属性)等字段。

  • OperandOperand 是计算图中的边,表示数据流动。它们通常是张量(Tensor), 用于存放多维数据,作为 Operator 的输入和输出,方便数据在计算节点之间传递。

  • Layer: 计算节点中运算的具体执行者,Layer类先读取输入张量中的数据,然后对输入张量进行计算,得到的结果存放到计算节点的输出张量中,不同的算子中Layer的计算过程会不一致

PNNX计算图Linear节点属性
image-20240815204526749image-20240815204639847

上图中模型在PyTorch中的定义如下,其作用是对输入x进行线性映射(从32维到128维),并对输出进行sigmoid计算,从而得到最终的计算结果。

class Model(nn.Module):def __init__(self):super(Model, self).__init__()self.linear = nn.Linear(32, 128)def forward(self, x):x = self.linear(x)x = F.sigmoid(x)return x
  • Linear层有#0#1两个操作数(Operand),分别为输入和输出张量,形状依次为(1, 32)(1, 128);
  • Linear层有两个属性参数@weight@bias,用于存储该层的权重数据信息,分别对应权重(即weight)和偏置(即bias)。可以看到这两个权重的形状分别为(1, 32)(1, 128),在后续过程中可以根据需要进行权重加载。
  • Linear层有三个属性:bias, in_featuresout_features,分别表示是否使用偏置项、线性连接层的输入维度和输出维度。

2.3 Graph图结构

Graph在runtime文件夹ir.h中定义的(ncnn中在tools/pnnx/src/ir.h),用于描述神经网络模型的基本数据结构和操作。该文件定义了一个描述神经网络模型的中间表示**(IR)层次结构**。它包含了表示模型参数、属性、操作数和操作的类,以及操作这些类的方法。这些定义提供了一个抽象层,用于描述和操作神经网络模型。

class Graph
{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);Operand* new_operand(const torch::jit::Value* v);Operand* new_operand(const std::string& name);Operand* get_operand(const std::string& name);std::vector<Operator*> ops;       // 运算符(算子)std::vector<Operand*> operands;   // 操作数
};

Graph的核心作用是管理计算图中的运算符和操作数

  1. Operator类用来表示计算图中的运算符(算子),比如Convolution, Pooling等算子;
  2. Operand类用来表示计算图中的操作数,即与一个运算符有关的输入和输出张量
  3. Graph类的成员函数提供了方便的接口用来创建和访问操作符和操作数,以构建和遍历计算图。同时,它也是模型中运算符(算子)和操作数的集合

2.4 Operator运算符

PNNX中的运算符结构Operator定义如下:

class Operator
{
public:std::vector<Operand*> inputs;std::vector<Operand*> outputs;// keep std::string typed member the last for cross cxxabi compatibilitystd::string type;std::string name;std::vector<std::string> inputnames;std::map<std::string, Parameter> params;std::map<std::string, Attribute> attrs;
};

在PNNX中,Operator用来表示一个算子,它由以下几个部分组成:

  1. inputs:类型为std::vector<operand>, 表示这个算子在计算过程中所需要的输入操作数operand
  2. outputs:类型为std::vector<operand>, 表示这个算子在计算过程中得到的输出操作数operand
  3. typename类型均为std::string, 分别表示该运算符号的类型和名称
  4. params, 类型为std::map, 用于存放该运算符的所有参数(例如卷积运算符中的params中将存放stride, padding, kernel size等信息);
  5. attrs, 类型为std::map, 用于存放该运算符所需要的具体权重属性(例如卷积运算符中的attrs中就存放着卷积的权重和偏移量,通常是一个float32数组)。

2.5 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=cp32int type;std::vector<int> shape;// keep std::string typed member the last for cross cxxabi compatibilitystd::string name;std::map<std::string, Parameter> params;};

producercustomers, 分别表示生成该操作数的操作算子使用该操作数的操作算子列表。注意,产生这个操作数的算子只能有一个,而使用这个操作数的算子可以有很多个。

2.6 Attribute与Parameter

在PNNX中,**权重数据结构(Attribute)和参数数据结构(Param)**定义如下,它们通常与一个运算符相关联,例如Linear算子的in_features属性和weight权重。

class Parameter
{
public:Parameter(): type(0){}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=othersint type;  // 用于表示 Parameter 对象的具体类型// valuebool b;int i;float f;std::vector<int> ai;std::vector<float> af;// keep std::string typed member the last for cross cxxabi compatibilitystd::string s;std::vector<std::string> as;
};
class Attribute
{
public:Attribute(): type(0){}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=boolint type;std::vector<int> shape;std::vector<char> data;
};

以上来源于nccn中的pnnx的src。

  • Graph 类 : 是整个计算图的控制中心,它管理着 OperatorOperand,即图中的节点和边。Graph 包含了一个 ops 向量,用来存储所有的 Operator 对象;还有一个 operands 向量,用来存储所有的 Operand 对象。

  • Operator 类 : 是计算图中的节点,代表着某种操作。每个 Operator 都有一个 inputs 向量,用来存储指向输入 Operand 的指针;还有一个 outputs 向量,用来存储指向输出 Operand 的指针。Operator 还包含 type(表示操作的类型,例如卷积、ReLU等)、name(操作的名称)、params(参数)和 attrs(属性)等字段。

  • Operand 类 : 是计算图中的边,表示模型中的操作数,代表着数据流动。它有一个 producer 指针,指向生成该 OperandOperator,还有一个 consumers 向量,存储着所有使用该 OperandOperatorOperand 还包含了 type(数据类型)、shape(张量形状)、name(操作数名称)和 params(参数)等字段。

  • Parmeter 类:表示操作符的参数,这些参数通常是一些标量或向量类型的数据,用于配置操作符的行为。例如,一个卷积操作的核大小、步幅、填充方式等都可以作为 Parameter

  • Attribute 类 : 表示操作符的权重或常量数据,这些数据通常是在训练阶段确定的,并在推理阶段保持不变。例如,卷积层的权重、偏置项等都可以作为 Attribute

小结:

Graph 组织和管理 OperatorOperand,形成完整的计算图。

Operator 通过 inputsoutputsOperand 连接,形成数据流动的路径。

Operand 通过 producerconsumers 确定数据的流向,并与多个 Operator 关联。

ParameterAttribute 在 PNNX 中分别用于处理操作符的配置参数(卷积核大小,步长等)权重数据(卷积层权重,偏置)

三、RuntimeGraph

3.1 RuntimeGraph整体介绍

下面对PNNX中的计算图进一步封装,实现RuntimeGraph,集成了 PNNX 的 Graph 以管理计算节点(RuntimeOperator)和数据流(Operand)。

/// 计算图结构,由多个计算节点和节点之间的数据流图组成
class RuntimeGraph {
public:RuntimeGraph(std::string param_path, std::string bin_path);// 计算图的初始化,会调用下面各初始化函数bool Init();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 voidInitGraphAttrs(const std::map<std::string, pnnx::Attribute> &attrs,const std::shared_ptr<RuntimeOperator> &runtime_operator);/*** 初始化kuiper infer计算图中的节点参数* @param params pnnx中的参数属性* @param runtime_operator 计算图节点*/static voidInitGraphParams(const std::map<std::string, pnnx::Parameter> &params,const std::shared_ptr<RuntimeOperator> &runtime_operator);public:
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
};

RuntimeGraph 使用了 PNNX 的 Graph 作为其内部数据结构,存储了计算图的节点和边。在 RuntimeGraph 中,graph_ 是一个指向 PNNX Graph 的独占指针 (std::unique_ptr<pnnx::Graph>),用于表示整个计算图。

RuntimeGraph 将 PNNX 的 OperatorOperand 结构映射到自定义的 RuntimeOperatorRuntimeOperand,并在初始化Init()函数中设置它们之间的输入输出关系。这些映射操作由以下函数完成:

  • InitGraphOperatorsInput:初始化计算图节点中的输入操作数。
  • InitGraphOperatorsOutput:初始化计算图节点中的输出操作数。
  • InitGraphAttrs:初始化计算图节点中的属性。
  • InitGraphParams:初始化计算图节点中的参数

这些函数用于初始化和管理推理节点RuntimeOperator)的输入、输出、属性和参数,并且这些函数基于 PNNX Graph 中的数据进行操作。

PNNX的Graph和RuntimeGraph 联系:PNNX 的 Graph 主要用于表示和处理模型的计算图结构,提供了模型的结构化表示RuntimeGraph 则专注于推理阶段,使用 PNNX Graph 提供的数据来初始化并管理推理过程中的计算节点和数据流。通过这种方式,RuntimeGraph 能够灵活地管理推理过程中的操作节点和数据流,同时充分利用 PNNX 提供的模型表示和处理能力。

在RuntimeGraph中,RuntimeOperator、RuntimeOperand、RuntimeParameter以及RuntimeAttribute的UML结构图如下:

在这里插入图片描述

RuntimeOperator 表示计算图中的一个操作节点,每个节点对应着一个特定的计算任务,例如卷积、激活等操作。RuntimeOperand 表示计算节点的输入或输出的数据。它可以视为计算图中节点之间连接的边,传递数据。

RuntimeOperatorRuntimeOperand关系

  • 输入输出关系:每个 RuntimeOperator 通过 input_operands 接收一个或多个 RuntimeOperand 作为输入,通过 output_operands 产生一个或多个 RuntimeOperand 作为输出。这些操作数代表了节点之间传递的数据流。

  • 数据流与计算流的联动: RuntimeOperandRuntimeOperator 的输入和输出数据。操作数的数据流(RuntimeOperand)决定了计算流(RuntimeOperator)的执行顺序和依赖关系。

  • 计算图的构建: 在 RuntimeGraph 中,这些 RuntimeOperator 通过 RuntimeOperand 连接起来,形成一个有向无环图(DAG),用于描述整个模型的计算流程。

总之,RuntimeOperandRuntimeOperator 的输入和输出,而多个 RuntimeOperator 通过 RuntimeOperand 连接,形成完整的计算图结构。

3.2 RuntimeOperator

RuntimeOperatorKuiperInfer计算图中的核心数据结构,是对PNNX::Operator的再次封装,在runtime_op文件中,它有如下的定义:

/// 计算图中的计算节点
struct RuntimeOperator {virtual ~RuntimeOperator();bool has_forward = false;std::string name;              /// 计算节点的名称std::string type;              /// 计算节点的类型std::shared_ptr<Layer> layer;  /// 节点对应的计算Layerstd::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的结构体。结构体包含以下成员变量:

  1. name: 运算符节点的名称,可以用来区分一个唯一节点,例如 Conv_1, Conv_2 等;

  2. type: 运算符节点的类型,例如 Convolution, Relu 等类型;

  3. layer: 负责完成具体计算的组件,例如在 Convolution Operator 中,layer 对输入进行卷积计算,即计算其相应的卷积值;

  4. input_operandsoutput_operands 分别表示该运算符的输入和输出操作数

    如果一个运算符(RuntimeOperator)的输入大小为 (4, 3, 224, 224),那么在 input_operands 变量中,datas 数组的长度为 4,数组中每个元素的张量大小为 (3, 224, 224)

  5. params 是运算符(RuntimeOperator)的参数信息,包括卷积层的卷积核大小、步长等信息;

  6. attribute 是运算符(RuntimeOperator)的权重、偏移量信息,例如 Matmul 层或 Convolution 层需要的权重数据;

  7. 其他变量的含义可参考注释。

在这个过程中,需要先从 PNNX::Operator提取数据信息(包括 OperandOperator 结构),并依次填入到 KuiperInfer 对应的数据结构中。相应的代码如下所示,由于篇幅原因,在课件中省略了一部分内容,完整的代码可以在runtime_ir.cpp 文件夹中查看。

bool RuntimeGraph::Init() {if (this->bin_path_.empty() || this->param_path_.empty()) {LOG(ERROR) << "The bin path or param path is empty";return false;}this->graph_ = std::make_unique<pnnx::Graph>();int load_result = this->graph_->load(param_path_, bin_path_);if (load_result != 0) {LOG(ERROR) << "Can not find the param path or bin path: " << param_path_<< " " << bin_path_;return false;}std::vector<pnnx::Operator *> operators = this->graph_->ops;// 在for循环中依次对每个运算符进行处理for (const pnnx::Operator *op : operators) {std::shared_ptr<RuntimeOperator> runtime_operator = std::make_shared<RuntimeOperator>();// 初始化算子的名称,提取PNNX运算符中的名字(name)和类型(type).runtime_operator->name = op->name;runtime_operator->type = op->type;// 初始化算子中的inputconst std::vector<pnnx::Operand *> &inputs = op->inputs;InitGraphOperatorsInput(inputs, runtime_operator);// 记录输出operand中的名称const std::vector<pnnx::Operand *> &outputs = op->outputs;InitGraphOperatorsOutput(outputs, runtime_operator);// 初始化算子中的attribute(权重)const std::map<std::string, pnnx::Attribute> &attrs = op->attrs;InitGraphAttrs(attrs, runtime_operator);// 初始化算子中的parameterconst std::map<std::string, pnnx::Parameter> &params = op->params;InitGraphParams(params, runtime_operator);this->operators_.push_back(runtime_operator);this->operators_maps_.insert({runtime_operator->name, runtime_operator});}return true;
}

RuntimeGraph::Init() 函数**负责从 PNNX 格式的计算图文件中读取图结构,并将其转换为适用于 RuntimeGraphRuntimeOperator 格式。**这些操作包括加载图文件、解析操作符的输入输出、初始化属性和参数等。这个函数的顺利执行是后续图推理或训练的基础。

3.3 RuntimeOperand

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

RuntimeOperand 是在计算图中表示操作数的数据结构,用于存储每个计算节点的输入和输出。RuntimeGraph::InitGraphOperatorsInputRuntimeGraph::InitGraphOperatorsOutput 两个函数负责初始化 RuntimeOperator 中的输入和输出操作数。这两个函数在上面RuntimeGraph::Init() 中调用的,它们对 RuntimeOperand 的初始化如下:

void RuntimeGraph::InitGraphOperatorsInput(const std::vector<pnnx::Operand *> &inputs,const std::shared_ptr<RuntimeOperator> &runtime_operator) {// 遍历所有的输入张量for (const pnnx::Operand *input : inputs) {if (!input) {continue;}const pnnx::Operator *producer = input->producer;std::shared_ptr<RuntimeOperand> runtime_operand = std::make_shared<RuntimeOperand>();// 设置操作数的名称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;}// 将初始化的操作数添加到 RuntimeOperator 的输入操作数映射和顺序列表中runtime_operator->input_operands.insert({producer->name, runtime_operand});runtime_operator->input_operands_seq.push_back(runtime_operand);}
}

**这段代码的两个参数分别是来自 PNNX 中的一个运算符的所有输入操作数(Operand)和待初始化的 RuntimeOperator。**在以下的循环中:

  for (const pnnx::Operand *input : inputs) 

需要依次将每个 Operand 中的数据信息填充到新初始化的 RuntimeOperand,包括 type, name, shapes 等信息,并记录输出这个操作数(Operand)的运算符(producer)。然后,再将数据完备的 RuntimeOperand 插入到待初始化的 RuntimeOperator 中。

然后InitGraphOperatorsOutput初始化计算节点(RuntimeOperator)的输出操作数。在这个函数中,虽然没有直接初始化 RuntimeOperand,但它处理了输出操作数的关联信息:

void RuntimeGraph::InitGraphOperatorsOutput(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);}}
}

这段代码的两个参数分别是来自 PNNX 中的一个运算符的所有输出操作数Operand)和待初始化的 RuntimeOperator在这里,只需要记录操作数的消费者的名字(customer.name)即可。后面,我们才会对 RuntimeOperator 中的输出操作数(RuntimeOperand)进行构建。

RuntimeGraph::InitGraphOperatorsInput 主要负责初始化 RuntimeOperand,包括其名称、形状和数据类型,并将其添加到对应 RuntimeOperator 的输入操作数中。

RuntimeGraph::InitGraphOperatorsOutput 主要负责记录输出操作数的消费者信息,并将消费者的名称存储在 RuntimeOperatoroutput_names 中,但不直接初始化 RuntimeOperand

3.4 RuntimeAttribute

RuntimeAttribute 是用来存储计算图节点(RuntimeOperator)的属性信息的结构体,通常包含权重参数、形状信息和数据类型。

/// 计算图节点的属性信息
struct RuntimeAttribute {std::vector<char> weight_data;  /// 节点中的权重参数std::vector<int> shape;         /// 节点中的形状信息RuntimeDataType type = RuntimeDataType::kTypeUnknown;  /// 节点中的数据类型// 从节点中加载权重参数template <class T>  //std::vector<T> get(bool need_clear_weight = true);//  清除权重void ClearWeight();
};

RuntimeGraph::InitGraphAttrs 函数则负责从 pnnx 的节点属性(pnnx::Attribute)中初始化并填充 RuntimeAttribute,并将这些属性关联到对应的 RuntimeOperator 中。

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;// 将 pnnx::Attribute 中的权重数据拷贝到 RuntimeAttribute 的 weight_data 中runtime_attribute->weight_data = attr.data;// 将 pnnx::Attribute 中的形状信息拷贝到 RuntimeAttribute 的 shape 中runtime_attribute->shape = attr.shape;// 将已初始化的 RuntimeAttribute 添加到 RuntimeOperator 的 attribute 映射中runtime_operator->attribute.insert({name, runtime_attribute});break;}default: {LOG(FATAL) << "Unknown attribute type: " << attr.type;}}}
}

这段代码的两个参数分别是来自 PNNX 中的一个运算符的所有权重数据结构(Attribute)和待初始化的RuntimeOperator。在以下的循环中,

for (const auto& [name, attr] : attrs)

需要依次将 Attribute 中的数据信息填充到新初始化的 RuntimeAttribute,包括 type, weight_data, shapes 等信息。然后,将数据完备的 RuntimeAttribute 插入到待初始化的 RuntimeOperator 中,同时记录该权重的名字。

Linear层中这里的name就是weightbias, 对于前文测试模型中的Linear层,它的weight shape是(32, 128),weight_data就是32 x 128个float数据。

3.5 RuntimeParam

/// 计算节点中的参数信息
struct RuntimeParameter { virtual ~RuntimeParameter() = default;explicit RuntimeParameter(RuntimeParameterType type = RuntimeParameterType::kParameterUnknown) : type(type) {}RuntimeParameterType type = RuntimeParameterType::kParameterUnknown;
};struct RuntimeParameterInt : public RuntimeParameter {RuntimeParameterInt() : RuntimeParameter(RuntimeParameterType::kParameterInt) {}int value = 0;
};

RuntimeParameter 是一个抽象类或接口,用于表示运行时参数。在推理系统中,运行时参数通常用于表示模型中节点的配置或权重等数据。它有多个子类,分别对应不同的数据类型,如 intfloatstringbool 以及它们的数组类型。

RuntimeGraph::InitGraphParams 函数的作用是从 pnnx::Parameter 中读取节点参数数据,并将其转换为 RuntimeParameter 的具体子类对象,然后将这些参数与对应的 RuntimeOperator 关联。

void RuntimeGraph::InitGraphParams(const std::map<std::string, pnnx::Parameter> &params,const std::shared_ptr<RuntimeOperator> &runtime_operator) {for (const auto &[name, parameter] : params) {const int type = parameter.type;switch (type) {// 对应不同的参数类型,根据类型创建对应的 RuntimeParameter 子类对象case int(RuntimeParameterType::kParameterUnknown): {RuntimeParameter *runtime_parameter = new RuntimeParameter;runtime_operator->params.insert({name, runtime_parameter});break;}case int(RuntimeParameterType::kParameterBool): {RuntimeParameterBool *runtime_parameter = new RuntimeParameterBool;runtime_parameter->value = parameter.b;runtime_operator->params.insert({name, runtime_parameter});break;}......case int(RuntimeParameterType::kParameterStringArray): {RuntimeParameterStringArray *runtime_parameter = new RuntimeParameterStringArray;runtime_parameter->value = parameter.as;runtime_operator->params.insert({name, runtime_parameter});break;}default: {LOG(FATAL) << "Unknown parameter type: " << type;}}}
}

通过这种方式,每个 RuntimeOperator 节点都能够访问和使用其参数信息,从而在计算过程中可以依据这些参数进行操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/402239.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

引领企业全球化发展 极光亮相华为亚太ICT峰会2024·泰国

近日&#xff0c;华为亚太ICT峰会2024泰国正式开幕&#xff0c;极光&#xff08;Aurora Mobile&#xff0c;纳斯达克股票代码&#xff1a;JG&#xff09;凭借其创新的技术实力与前瞻性的产品布局&#xff0c;受邀出席本次活动。会上&#xff0c;极光展示了其全域消息通知解决方…

【ocr识别003】flask+paddleocr+bootstrap搭建OCR文本推理WEB服务

1.欢迎点赞、关注、批评、指正&#xff0c;互三走起来&#xff0c;小手动起来&#xff01; 2.了解、学习OCR相关技术知识领域&#xff0c;结合日常的场景进行测试、总结。如本文总结的flaskpaddleocrbootstrap搭建OCR文本推理WEB服务应用示例场景。 文章目录 1.代码结构2.效果演…

MySQL 在 Windows 和 Ubuntu 上的安装与远程连接配置简介

MySQL 是一个广泛使用的开源关系型数据库管理系统&#xff0c;它提供了多用户、多线程的数据库服务。本文将介绍如何在 Windows 和 Ubuntu 操作系统上安装 MySQL&#xff0c;并配置远程连接。 Windows 上的 MySQL 安装 1. 下载 MySQL Installer 访问 MySQL 官方网站下载 Win…

融合创新:EasyCVR视频汇聚平台云计算技术与AI技术共筑雪亮工程智能防线

随着信息技术的飞速发展&#xff0c;视频云计算技术作为云计算领域的一个重要分支&#xff0c;正逐步在公共安全、社会治理等领域展现出其独特的优势。特别是在雪亮工程这一群众性治安防控工程中&#xff0c;视频云计算技术更是发挥了不可替代的作用。本文将从视频云计算技术的…

【leetcode详解】特殊数组II : 一题代表了一类问题(前缀和思想)

前缀和的优势 给定一个数组&#xff0c;前缀和的特点在于&#xff0c;任意给出一对始末位置&#xff0c;能够用O(1)的时间复杂度得到始末位置之间所有元素的某种关系。 题型分析 这道题目正是“给出始末位置&#xff0c;检测其中元素特点”那一类&#xff0c;那我们就想&#…

自动化与高效设计:推理技术在FPGA中的应用

想象一下&#xff0c;你正在设计一个复杂的电路系统&#xff0c;就像在搭建一座精巧的积木城堡。你手头有各种形状和功能的积木块&#xff0c;这些积木块可以组合成任何你需要的结构。在这个过程中&#xff0c;你有两种主要的方法&#xff1a;一种是手动挑选和搭建每一块积木&a…

【Qt】内置对话框

一.Qt内置对话框 Qt 提供了多种可复⽤的对话框类型&#xff0c;即 Qt 标准对话框。Qt标准对话框全部继承于QDialog类。常⽤标准对话框如下&#xff1a; 二.内置对话框分类 1.消息对话框 QMessageBox 1.1 概念 消息对话框是应⽤程序中最常⽤的界⾯元素。消息对话框主要⽤于为…

Android-RK356x GT9XX多点触控设置为单点触控的方法

本文基于RK356x Android11系统描述GT9XX驱动芯片由多点触摸改为单点触摸功能。本次介绍的是触觉智能的Purple Pi OH鸿蒙开源主板&#xff0c;Purple Pi OH是华为Laval官方社区主荐的一款鸿蒙开发主板。 该主板主要针对学生党&#xff0c;极客&#xff0c;工程师&#xff0c;极大…

Opencv模板匹配

使用OpenCV和C来识别彩色图片中的特定物体&#xff0c;如黑桃♠&#xff0c;通常涉及几个步骤&#xff1a;预处理图像、特征提取、对象检测等。下面是一个基本的示例代码&#xff0c;演示如何使用OpenCV的模板匹配方法来识别图片中的黑桃♠。 函数原型 void matchTemplate(Inp…

【Mac】植物大战僵尸杂交版 for Mac(经典策略塔防游戏)游戏介绍

游戏介绍 植物大战僵尸杂交版 for Mac是一款非常受欢迎的策略塔防游戏&#xff0c;植物大战僵尸游戏以其独特的主题、幽默的风格和富有挑战性的关卡设计而著称。玩家需要种植各种植物来防御入侵的僵尸&#xff0c;每种植物都有其特定的功能和攻击方式。植物大战僵尸杂交版&…

老友记台词 第一季 第十五集 Friends 115(全英版)

文章目录 115 The One With the Stoned Guy[Scene: Central Perk, Rachel is serving Joey, Ross, and Monica their drinks.][Scene: Chandlers job, Chandler is typing data into his computer, he keeps typing even while taking a drink of coffee with one hand. One of…

VScode前端环境搭建

前言 VScode是企业中最常用的前端开发工具&#xff0c;本文描述如何利用VScode搭建前端开发环境 一、安装VScode 下载Vscode 点击前往下载页面 安装 安装时一直点击下一步即可 二、环境配置 1&#xff09;更改语言 点击拓展搜索Chinese后下载第一个&#xff0c;下载完后…

Bruno API 工具

Bruno 是Postman 和Insomnia 的开源桌面替代品&#xff0c;用于 API 的测试、开发和调试。它将测试集合保存在本地&#xff0c;因此可以使用 Git 或其他版本控制工具来进行协作。 下载地址: https://www.usebruno.com/downloads 功能 1. 左边菜单 Collections Create Collec…

影院订票系统/电影院售票系统/电影院购票系统的设计与实现/影院管理系统

摘 要 “互联网”的战略实施后&#xff0c;很多行业的信息化水平都有了很大的提升。但是目前很多电影院日常业务仍是通过人工管理的方式进行&#xff0c;需要在影院订票投入大量的人力进行很多重复性工作&#xff0c;这样就浪费了许多的人力物力&#xff0c;工作效率较低&…

Nginx 核心配置详解

目录 1 配置文件说明 1.1 nginx 配置文件格式说明 1.2 Nginx 主配置文件的配置指令方式&#xff1a; 1.3 主配置文件结构&#xff1a;四部分 1.4 nginx 文件作用解释 1.5 配置文件说明 2 nginx-web应用 2.1 定义进程数以及进程绑定 2.2 定义进程优先级与文件打开上限 2.3 even…

解锁冻结的 iPhone 和 iPad 的具体教程

许多苹果用户选择 iDevices 主要是因为他们认为苹果产品更稳定&#xff0c;使用效果也更好。然而&#xff0c;一些苹果用户报告说他们的 iPhone 或 iPad 屏幕没有响应。换句话说&#xff0c;他们的 iOS 设备被冻结了。如果你想解决这样的故障并恢复 iOS 数据&#xff0c;你可以…

挺进大别山(一)

大别山坐落于安徽省、湖北省、河南省交界处&#xff0c;是长江与淮河的分水岭。8月3日&#xff0c;我们早上8点&#xff0c;从马村出发&#xff0c;穿越郑州--许昌--周口--驻马店--信阳&#xff0c;日行5百多公里&#xff0c;到了安徽进入了大别山。近距离的领略了它的魅力。 雨…

Cesium模型制作,解决Cesium加载glb/GLTF显示太黑不在中心等问题

Cesium模型制作&#xff0c;解决Cesium加载glb/GLTF显示太黑不在中心等问题 QQ可以联系这里&#xff0c;谢谢

鸿蒙环境和模拟器安装

下载华为开发者工具套件&#xff0c;并解压 https://developer.harmonyos.com/deveco-developer-suite/enabling/kit?currentPage1&pageSize10 双击dmg安装ide 复制并解压sdk 安装模拟器 https://yuque.antfin-inc.com/ainan.lsd/cm586u/po19k1mi9b2728da?singleDoc#…

算法【Java】—— 双指针算法

双指针算法 常见的双指针有对撞指针&#xff0c;快慢指针以及前后指针&#xff08;这个前后指针是指两个指针都是从从一个方向出发&#xff0c;去往另一个方法&#xff0c;也可以认为是小学学习过的两车并行&#xff0c;我也会叫做同向指针&#xff09;&#xff0c;在前后指针…