从零构建深度学习推理框架-10 算子的执行流程

计算图的设计

Graph的结构

  1. Operators: 记录所有的节点
  2. Input operator: 指定的输入节点
  3. Output operator: 指定的输出节点
  4. Global input data: 模型的外部全局输入(用户指定的输入)

Operator的结构

  1. Input data: 节点的输入数据
  2. Output data: 节点的输出数据
  3. Operator params: 计算节点的参数
  4. Next operators: 该节点的下一个节点,数量有且大于一个
  5. Layer:

                  每个Operator具体计算的执行者,layer先从input data中取得本层的输入,再通过layer定义的计算过程,并得到output data

                   计算的过程中所需要的参数已经被提前存放到Operator params

Graph中的数据流动

我们从下图中可以看出,一个Graph中包含了两个要素,一个要素是多个operators,另一个要素是连通operators之间的数据通路。

也就是说,前一个operator的输出将作为后一个operator的输入存在,其中在输入和输出中传递的数据,是以前面课程中谈到的Tensor类进行的。

 其中,在普通的计算中,上面的op1的output_data是拷贝到op2的input_data中的,而在我们的这个推理网络中,我们是进行了一个内存的复用的。

我们可以看到,在图中,Graph在执行时在逻辑上可以分为两条路径,一条是控制流,另外一条是数据流。在数据流中,前一个operator产生的输出传递到后续operator作为输入。

那么Graph是如何得知一个operator的后续operator的?我们可以看到在前方Operator定义中,有一个变量为Next operators,这个变量记录了一个operator的后继节点。在上图中,我们可以看到op1有两个后继节点op2op3,他们也是通过op1.next_oprators得到的。

所以在图的执行中,有两个很重要的部分:

  1. 通过op.layer根据输入来进行计算,并得到当前层的输出
  2. 将当前层的输出顺利并且正确地传递到后继节点的输入当中。传递的路径是previous op.output to next op.input 这部分看起来是赋值,但在这个项目中已经变成了指针的拷贝会快很多。

计算图的执行顺序:

计算节点的执行是通过广度优先搜索来实现的,当然也有人说这就是一种拓扑排序的实现。

那什么是广度优先呢?

从图中我们可以看出,现在要执行的图是总共拥有7个op, 分别从op1到op7.

它们之间的前后关系如图中的箭头指向,例如op2, op3, op4均为op1的后继节点,换句话说,只有等到op1执行结束之后,op2, op3, op4才能开始执行,这三个节点的输入也都来自于op3的输出,以下的顺序是上面这个图中的执行顺序。

  1. graph.input_operator的定义可以知道,op1是开始执行的节点,因此在当前时刻将op1放入到执行队列中
  2. op1被从执行队列中取出执行,并得到op1的计算输出,存放到op1.output_data中;同时,根据op1.output_operators定位到op1的后续三个节点,op2, op3op4, 随后将op1.output_data拷贝到这三个后继节点的输入中
  3. 现在的执行队列存放了三个节点,分别为op2, op3op4. 随后我们根据先进先出的顺序取出op2开始执行,因为op2没有后继节点,所以执行完毕后直接开始下一轮迭代
  4. 取出队列中的队头op3,在op3执行完毕之后将op3.output_data拷贝到op5.input_data中,并将op5入执行队列

......

随后的执行顺序如图所示,总之也是在一个节点执行完毕之后,通过current_op.output_operators来寻找它的后继节点,并将当前节点的输出拷贝到后继节点的输入中

项目中计算图调度执行实现

项目中的计算图调度执行是对上方图例的一个还原,我们在这一节中通过分析代码的方式来看看怎么来做一个广度优先搜索(拓扑排序)。

寻找并拷贝上一级的输出到后继节点

void RuntimeGraph::ProbeNextLayer(const std::shared_ptr<RuntimeOperator> &current_op,std::deque<std::shared_ptr<RuntimeOperator>> &operator_queue,std::vector<std::shared_ptr<Tensor<float>>> layer_output_datas) {const auto &next_ops = current_op->output_operators;std::vector<std::vector<std::shared_ptr<ftensor>>> next_input_datas_arr;for (const auto &next_op : next_ops) {const auto &next_rt_operator = next_op.second;const auto &next_input_operands = next_rt_operator->input_operands;// 找到后继节点if (next_input_operands.find(current_op->name) != next_input_operands.end()) {std::vector<std::shared_ptr<ftensor>> next_input_datas =next_input_operands.at(current_op->name)->datas;next_input_datas_arr.push_back(next_input_datas);next_rt_operator->meet_num += 1;
//检查 next_rt_operator 是否需要当前操作符的输出数据作为输入(通过检查 next_input_operands 中是否包//含当前操作符的名字)。
//如果需要当前操作符的输出作为输入,那么就获取相应的输入数据(next_input_datas)。
//将 next_input_datas 存入 next_input_datas_arr,这是一个二维向量,用于存储所有下一层操作符的输入数//据。
//增加 next_rt_operator 的 meet_num,可能是用来追踪该操作符已满足的条件数目。if (std::find(operator_queue.begin(), operator_queue.end(),next_rt_operator) == operator_queue.end()) {if (CheckOperatorReady(next_rt_operator)) {operator_queue.push_back(next_rt_operator);
//代码检查 next_rt_operator 是否已经存在于 operator_queue 中:
//如果不存在于队列中,并且满足一定的就绪条件(通过 CheckOperatorReady 函数判断),则将 //next_rt_operator 添加到 operator_queue 中,以便后续处理。
//最后,调用 SetOpInputData 函数,将之前收集到的下一层操作符的输入数据与当前层的输出数据关联起来。//如果ready了,那就把后继节点放入到队列之中}}}}SetOpInputData(layer_output_datas, next_input_datas_arr);
}
void RuntimeGraph::ProbeNextLayer(const std::shared_ptr<RuntimeOperator> &current_op,std::deque<std::shared_ptr<RuntimeOperator>> &operator_queue,std::vector<std::shared_ptr<Tensor<float>>> layer_output_datas)

可以看到该函数有三个参数,分别为current_op,operator_queuelayer_output_datas,这三个参数的定义如下:

current_op表示当前执行完毕的节点,operator_queue就是在上一节中提到的节点执行队列,layer_output_datas就是当前current_op被执行后得到的对应输出。

const auto &next_ops = current_op->output_operators;std::vector<std::vector<std::shared_ptr<ftensor>>> next_input_datas_arr;

得到当前节点current_op的后继节点, next_ops

std::vector<std::vector<std::shared_ptr<ftensor>>> next_input_datas_arr;for (const auto &next_op : next_ops) {const auto &next_rt_operator = next_op.second;// layer_output_datas 需要拷贝到next_input_operands的datas中const auto &next_input_operands = next_rt_operator->input_operands;

这里对next_ops进行遍历,依次获得后继节点中的其中一个next_op,随后我们得到next_op的输入数据引用。

我们要得到next_op.input_operands呢?我们就是要把current_op.output_data拷贝到其中,完成current_op输出到后继节点输入的拷贝。

next_rt_operator->meet_num += 1; // 0 --> 1
if (std::find(operator_queue.begin(), operator_queue.end(),next_rt_operator) == operator_queue.end()) {if (CheckOperatorReady(next_rt_operator)) {// 把后继节点放入到执行队列operator_queue.push_back(next_rt_operator);}
}

可以看到其中的meet_num,对于一个节点next_operator来说,如果meet_num的数量等于它前驱的数量,说明它现在可以被放入到执行队列中。

bool RuntimeGraph::CheckOperatorReady(const std::shared_ptr<RuntimeOperator> &op) {CHECK(op != nullptr);CHECK(op->meet_num <= op->input_operands.size());if (op->meet_num == op->input_operands.size()) {return true;} else {return false;}
}

判断,如果meet_num == 输入节点数的话,那就代表之前节点的输出已经全部结束了,现在可以将他们放入到下一节点的输入里面了。


void RuntimeGraph::SetOpInputData(std::vector<std::shared_ptr<Tensor<float>>> &src,std::vector<std::vector<std::shared_ptr<Tensor<float>>>> &dest) {CHECK(!src.empty() && !dest.empty()) << "Src or dest array is empty!";for (uint32_t j = 0; j < src.size(); ++j) {const auto &src_data = src.at(j)->data();for (uint32_t i = 0; i < dest.size(); ++i) {//      CHECK(!dest.empty() && dest.at(i).size() == src.size());dest.at(i).at(j)->set_data(src_data);}}
}// 这是一个名为 SetOpInputData 的函数,可能是在运行时图中进行数据关联操作的一部分。// 函数的参数包括://   src:一个存储浮点类型张量(Tensor)共享指针的向量,表示要用于设置输入数据的源数据。// dest:一个二维向量,其中每行表示一个操作符的输入数据,每列表示不同的源数据。// 函数开始时,会使用断言(CHECK)来确保源数据 src 和目标数据 dest 都不为空,否则会产生错误信息。//  然后,通过两个嵌套的循环遍历源数据 src 和目标数据 dest://    外部循环遍历源数据 src 中的每个元素。//  内部循环遍历目标数据 dest 中的每一行(操作符的输入数据)。//  在内部循环中,获取源数据 src 的具体数据(src_data)。// 接着,将源数据 src_data 设置到目标数据中,这个过程通过 dest.at(i).at(j)->set_data(src_data) 来实现。这里 i 表示操作符的索引,j 表示源数据的索引。

总体是将layer_output_datas这个输出张量复制到next_input_datas_arr这个张量数组(后继的输入)上,指针复制几乎无消耗。

广度优先搜索的执行顺序的实现

就是在咱们的Forward函数中:

我们首先来看它的两个参数,inputs为模型的输入张量,debug表示是否开启打印调试功能。

std::vector<std::shared_ptr<Tensor<float>>> RuntimeGraph::Forward(const std::vector<std::shared_ptr<Tensor<float>>> &inputs, bool debug)

这里是Forward方法中对图状态的检查,只有图状态为complete的时候才能执行图的调度,图的complete时间发生在:

  1. 图中的计算节点都初始化完毕
  2. 输入输入输出算子都准备好相关的空间之后

input_op为整张图的开始执行节点,也就是模型的执行入口。

if (graph_state_ < GraphState::Complete) {LOG(FATAL) << "Graph need be build!";}CHECK(graph_state_ == GraphState::Complete)<< "Graph status error, current state is " << int(graph_state_);std::shared_ptr<RuntimeOperator> input_op;if (input_operators_maps_.find(input_name_) == input_operators_maps_.end()) {LOG(FATAL) << "Can not find the input node: " << input_name_;} else {input_op = input_operators_maps_.at(input_name_);}

将输入节点送入到执行队列中, 执行队列在这里的变量为operator_queue,是一个deque结构,方便从尾部插入,并从头部取出(完成先进先出)。

std::deque<std::shared_ptr<RuntimeOperator>> operator_queue;
operator_queue.push_back(input_op);std::map<std::string, double> run_duration_infos;
while (!operator_queue.empty()) {std::shared_ptr<RuntimeOperator> current_op = operator_queue.front();operator_queue.pop_front();if (!current_op || current_op == output_op) {if (debug) {LOG(INFO) << "Model Inference End";}break;}  ......
}

std::shared_ptr\<RuntimeOperator> current_op = operator_queue.front(); 从队列中获取一个被执行的节点,按照先进先出的顺序执行。

if (current_op == input_op) {ProbeNextLayer(current_op, operator_queue, inputs);
}

这里分为两种情况,如果 当前节点是输入节点,就直接使用ProbeNextLayer将输入拷贝到输入节点的下一层中(因为input节点不涉及到别的操作,所以可以直接赋值)。

std::string current_op_name = current_op->name;
if (!CheckOperatorReady(current_op)) {if (operator_queue.empty()) {// 当current op是最后一个节点的时候,说明它已经不能被ready 就是说既没有ready,又是最后一个节点,所以没有其他的节点,不能被meet_num+1了。LOG(FATAL) << "Current operator is not ready!";break;} else {// 如果不是最后一个节点,它还有被ready的可能性,只是可能由于什么原因放错了位置,那就放回到里面等待再meet_num+1再执行operator_queue.push_back(current_op);}
}

如果当前的节点(current_op)不是输入节点(input_operator)就对它是否准备好进行检查,检查的方式同样是使用CheckOperatorReady检查当前节点的入度,如果入度等于0,那么当前的节点就允许被执行。

如果这个节点还没有ready,就需要重新被放入到operator_queue当中。

const std::vector<std::shared_ptr<RuntimeOperand>> &input_operand_datas = current_op->input_operands_seq;
std::vector<std::shared_ptr<Tensor<float>>> layer_input_datas;
for (const auto &input_operand_data : input_operand_datas) {for (const auto &input_data : input_operand_data->datas) {layer_input_datas.push_back(input_data);}
}

将当前op中的input移动到layer_input_datas(全指针拷贝,损耗可以忽略不计),也就是从op->input_operands_seq中到layer_input_datas中。

InferStatus status = current_op->layer->Forward(layer_input_datas, current_op->output_operands->datas);

在op自身ready,且输入已经准备到layer_input_data之后,开始执行算子,但是这节课中算子执行不讨论。

ProbeNextLayer(current_op, operator_queue, current_op->output_operands->datas);

在执行完毕后,对当前的算子current_op的输出同步它下一级后继节点的输入中。

while (!operator_queue.empty())

当执行队列中的节点执行均执行完毕,且图中没有未执行的节点时就跳出循环。

CHECK(output_op->input_operands.size() == 1)<< "The graph only support one path to the output node yet!";const auto &output_op_input_operand = output_op->input_operands.begin();const auto &output_operand = output_op_input_operand->second;return output_operand->datas;

output operatorinput operand输出为最后的结果,换句话理解,输出节点的输入张量就是最后得到的结果。

最后可以看到对于resnet18的输出网络,实现执行分支再执行最下面的

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

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

相关文章

系统架构技能之设计模式-单件模式

一、开篇 其实我本来不是打算把系统架构中的一些设计模式单独抽出来讲解的&#xff0c;因为很多的好朋友也比较关注这方面的内容&#xff0c;所以我想通过我理解及平时项目中应用到的一 些常见的设计模式,拿出来给大家做个简单讲解&#xff0c;我这里只是抛砖引玉&#xff0c…

ArmSoM-W3 DDR压力测试

1. 简介 专栏总目录 ArmSoM团队在产品量产之前都会对产品做几次专业化的功能测试以及性能压力测试&#xff0c;以此来保证产品的质量以及稳定性 优秀的产品都要进行多次全方位的功能测试以及性能压力测试才能够经得起市场的检验 2. 环境介绍 硬件环境&#xff1a; ArmSoM-W…

任意文件读取

文章目录 渗透测试漏洞原理任意文件读取1. 任意文件读取概述1.1 漏洞成因1.2 漏洞危害1.3 漏洞分类1.4 任意文件读取1.4.1 文件读取1.4.2 任意文件读取1.4.3 权限问题 1.5 任意文件下载1.5.1 一般情况1.5.2 PHP实现1.5.3 任意文件下载 2. 任意文件读取攻防2.1 路径过滤2.1.1 过…

字符设备驱动(内核态用户态内存交互)

前言 内核驱动&#xff1a;运行在内核态的动态模块&#xff0c;遵循内核模块框架接口&#xff0c;更倾向于插件。 应用程序&#xff1a;运行在用户态的进程。 应用程序与内核驱动交互通过既定接口&#xff0c;内核态和用户态访问依然遵循内核既定接口。 环境搭建 系统&#…

Maven 基础之安装和命令行使用

Maven 的安装和命令行使用 1. 下载安装 下载解压 maven 压缩包&#xff08;http://maven.apache.org/&#xff09; 配置环境变量 前提&#xff1a;需要安装 java 。 在命令行执行如下命令&#xff1a; mvn --version如出现类似如下结果&#xff0c;则证明 maven 安装正确…

【100天精通python】Day49:python web编程_web框架,Flask的使用

目录 1 Web 框架 2 python 中常用的web框架 3 Flask 框架的使用 3.1 Flask框架安装 3.2 第一个Flask程序 3.3 路由 3.3.1 基本路由 3.3.2 动态路由 3.3.3 HTTP 方法 3.3.4 多个路由绑定到一个视图函数 3.3.5 访问URL 参数的路由 3.3.6 带默认值的动态路由 3.3.7 带…

文件读取漏洞复现(Metinfo 6.0.0)

安装环境 安装phpstudy&#xff0c;下载MetInfo 6.0.0版本软件&#xff0c;复制到phpstudy目录下的www目录中。 打开phpstudy&#xff0c;访问浏览器127.0.0.1/MetInfo6.0.0/install/index.php&#xff0c;打开Meinfo 6.0.0主页&#xff1a; 点击下一步、下一步&#xff0c…

深入理解css3背景图边框

border-image知识点 重点理解 border-image-slice 设置的值将边框背景图分为9份&#xff0c;图像中间的舍弃&#xff0c;其他部分图像对应边框的相应区域放置&#xff0c;上右下左四角固定&#xff0c;border-image-repeat设置的是除四角外其他部分的显示方式。 截图来自菜鸟教…

【锁】定时任务推送数据-redission加锁实例优化

文章目录 redission 加锁代码-有问题优化代码看门狗是什么&#xff1f; redission 加锁代码-有问题 /*** 收货入库物料标签(包装码)推送接口** throws Exception*/public void synReceiveMaterialTags() throws Exception {String tag DateFormatUtils.format(new Date(), &qu…

spring事务详解

spring事务整体流程&#xff08;图画的不是很细节&#xff0c;但是大体流程体现出来了&#xff09; 一、EnableTransactionManagement工作原理 开启Spring事务本质上就是增加了一个Advisor&#xff0c;但我们使用EnableTransactionManagement注解来开启Spring事务是&#xff…

设计模式-迭代器

文章目录 1. 引言1.1 概述1.2 设计模式1.3 迭代器模式的应用场景1.4 迭代器模式的作用 2. 基本概念2.1 迭代器 Iterator2.2 聚合 Aggregate2.3 具体聚合 ConcreteAggregate 3. Java 实现迭代器模式3.1 Java 集合框架3.2 Java 迭代器接口3.3 Java 迭代器模式实现示例 4. 迭代器模…

打破数据孤岛!时序数据库 TDengine 与创意物联感知平台完成兼容性互认

新型物联网实现良好建设的第一要务就是打破信息孤岛&#xff0c;将数据汇聚在平台统一处理&#xff0c;实现数据共享&#xff0c;放大物联终端的行业价值&#xff0c;实现系统开放性&#xff0c;以此营造丰富的行业应用环境。在此背景下&#xff0c;物联感知平台应运而生&#…

BOM对MES管理系统的影响与作用

在建设MES管理系统中&#xff0c;BOM&#xff08;物料清单&#xff09;具有至关重要的作用。它提供了产品的组成部分和结构信息&#xff0c;支持生产过程的监控、协调和管理。本文将详细探讨BOM在MES管理系统中的影响和作用。 一、生产过程指导 BOM为MES系统提供了产品的组成部…

centos中得一些命令 记录

redis命令 链接redis数据库的命令 redis-cli如果 Redis 服务器在不同的主机或端口上运行&#xff0c;你需要提供相应的主机和端口信息。例如&#xff1a; redis-cli -h <hostname> -p <port>连接成功后&#xff0c;你将看到一个类似于以下的提示符&#xff0c;表…

哪吒汽车“三头六臂”之「浩智电驱」

撰文 / 翟悦 编审 / 吴晰 8月21日&#xff0c;在哪吒汽车科技日上&#xff0c;哪吒汽车发布“浩智战略2025”以及浩智技术品牌2.0。根据公开信息&#xff0c;主编梳理了以下几点&#xff1a;◎浩智滑板底盘支持400V/800V双平台◎浩智电驱包括180kW 400V电驱系统和250kW 800…

Ansible学习笔记5

copy模块&#xff1a;&#xff08;重点&#xff09; copy模块用于对文件的远程拷贝&#xff08;如把本地的文件拷贝到远程主机上。&#xff09; 在master的主机上准备一个文件&#xff0c;拷贝文件到group1的所有主机上。 这个用的频率非常高&#xff0c;非常有用的一个模块…

简单聊聊Https的来龙去脉

简单聊聊Https的来龙去脉 Http 通信具有哪些风险Https Http SSL/TLS对称加密 和 非对称加密数字证书数字证书的申请数字证书怎么起作用 Https工作流程一定需要Https吗&#xff1f; Http 通信具有哪些风险 使用明文通信&#xff0c;通信内容可能会被监听不验证通信双方身份&a…

lnmp架构-mysql2

4.mysql 组复制集群 首先对所有的节点重新初始化 因为对节点的数据一致性要求非常高 主从复制的时候 slave只会复制master的binlog日志 就是二进制日志 不会复制relay_log 在server1上 根据实际情况修改主机名和网段 log_slave_updateON 意思就是 当slave的sql线程做完之后…

【docker】docker的一些常用命令-------从小白到大神之路之学习运维第92天

目录 一、安装docker-ce 1、从阿里云下载docker-cer.epo源 2、下载部分依赖 3、安装docker 二、启用docker 1、启动docker和不启动查看docker version 2、启动服务查看docker version 有什么区别&#xff1f;看到了吗&#xff1f; 3、看看docker启动后的镜像仓库都有什…

趣味微项目:玩转Python编程,轻松学习快乐成长!

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 在学习Python编程的旅程…