KuiperInfer深度学习推理框架-源码阅读和二次开发(3):计算图

news2024/12/23 0:15:49

前言:KuiperInfer是一个从零实现一个高性能的深度学习推理库,中文教程已经非常完善了。本系列博客主要是自己学习的一点笔记和二次开发的教程,欢迎更多的AI推理爱好者一起来玩。这篇写一下计算图相关的知识点,重点说明ONNX有什么缺点?为什么选择PNNX?如何构建计算图?PNNX的计算图里有哪些优化手段值得学习?

目录

为什么选择PNNX?

从OpenPPL的角度看为什么不选择ONNX?

讨论

构建计算图

C++ 前向声明

学习:PNNX中的计算图优化手段

1. Constant Folding

2. 去除冗余算子

3. 各种融合Pass

4. 算子等效变化Transform

参考


为什么选择PNNX?

首先这个问题要问nihui,为什么有了onnx还要再写一个pnnx?当然是为了有产出好晋升。当然是onnx有其自己的缺点,强烈推荐nihui自己写的博客:

PNNX: PyTorch Neural Network Exchange - 知乎

这里我总结一下:

  • PyTorch 一些模型的算子可能在 ONNX 是没有的,这样导出 ONNX 的时候会出错导不出来。
  • PyTorch 导 ONNX,有时候会导出一个非常复杂的计算图,这个情况会导致推理效率的下降。
  • ONNX对第三方库的算子支持不够;导出的计算图比较复杂、细碎。

从OpenPPL的角度看为什么不选择ONNX?

我在之前openppl/ppl的文章里有写过,在做量化之前做了大量的计算图优化操作:

https://xduwq.blog.csdn.net/article/details/128839708

事实上,量化精度很大程度上取决于推理的支持,如果直接用onnx做推理的话,算子拆的稀巴烂,插入的量化节点必然会过多,严重影响量化后的模型推理。所以ppq在做量化之前做了大量的预处理-计算图优化。 

https://xduwq.blog.csdn.net/article/details/128839708

讨论

当然会有很多大佬觉得根本没有必要花大力气再造一个ONNX,排除个人因素(影响个人升职加薪)之外,很多人会觉得我们只要像OpenPPL那样自己再做一些图优化就行。

我个人觉得PNNX还是挺好用的,真香就完事…… 

构建计算图

这一方面傅大狗说的非常详细,kuiper会封装PNNX,然后转换成自己的计算图,请看原文和视频:

自制深度学习推理框架-第七课-构建自己的计算图

自制深度学习推理框架-第七课-构建自己的计算图 - 知乎

C++ 前向声明

在定义IR的时候使用了C++中前向声明的知识点。先看一下源代码(在KuiperInfer/ir.h):

class Operator;
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;

};

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;
};

前向声明:可以声明一个类而不定义它。这个声明,有时候被称为前向声明(forward declaration)。在声明之后,定义之前,类Operator是一个不完全类型(incompete type),即已知Operator是一个类型,但不知道包含哪些成员。不完全类型只能以有限方式使用,不能定义该类型的对象,不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。

请注意:尽管使用了前向引用声明,但是在提供一个完整的类声明之前,不能定义该类的对象,也不能在内联成员函数中使用该类的对象。 即当你使用前向引用声明时,你只能使用被声明的符号,而不能涉及类的任何细节。

如以下写法是错误的:

class Fred;    //前向引用声明

class Barney
{
public:
    void method()
    {
        x->yabbaDabbaDo();    //错误:Fred类的对象在定义之前被使用
    }
private:
    Fred* x;   //正确,经过前向引用声明,可以声明Fred类的对象指针
};

class Fred
{
public:
    void yabbaDabbaDo();
private:
    Barney* y;
};

学习:PNNX中的计算图优化手段

计算图优化手段是很多AI推理框架/AI编译器的重要卖点之一,而PNNX在这方面做的非常好,非常值得抄袭借鉴!

ncnn/tools/pnnx at master · Tencent/ncnn · GitHub

PNNX分成了5个level优化,很像AI编译器高pass优化到低pass优化的思路啊:

pnnx的五级pass
CINN的优化

 博主的功力还没有太多这方面的经验,我们可以看看大佬是怎么总结的:

如何选择深度学习推理框架? - 知乎

1. Constant Folding

首先是常量折叠,有时候我们有一些计算是会用到常量的,这些常量可以初始化成为图的一部分,不需要在推理的时候去算。适用于常量折叠的情况包括:

  • 没有输入节点的算子,但是又不是input,例如引入的一个常量输入到某个算子中,这个Node是需要被挑出来的;
  • 对于中间计算用到了Shape的操作,如果输入是固定的,那么这个shape可以被实现固定,不过对于torchscript来说,这点比较麻烦,因为torchscript默认都是动态的;
  • 一些奇葩的操作可以被shrink,例如你在Conv外面又算了一次Pad,此时我们可以把Pad的操作柔到Conv里。

找到了这些可以被折叠的算子之后,就可以做折叠,其实就是把这些Node,改成Attribute,然后新建一个Operator,把这个Attribute当作是改Operator的固定参数。

2. 去除冗余算子

例如一些十分简单的:

  • Identity Elimination;
  • Slice Elimination;
  • Unsqueeze Elimination;
  • Dropout Elimination;
  • Expand Elimination;
  • Pooling Elimination;
  • Duplicated Reshape Elimination;
  • Opposite Operator Elimination;
  • Common subGraph Elimination;

这些都是基本的操作,dropout大家都懂,Identity这个玩意儿实际上就是返回你的输入,也就是说你给他什么输入,他给你返回什么,看起来什么都没做,但是有些人写网络的和会用到这个,比如你要删除某个算子,但是又不想影响后面的操作,假设维度不变的时候,你就可以直接把那一层变为Identity。

这个Identity去掉的逻辑也很简单,遍历整个图,看看哪些算子的输入输出是一样的,如果是,那就把他删掉,直接把当前节点的输入接到下一个节点的输入。

对于Slice的剔除,主要是针对一个维度为d,然后slice为 [0, d-1]的操作,这就是slice了个寂寞,我们可以直接干掉。判断方法也很简单:

if (!op->inputs[0]->shape.empty() && op->inputs[0]->shape == op->outputs[0]->shape)
{
    matched = true;
}

直接看输入shape是不是空,以及shape是否slice了个寂寞。

还有Expand也是可以被拿掉的,如果你Expand了之后维度没变,那不就是expand了个寂寞么?直接干掉,判断逻辑可以复用Slice。

这个Pooling在1x1的时候是可以被干掉的,你pool了等于没有pool,要你何用?

当然后面这两种情况,取决于你的实际支持模型,一般高级一点的算法工程师不会干出这么猥琐的事情。

还有重复的Reshape,是可以合并成一个的。

对于前后反义的算子也可以被拿掉,例如,你先Squeeze了一下,然后又Expandim了,而且axis一样,就可以被换成Identity,然后被后面的Pass拿掉。所以你在此看到了Identity的应用了。

公共子图也是可以被优化的。例如,多个输入输入到了一个相同的Node后者Module,此时可以将其合并成一个。

最后其实还有一些量化相关的操作可以拿掉,比如:

  • Quant-dequant Elimination;

关于QDQ的支持暂时不展开了,这个坑比较深。

3. 各种融合Pass

其实这些操作在量化里面也会用到,而且这些fusion非常有必要,可以极大的提高模型的运行速度,例如:

  • Conv + (Add) Bias fusion;
  • Conv + Scale fusion;
  • Conv + Mul fusion;
  • Conv + Relu fusion;
  • MatMul + Add fusion;
  • MatMul + Scale fusion;
  • BN + Scale fusion;
  • Conv + Bn fusion;
  • Conv + Bn + Bias fusion;
  • Conv + Bn + Relu fusion;
  • FC + BN fusion;
  • FC + Add Fusion;

类似于这样的计算等效,目的是减少访存的次数,从而被动增加推理速度。前提是框架里面要有对应的融合后算子的推理支持。

除此之外,你还可以做一些更高级别的融合Pass,这也需要推理Layer的强力支持。例如:

  • BERT Embeding + LayerNorm + AttentionMask fusion;
  • FC + LayerNorm fusion;
  • MultiHead Attention fusion;

这些融合可以进一步的提高模型的速度,在Fastransformer,HuggingFace的transformer ONNXRuntime推理里面,都有这样的操作,尤其是对于Transformer-based的模型,融合大块算子显得非常有必要。

当然,这一部分也有量化相关的融合:

  • Conv + BN + Relu -> ConvInt8;
  • Conv1dQuant -> Conv2dQuant;

4. 算子等效变化Transform

对算子进行等效,也是必不可少的一步,这一步我们将一些不常用的算子替换成常用的算子,由于常用算子通常做了计算优化,可以更快。

  • MatMul to Conv2D;
  • FC (InnerProduct) to Conv2D 1x1;
  • BN to Scale;
  • ShuffleChannel to Reshape + Permute;
  • GroupConv to Conv + Slice

差不多就是这些内容,最后以不就是把这些优化之后,鼓励的算子删掉就行了。

参考

  • c++中的前向声明_前向声明 c++_和大黄的博客-CSDN博客
  • https://www.cnblogs.com/wkfvawl/p/10801725.html
  • PNNX: PyTorch Neural Network Exchange - 知乎
  • ncnn/tools/pnnx at master · Tencent/ncnn · GitHub
  • 如何选择深度学习推理框架? - 知乎

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一口气整理三种不同二维码生成的Java 接入代码

引言 二维码已经成为现代社会中广泛应用的一种工具&#xff0c;它们具有快速、可靠和高容量的信息传递能力。通过扫描二维码&#xff0c;用户可以轻松获取网址、产品信息、支付链接等各种信息。 本文将介绍二维码生成器 API 作为一种工具&#xff0c;并探讨其功能和用法&…

【Linux高级 I/O(4)】异步 IO实例及其优化(全文代码)

异步 I/O 在 I/O 多路复用中&#xff0c;进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。 而在异步 I/O 中&#xff0c;当文件描述符上可以执行 I/O 操作时&#xff0c;进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它…

rocketmq中ConsumeThreadMax不生效原因探究

在项目中设置了ConsumeThreadMax但是消息消费时仍是单线程消费&#xff0c;故而进行追踪排查。 其根本原因是rocketmq中&#xff0c;消费者线程池队列使用的是LinkedBlockingQueue。 rocket版本&#xff1a;rocketmq-client-4.9.3 代码追踪&#xff1a;从DefaultMQPushConsu…

C++设计模式之抽象工厂模式(Abstract Factory)

文章目录 定义前言问题解决方案 结构适用场景实现方式优点缺点与其他模式的关系实例 [C]22种设计模式的C实现大纲 定义 抽象工厂是一种创建型设计模式&#xff0c;它能创建一系列相关的对象&#xff0c;而无需指定其具体类。 前言 问题 假设你正在开发一款家具商店模拟器。…

Multi-Head Attention和Transformer Decoder(GPT)详解

文章目录 一、Transformer的Attention1. Self-Attention2. Masked Self-Attention3. Multi-Head Attention 二、Transformer Decoder&#xff08;GPT&#xff09;1. GPT的网络结构2. GPT的计算原理 一、Transformer的Attention 1. Self-Attention 如前篇文章所述&#xff08;ht…

如何开发一个人人爱的组件?

组件&#xff0c;是前端最常打交道的东西&#xff0c;对于 React、Vue 等应用来说&#xff0c;万物皆组件毫不为过。 有些工作经验的同学都知道&#xff0c;组件其实也分等级的&#xff0c;有的组件可以被上万开发者复用&#xff0c;有些组件就只能在项目中运行&#xff0c;甚…

Springboot +spring security,配置多个数据源:验证不同用户表

一.简介 上篇文章写到&#xff0c;我们在配置jdbc和mybatis 来源&#xff0c;进行登录后&#xff0c;出现了如下错误! 后面解决方案是&#xff1a;屏蔽了其中一个来源&#xff0c;登陆成功&#xff0c;也分析了其原因。 但是&#xff0c;但是如果需要配置多个数据来源&#…

2023年认证杯SPSSPRO杯数学建模B题(第一阶段)考订文本全过程文档及程序

2023年认证杯SPSSPRO杯数学建模 B题 考订文本 原题再现&#xff1a; 古代文本在传抄过程中&#xff0c;往往会出现种种错误&#xff0c;以至于一部书可能流传下来多种版本。在文献学中&#xff0c;错误往往被总结成“讹”、“脱”、“衍”、“倒”等形式&#xff0c;也可能同…

cda 1级模拟题错题知识点总结

Sql truncate函数 格式&#xff1a;TRUNCATE(number, decimals) number: the number to be truncated decimals:the number of decimal places to truncate to 截断到的小数位数&#xff0c;如果为0则表示不保留小数 例如: select truncate(2.83,0) 结果为2 select truncate(…

解读kubernetes部署:配置docker私服密钥与SSL证书创建

为k8s配置docker私服密钥 为了kubernetes有权访问您的docker私服&#xff0c;需要在kubernetes的凭证中建立docker私服的密钥&#xff1a; kubectlcreatesecretdocker-registryaliyun-secret--docker-server--docker-username--docker-password--docker-email--namespacens-jav…

2.golang的变量、常量、数据类型、循环和条件判断

一、变量 变量&#xff08;Variable&#xff09;的功能是存储数据。Go语言中的每一个变量都有自己的类型&#xff0c;并且变量必须经过声明才能开始使用。 Go语言的变量声明格式为&#xff1a; var 变量名 变量类型 例如&#xff1a; var name string var age int var isOk b…

线上问题处理案例:出乎意料的数据库连接池 | 京东云技术团队

导读 本文是线上问题处理案例系列之一&#xff0c;旨在通过真实案例向读者介绍发现问题、定位问题、解决问题的方法。本文讲述了从垃圾回收耗时过长的表象&#xff0c;逐步定位到数据库连接池保活问题的全过程&#xff0c;并对其中用到的一些知识点进行了总结。 一、问题描述…

LabVIEWCompactRIO 开发指南29 数据通信

LabVIEWCompactRIO 开发指南29 数据通信 LabVIEW FPGA中的数据通信分为两类&#xff1a;进程间和目标间。进程间通信通常对应于FPGA目标上的两个或多个环路之间的数据共享。目标间数据通信是在FPGA目标和主机处理器之间共享数据。对于这两种情况&#xff0c;在决定使用哪种机…

扩散能垒计算在电池材料领域的革新应用

扩散能垒计算在电池材料领域的革新应用 随着能源需求的增长和环境意识的提高&#xff0c;电池技术成为解决可再生能源存储和移动电子设备需求的关键。电池材料的研究和开发变得日益重要&#xff0c;而扩散能垒计算作为一种先进的计算方法&#xff0c;为电池材料领域带来了革新的…

设计模式之【观察者模式】,MQ的单机实现雏形

文章目录 一、什么是观察者模式1、观察者模式应用场景2、观察者模式的四大角色3、观察者模式优缺点 二、实例1、观察者模式的一般写法2、微信公众号案例3、鼠标响应事件API案例 三、实现一个异步非阻塞框架1、EventBus2、使用MQ 四、源码中的观察者模式1、Observable/Observer2…

pygam第3课——画图小程序

前言&#xff1a;我们前两节课已经学习了&#xff0c;界面的设计、图片的加载、那么今天我们将继续学习pygame的基础知识&#xff0c;我们的今天学习的内容是&#xff1a;鼠标滑动时坐标的实时获取、鼠标的移动事件、鼠标的点击事件、图形绘制等。希望大家能 搭建界面&#xf…

firewalld防火墙(又到了可以看日落和晚霞的日子了)

文章目录 一、firewalld概述二、firewalld和iptables的关系三、firewalld区域的概念四、firewalld数据处理流程五、firewalld检查数据包源地址的规则六、firewalld防火墙的配置种类1.运行时配置2.永久配置 七、firewalld防火墙的配置方法八、使用命令配置firewalld防火墙1.获取…

Ventoy 多合一启动盘制作工具神器 - 将多个系统 Win/PE/Linux 镜像装在1个U盘里

最近很多操作系统都纷纷发布了新版本&#xff0c;比如 Windows 11、Ubuntu、Deepin、优麒麟、CentOS、Debian 等等&#xff0c;对喜欢玩系统的人来说绝对是盛宴。 不过一般用 Rufus 等工具&#xff0c;一个 U 盘往往只能制作成一个系统的启动盘/安装盘&#xff0c;想要增加另一…

零入门kubernetes网络实战-33->基于nat+brigde+veth pair形成的跨主机的内网通信方案

《零入门kubernetes网络实战》视频专栏地址 https://www.ixigua.com/7193641905282875942 本篇文章视频地址(稍后上传) 本文主要使用的技术是 nat技术Linux虚拟网桥虚拟网络设备veth pair来实现跨主机网桥的通信 1、测试环境介绍 两台centos虚拟机 # 查看操作系统版本 cat …

VIBRO-METER VM600 IRC4 智能继电器卡

额外的继电器&#xff0c;由来自MPC4和/或AMC8卡的多达86个输入的方程驱动&#xff0c;用于需要2oo3表决等更复杂的逻辑时8个继电器&#xff0c;可配置为8个SPDT或4个DPDT使用IRC4配置器软件进行完全软件配置继电器可配置为正常通电(NE)或正常断电(NDE)&#xff0c;具有可配置的…