从零构建深度学习推理框架-9 再探Tensor类,算子输入输出的分配

news2024/9/22 7:02:31

再探Tensor类:

第二节中我们编写的Tensor类其实并不能满足我们的使用需要,我们将在这一节以代码阅读的方式来看看一个完全版本的Tensor应该具备怎样的要素,同时我们对Tensor类的分析来看看在C++中一个设计好的类应该是怎么样的。

Tensor<float>::Tensor(uint32_t channels, uint32_t rows, uint32_t cols) {
  data_ = arma::fcube(rows, cols, channels);
  if (channels == 1 && rows == 1) {
    this->raw_shapes_ = std::vector<uint32_t>{cols};
  } else if (channels == 1) {
    this->raw_shapes_ = std::vector<uint32_t>{rows, cols};
  } else {
    this->raw_shapes_ = std::vector<uint32_t>{channels, rows, cols};
  }
}

在这里,raw_shape记录的是另外一个方面的形状信息,主要用于reviewflatten层中。

举一个简单的例子,当Tensor将一个大小为(2,16,1)的Tensor reshape到(32,1,1)的大小时,raw_shapes变量会被记录成(32). 将一个大小为(2,16, 2)的Tensor reshape到(2, 64)的大小时,raw_shapes会被记录成(2,64).

那这样做的目的是什么呢?原来的Tensor不能在逻辑上区分当前的张量是三维的、二维的还是一维的,因为实际的数据存储类arma::fcube总是一个三维数据。所以我们要区分他的逻辑结构,就需要这么一个raw_shape

列优先的Reshape

void Tensor<float>::ReRawshape(const std::vector<uint32_t>& shapes) {
  CHECK(!this->data_.empty());
  CHECK(!shapes.empty());
  const uint32_t origin_size = this->size();
  uint32_t current_size = 1;
  for (uint32_t s : shapes) {
    current_size *= s;
  }
  CHECK(shapes.size() <= 3);
  CHECK(current_size == origin_size);

  if (shapes.size() == 3) {
    this->data_.reshape(shapes.at(1), shapes.at(2), shapes.at(0));
    this->raw_shapes_ = {shapes.at(0), shapes.at(1), shapes.at(2)};
  } else if (shapes.size() == 2) {
    this->data_.reshape(shapes.at(0), shapes.at(1), 1);
    this->raw_shapes_ = {shapes.at(0), shapes.at(1)};
  } else {
    this->data_.reshape(shapes.at(0), 1, 1);
    this->raw_shapes_ = {shapes.at(0)};
  }
}

我们再来分析一下这个函数,如果传入的shapes是1维的,就相当于将数据展开为(elem_size,1,1),并将逻辑维度赋值为1. 如果传入的shapes,相当于将数据展开为(shapes.at(0), shapes.at(1), 1). 我们来看看下面的这个图例:

如果把上面的(2,2,3)展平为一维的,那就应该是如下图所示:

 而且这也是arma:cube的默认排序(列排序)

行优先的Reshape

那如果我们在某些情况下需要行优先的Reshape呢?

void Tensor<float>::ReView(const std::vector<uint32_t>& shapes) {
  CHECK(!this->data_.empty());
  const uint32_t target_channels = shapes.at(0);
  const uint32_t target_rows = shapes.at(1);
  const uint32_t target_cols = shapes.at(2);
  arma::fcube new_data(target_rows, target_cols, target_channels);

  const uint32_t plane_size = target_rows * target_cols;
  for (uint32_t c = 0; c < this->data_.n_slices; ++c) {
    const arma::fmat& channel = this->data_.slice(c);
    for (uint32_t c_ = 0; c_ < this->data_.n_cols; ++c_) {
      const float* colptr = channel.colptr(c_);
      for (uint32_t r = 0; r < this->data_.n_rows; ++r) {
        const uint32_t pos_index =
            c * data_.n_rows * data_.n_cols + r * data_.n_cols + c_;
        const uint32_t ch = pos_index / plane_size;
        const uint32_t row = (pos_index - ch * plane_size) / target_cols;
        const uint32_t col = (pos_index - ch * plane_size - row * target_cols);
        new_data.at(row, col, ch) = *(colptr + r);
      }
    }
  }
  this->data_ = new_data;
}

我们只能通过位置计算的方式来对逐个元素进行搬运,const uint32_t plane_size = target_rows * target_cols;来计算行数和列数相乘的积。

const uint32_t pos_index = c * data_.n_rows * data_.n_cols + r * data_.n_cols + c_; 得 到调整前的元素下标,随后我们计算调整后的通道下标位置:ch = pos_index / plane_size,plane_size就是和一面,一行乘一列。同理计算row,col等调整位置后的行、列坐标。


计算图关系

内容回顾

我们在回顾一下之前的内容,我们根据pnnx计算图得到了我们的计算图,我们的计算图由两部分组成,分别是kuiper_infer::RuntimeOperatorkuier_infer::RuntimeOperand.

但是作为一个计算图,计算节点之间往往是有连接的,包括从input operator到第一个计算节点再到第二个计算节点,直到最后的输出节点output operator,我们再来回顾一下这两个数据结构的具体定义:

struct RuntimeOperator {
  int32_t meet_num = 0; /// 计算节点被相连接节点访问到的次数
  ~RuntimeOperator() {
    for (auto &param : this->params) {
      if (param.second != nullptr) {
        delete param.second;
        param.second = nullptr;
      }
    }
  }
  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; /// 算子的属性信息,内含权重信息
};
  1. std::map<:string std::shared_ptr>> output_operators; 我们重点来看这个定义,它是当前这个计算节点的下一个计算节点,当数据在当前RuntimeOperator上计算完成之后,系统会读取output_operators中准备就绪的算子并开始执行。
  2. std::map<:string std::shared_ptr>> input_operands; 是当前计算节点所需要的输入,它往往来自于上一个RuntimeOperator的输入。
  3. std::shared_ptr output_operands; 是当前节点计算得到的输出,它是通过当前的op计算得到的。

具体的流程是这样的,假设我们在系统中有三个RuntimeOperators,分别为op1,op2op3. 这三个算子的顺序是依次执行的,分别是op1-->op2-->op3.

  • 当我们执行第一个算子op1的时候,需要将来自于图像的输入填充到op1->input_operands中。
  • 第一个算子op1开始执行,执行的过程中读取op1->input_operands并计算得到相关的输出,放入到op1->output_operands
  • op1output_operators中读取到ready的op2
  • 第二个算子op2开始执行,执行的过程读取op1->output_operands并拷贝op2->input_operands中,随后op2算子开始执行并计算得到相关的输出,放入到op2->output_operands中。

所以我们可以看到者之间是有一个图关系的,那我们来看一下他是怎么构建这样一个图关系的

怎样构建图关系:



/ 构建图关系
  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});
      }
    }
  }
  ```
  -  **const std::vector\<std::string> &output_names = current_op->output_names;** 存放的是当前`op`的`output_names`,`output_names`也就是当前算子的后一层算子的名字。对于`op1`,它的`output_names`就是`op2`的name.
  - **const auto &next_op : this->operators_**  我们遍历整个图中的`RuntimeOperators`,如果遇到`next_op`的name和当前`current_op->output_name`是一致的,那么我们就可以认为`next_op`是当前`op`的下一个节点之一。
  - **current_op->output_operators.insert({next_op->name, next_op});** 将`next_op`插入到`current_op`的下一个节点当中。
  - 这样一来,当`current_op`执行完成之后就取出`next_op`,并将当前`current_op`的输出`output_opends`(输出)拷贝到`next_op`的`input_operands`(输入)中。

因为在初始化的时候就已经约定好了op1的输出是op2,所以只要在接下来的点中不停地寻找op2就好了,找到了之后就把它insert到output_operators里面.这个output_operators是一个map,就可以让输出节点的名字和节点对应。

这么一个计算图也得有输入输出节点吧

作者:傅大狗
链接:https://zhuanlan.zhihu.com/p/604613883
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

  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") {
      if (kOperator->name == output_name) {
        this->output_operators_maps_.insert({kOperator->name, kOperator});
      } else {
        LOG(FATAL) << "The graph has two output operator!";
      }
    } else {
      std::shared_ptr<Layer> layer = RuntimeGraph::CreateLayer(kOperator);
      CHECK(layer != nullptr) << "Layer create failed!";
      if (layer) {
        kOperator->layer = layer;
      }
    }
  }
  • kOperator->type == "pnnx.Output" 找到this->operators中的输出节点,但是目前Kuiperinfer只支持一个输出节点,其实也可以多输出,作为一个教学框架我实在不想支持这种corner case
  • 同理: kOperator->type == "pnnx.Input" 来找到图中,也就是op list中的输入节点

就是在op3结束之后,我们还要把op3的output_operand复制到输出节点的input_operand里面
 

初始化输入

struct RuntimeOperand {
  std::string name;                                     /// 操作数的名称
  std::vector<int32_t> shapes;                          /// 操作数的形状
  std::vector<std::shared_ptr<Tensor<float>>> datas;    /// 存储操作数 为什么是vector,因为是一个batch,如果batch是2的话,那就存储的是两个
  RuntimeDataType type = RuntimeDataType::kTypeUnknown; /// 操作数的类型,一般是float
};

可以看到这里的RuntimeOperand::datas就是存储具体数据的地方,我们初始化输入输出的空间也就是要在推理之前先根据shapes来初始化好这里datas的空间

代码位于runtime_ir.cppInitOperatorInputTensorRuntimeGraphShape::InitOperatorInputTensor(operators_) 这个函数的输入是operator list, 所以将在这个函数中对所有的op进行输入和输出空间的初始化。

  • 得到一个op的输入空间input_operands
const std::map<std::string, std::shared_ptr<RuntimeOperand>> &
            input_operands_map = op->input_operands;
  • 如果初始的是空就continue
for (const auto &op : operators) {
    if (op->input_operands.empty()) {
      continue;
    } 

  • 得到input_operands中记录的数据应有大小input_operand_shape和存储数据的变量input_datas
auto &input_datas = input_operand->datas;

CHECK(!input_operand_shape.empty());
const int32_t batch = input_operand_shape.at(0);
CHECK(batch >= 0) << "Dynamic batch size is not supported!";
CHECK(input_operand_shape.size() == 2 ||
      input_operand_shape.size() == 4 ||
      input_operand_shape.size() == 3)
  • 我们需要根据input_operand_shape中记录的大小去初始化input_datas. 而input_operand_shape可能是三维的,二维的以及一维的,如下方所示
  • input_operand_shape : (batch, elemsize) 一维的
  • input_operand_shape : (batch, rows,cols) 二维的
  • input_operand_shape : (batch, rows,cols, channels) 三维的

如果当前input_operand_shape是二维的数据,也就是说输入维度是(batch,rows,cols)的. 我们首先对batch进行遍历,对一个batch的中的数据input_datas= op->input_operand(输入)进行初始化。

input_datas.resize(batch);
for (int32_t i = 0; i < batch; ++i) {
}

for循环内,它会调用如下的方法去初始化一个二维的张量:

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) {
            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));

也就是如果是shape == 4 , 那就是三维的,那么1就是channel,2就是row,3就是col

那么如果输入的channel == 1,或者row == 1

Tensor<float>::Tensor(uint32_t channels, uint32_t rows, uint32_t cols) {
  data_ = arma::fcube(rows, cols, channels);
  if (channels == 1 && rows == 1) {
    this->raw_shapes_ = std::vector<uint32_t>{cols};
  } else if (channels == 1) {
    this->raw_shapes_ = std::vector<uint32_t>{rows, cols};
  } else {
    this->raw_shapes_ = std::vector<uint32_t>{channels, rows, cols};
  }
}

那就正好被初始化为了我们之前的raw_shape 这样的一个逻辑维度

这就和我们上面的课程内容对应上了,Tensor<float>原本是一个三维数据,我们怎么在逻辑上给他表现成一个二维的张量呢?这就要用到我们上面说到的raw_shapes了。

  • 调用并初始化一维的数据也同理, 在初始化的过程中会调用(channels==1&&rows==1) 这个条件判断,并将raw_shapes这个维度定义成一维。
input_datas.at(i) = std::make_shared<Tensor<float>>(1, input_operand_shape.at(1), 1)

避免第二次初始化

那么在计算的过程中,我们只需要一次初始化就可以。

所以在第二次遇到她的时候,只需要去检查空间是否发生改变就可以啦

  if (!input_datas.empty()) {
          CHECK(input_datas.size() == batch) << "Batch size is wrong!";
          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));
            }
          }
        } 
              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));

上面这一部分,左边是我们实际有的shape,也就是我们第一次初始化的shape,而右边的是我们再次遇到的时候应该具有的shape,所以这一次就是check这两个shape是否一致,如果check不通过,那就代表输入空间的大小被改变了。那这样的话就会报错,退出这个程序

示例:

 

 

 

 这里的conv的输出分别是1和2

expression就接受了1和2为输入

最近在忙老师布置的任务,就耽误了这方面的进度,慢慢补把

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

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

相关文章

K8s学习笔记3

Kubernetes功能&#xff1a; Kubernetes是一个轻便的可扩展的开源平台&#xff0c;用于管理容器化应用和服务。通过Kubernetes能够进行应用的自动化部署和扩缩容。在Kubernetes中&#xff0c;会将组成应用的容器组合成一个逻辑单元以更易管理和发现。Kubernetes积累了作为Goog…

打怪升级之从零开始的网络协议

序言 三个多月过去了&#xff0c;我又来写博客了&#xff0c;这一次从零开始学习网络协议。 总的来说&#xff0c;计算机网络很像现实生活中的快递网络&#xff0c;其最核心的目标&#xff0c;就是把一个包裹&#xff08;信息&#xff09;从A点发送到B点去。下面是一些共同的…

面试-快速学习计算机网络-UDP/TCP

1. OSI四层和七层映射 区别&#xff1a; 应用层&#xff0c;表示层&#xff0c;会话层合并为了应用层数据链路层和物理层合并为了网络接口层 2. TCP和UDP的区别&#xff1f; 总结&#xff1a; 1 . TCP 向上层提供面向连接的可靠服务 &#xff0c;UDP 向上层提供无连接不可靠服…

验证二叉搜索树

给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。 示例 1&#xff1a; 输…

8.21summary

计划部分完成&#xff0c;复习暂未完成

Linux环境下交叉编译环境安装、编译和运行

Win11主机上安装虚拟机&#xff0c;虚拟机内运行Ubuntu20.04&#xff0c;为了能够在本地电脑&#xff08;Win11&#xff09;上开发测试软件和算法&#xff0c;最终将编译好的可执行文件拷贝到Linux板上&#xff08;Rk3288上运行linux系统&#xff09;运行&#xff0c;因此需要安…

【QML】鼠标放在控件上颜色改变的效果实现

最近刚好要用到一个功能&#xff0c;在qml上实现鼠标放上去&#xff0c;控件的颜色改变&#xff0c;鼠标移走&#xff0c;控件颜色恢复。第一反应是这个功能非常简单&#xff0c;但是搞了一会儿都没实现&#xff0c;最后发现MouseArea其实提供了一个很简便的方法来提供使用&…

马蹄集 第九次oj赛

目录 MT2188单条件和 8421码 余3码 永恒之2 新十六进制 人脑计算机 二进制&#xff1f;不同&#xff01; 三进制计算机1 三进制计算机2 excel的烦恼 MT2188单条件和 号难度&#xff1a;黄金© 时间限制&#xff1a;1秒 巴占用内存&#xff1a;128M ★收藏△报错 “单…

MemSeg:一种差异和共性来检测图像表面缺陷的半监督方法

目录 1、摘要 2、Method 2.1 模拟异常样本 2.2 Memory Module 2.3 空间注意模块 2.4 多尺度特征融合模块 2.5 损失函数设置 2.6 Decoder模块 1、摘要 本文认为人为创建类内差异和保持类内共性可以帮助模型实现更好的缺陷检测能力&#xff0c;从而更好地区分非正常图像。如…

蓝蓝设计UI设计公司-界面设计与开发案例

天津航天中为项目 中国南方电网十二个软件交互优化和界面设计 图标设计 | 交互设计 | 界面设计 天津航天中为数据系统科技有限公司是航天503所控股的专业化公司&#xff0c;坐落于天津滨海新区航天技术产业园&#xff0c;是航天五院家入住天津未来科技城的军民融合型企业&…

el-tree组件图标的自定义

饿了么树形组件的图标自定义 默认样式: 可以看到el-tree组件左侧自带展开与收起图标,咱们可以把它隐藏:: .groupList {::v-deep .el-tree-node { .el-icon-caret-right {display: none;} } } 我的全部代码 <div class"groupList"><el…

韩顺平java集合

遍历集合方式: public static void main(String[] args) {List<Object> arrayList new ArrayList<>();arrayList.add(1);arrayList.add(3);arrayList.add(111);Iterator<Object> iterator arrayList.iterator();while (iterator.hasNext()){System.out.pri…

SpringBoot整合MongoDB(从安装到使用系统化展示)

SpringBoot整合MongoDB&#xff08;从安装到使用系统化展示&#xff09; MongoDB介绍 基础介绍 MongoDB是一种开源、面向文档的非关系型数据库管理系统&#xff08;NoSQL DBMS&#xff09;&#xff0c;它以其灵活性、可扩展性和强大的查询能力而闻名。MongoDB的设计理念是为了…

Linux journalctl命令详解(journalctl指令)

文章目录 Linux Journalctl命令详解1. Journalctl简介2. Journalctl基础使用3. 过滤日志条目4. 时间戳和日志轮转5. 高级应用6. journalctl --help指令文档英文中文 注意事项journal日志不会将程序输出的空行显示&#xff0c;日志会被压缩得满满当当。journal日志不会自动持久化…

Claude 2 国内镜像站

Claudeai是什么&#xff1f; Claude 2被称为ChatGPT最强劲的竞争对手&#xff0c;支持100K上下文对话&#xff0c;并且可以同时和5个文档进行对话&#xff0c;不过国内目前无法正常实用的&#xff0c;而claudeai是一个Claude 2 国内镜像站&#xff0c;并且免翻可用&#xff0…

当图像宽高为奇数时,如何计算 I420 格式的uv分量大小

背景 I420 中 yuv 数据存放在3个 planes 中。 网上一般说 I420 数据大小为 widthheight1.5 但是当 width 和 height 是奇数时&#xff0c;这个计算公式会有问题。 I420 中 u 和 v 的宽高分别为 y 的一半。 但是当不能整除时&#xff0c;是如何取整呢&#xff1f;向上还是向下&…

【C++】iota函数 + sort函数实现基于一个数组的多数组对应下标绑定排序

目录 一、iota函数 1. 函数解析 ​① 迭代器类型(补充) ② 头文件 ③ 参数 2. 函数用途与实例 二、sort函数 1、 函数解读 2、实现倒序排列 2.1 greater 与 less 模板参数 2.2 lambda表达式 三、下标绑定排序&#xff08;zip&#xff09; --- 833.字符串中的查找与替换 一、…

innodb事务实现

事务的特性 ACID 事务的类别 事务实现 redo redoLog buffer 的格式 undo 更新主键 purge group commit 因为上层的binlog和底层的redolog要保持一致&#xff0c;所以 事务控制语句 事务隔离级别 分布式事务 事务习惯

安装Ubuntu服务器、配置网络、并安装ssh进行连接

安装Ubuntu服务器、配置网络、并安装ssh进行连接 1、配置启动U盘2、配置网络3、安装ssh4、修改ssh配置文件5、重启电脑6、在远程使用ssh连接7、其他报错情况 1、配置启动U盘 详见: U盘安装Ubuntu系统详细教程 2、配置网络 详见&#xff1a;https://blog.csdn.net/davidhzq/a…

运放和三极管构成的恒流源电路

这是一个由运放和三极管构成的恒流源电路&#xff0c;RL为负载电阻&#xff0c;R1为采样电阻。 流过三极管集电极的电流 下面分析下这个电路的工作原理。首先我们可以看到这个运放引入了负反馈&#xff0c;所以它工作在线性区的&#xff0c;就有VINVPVN。 所以流过采样电阻R1的…