自制深度学习推理框架-第8.2节-ResNet中的Add层
自制深度学习推理框架-第8.2节-ResNet中的Add层
我们的视频课程
我们的上游项目
本课务必配合视频课程一起阅读 后续的课程根据上游项目的开发而制定课程表
本节课代码
git clone https://github.com/zjhellofss/KuiperCourse.git
git checkout eight
# 国内备用地址 https://gitee.com/fssssss/KuiperCourse.git
#https://github.com/zjhellofss/KuiperInfer 点个star
上节内容回顾
我们在上节课的内容中讲到了对计算图中表达式的解析, 表达式层在ResNet
中也有体现, ResNet
用Add
层做shortcut
. 对于Add
层,对应的表达式如下: add也是一个表达式
add(@0,@1)
上述表达式的表示将第一个输入数@0
和第二个输入数@1
. 表达式中的add
就是我们项目中的RuntimeOperator
,而两个输入数对应了项目中RuntimeOperand
.
Operand和Operator是什么
根据之前课程的讲述, RuntimeOperator
对应的是各个层的运算过程, 该结构中也保存了运算过程所需要的输入和输出操作数. RuntimeOperand
就是各个运算过程所需要的输入和输出操作数.
操作数的具体数据来源于训练得到的模型权重文件, 而运算过程来源于模型的定义文件, 他们在合适的时机被加载到推理系统中.
怎么得到KuiperInfer的Operand和Operator
回忆在上上节课中的内容, 我们曾经分享了从PNNX
模型定义文件和权重文件中加载PNNX::operator
和PNNX::operand
的过程.
我们根据上述以上的两个数据结构转换到对应的kuiperInfer::RuntimeOperator
和kuiperinfer::RuntimeOperand
, 其中包括了输入和输出数据的拷贝, 计算过程的参数解析和复制, 以及权重的转换.
逆波兰式
我们以简单的例子来说明, add(@0,@1)
对于这个计算式来说, 如果RuntimeOperator
对它进行处理, 首先遇到的节点将会是add
, 但是在遇到add
时候缺少计算所需要的具体数据@0
和@1
.
所以我们需要对它进行逆波兰转换
得到操作数
在前,计算
在后的形式, 它的实现也很简单, 就是将原有的二叉树进行后续遍历即可: @0,@1,add
void ReversePolish(const std::shared_ptr &root_node,
std::vector &reverse_polish) {
if (root_node != nullptr) {
ReversePolish(root_node->left, reverse_polish);
ReversePolish(root_node->right, reverse_polish);
reverse_polish.push_back(root_node);
}
}
add (@0,@1)
对应的逆波兰式如下:@0,@1,add
add(mul(@0,@1),@2)
对应的逆波兰式如下: @0,@1,mul,@2,add
整体过程串联
经过这样的转换, 就可以使得每次在处理计算
的时候都能够保证, 所需要的操作数已经到位. 所以我们结合上节课的内容来串联一下整体的过程.
传入
Expression:string
, 例如add(mul(@0,@1),@2)
字符串将
add(mul(@0,@1),@2)
按照词法分析为多个tokens
, 且在拆分的时候需要进行词法校验
- add
- mul
- (
- @0
- ,
- @1
- )
- ,
- @2
根据已知的
tokens
, 通过递归向下遍历的语法分析得到对应的计算二叉树. 二叉树的各个节点为add
,mul
或者@0
,@1
. 都在上节课当中将计算二叉树进行逆波兰变换, 得到的逆波兰式如下:
@0, @1, mul, @2, add
. ?
Expression Operator的定义
class ExpressionOp : public Operator {
public:
explicit ExpressionOp(const std::string &expr);
std::vector Generate();
private:
std::unique_ptr parser_; //语法分析和词法分析
std::vector nodes_; // 逆波兰之后的表达式,也是一个list
std::string expr_;
};
其中expr_
表示表达式字符串, nodes_
表示经过逆波兰变换之后得到的节点. 通过Operator去初始化Layer,layer才负责计算.
Expression Layer的定义
class ExpressionLayer : public Layer {
public:
explicit ExpressionLayer(const std::shared_ptr &op);
void Forwards(const std::vector &inputs,
std::vector &outputs) override;
private:
std::unique_ptr op_;
};
初始化Expression Layer
ExpressionLayer::ExpressionLayer(const std::shared_ptr &op) : Layer("Expression") {
CHECK(op != nullptr && op->op_type_ == OpType::kOperatorExpression);
ExpressionOp *expression_op = dynamic_cast(op.get());
CHECK(expression_op != nullptr) op_ = std::make_unique(*expression_op);
}
通过Expression operator
去初始化Layer
的过程, operator
和layer
的区别已经在之前多次讲过, operator
负责某类型节点参数的记录, layer
负责对应节点的具体计算.
Expression Layer中的输入排布
inputs当中的输入怎么排布. 两个或者多个操作数
C = A + B
input1 里面存放了A的四个数字
input2 里面存放了B的四个数字
C1 = A1+B1 = input11+input21
C2 = A2+B2 = input12+input22
A总共有batch size个
B总共有batch size个
在Expression Layer
的输入中, 多个输入依次排布. 如果batch_size
的大小为4, 则上图中input1
中的元素数量为4, input2
的元素数量也为4. 换句话说, input1
中的数据都来源于操作数1(operand 1
), input2
中的数据都来源于操作数2(operand 2
).
将数据存放到input1
和input2
的实现如下:
int batch_size = 4;
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr input = std::make_shared(3, 224, 224);
input->Fill(1.f);
inputs.push_back(input);
}
// [tensor(1.f) tensor(1.f) tensor(1.f) tensor(1.f)]
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr input = std::make_shared(3, 224, 224);
input->Fill(2.f);
inputs.push_back(input);
}
// [tensor(1.f) tensor(1.f) tensor(1.f) tensor(1.f) tensor(2.f) tensor(2.f) tensor(2.f) tensor(2.f)]
inputs
被分为两段, 前半段存放input1
, 前半段的长度为4. 后半段存放input2
, 后半段的长度为4.
计算的结果存放在outputs
, 8个输入数据两两相加, 最后的输出数据大小等于4.
Expression Layer的计算过程
数据排布
第一个例子
input1 = @0
顺序是0…3
input2 = @1
顺序是4…7
已知有如上的数据存储排布, 在本节中我们将讨论如何根据现有的数据完成add(@0,@1)
计算. 可以看到每一次计算的时候, 都以此从input1
和input2
中取得一个数据进行加法操作, 并存放在对应的输出位置.
@0 是一个张量, 总共有batch_size
个, 每个大小是3*224*224
因为batch_size里面来自与同一个批次,我们需要同一个批次另外一个操作数进行两两相加
第二个例子
下图的例子展示了对于三个输入,mul(add(@0,@1),@2)
的情况:
每次计算的时候依次从input1
, input2
和input3
中取出数据, 并作出相应的运算, 并将结果数据存放于对应的output
中.
input1 = @0
input2 = @1
input3 = @2
操作数处理的代码实现
在ExpressionLayer::Forward
函数中, 首先检查输入是否为空, 并初始化outputs
数组中的元素.
CHECK(!inputs.empty());
const uint32_t batch_size = outputs.size();
CHECK(batch_size != 0);
for (uint32_t i = 0; i < batch_size; ++i) {
CHECK(outputs.at(i) != nullptr & !outputs.at(i)->empty());
outputs.at(i)->Fill(0.f);
}
CHECK(this->op_ != nullptr & this->op_->op_type_ == OpType::kOperatorExpression);
std::stack op_stack;
const std::vector &token_nodes = this->op_->Generate();
this->op_->Generate();
获得的是逆波兰表达式. @0 @1 add @2 mul
for (const auto &token_node : token_nodes) {
if (token_node->num_index >= 0) {
uint32_t start_pos = token_node->num_index * batch_size;
//@0的num_index = 0
// @1的num_Index = 1
// inputs 按照上述的依次存放位置
std::vector input_token_nodes;
for (uint32_t i = 0; i < batch_size; ++i) {
CHECK(i + start_pos < inputs.size());
// @0 start_pos = 0 ---> i + start_pos = 0..3
// @1 start_pos = 4 ---> i + start_pos = 4..7
input_token_nodes.push_back(inputs.at(i + start_pos));
}
op_stack.push(input_token_nodes);
}
}
依次遍历逆波兰表达式, 如果当前的op
遇到的是一个操作数, 例如@0
或者@1
. 就将他们一个批次的数据(input_token_nodes
)全部读取出来, 并临时存放到栈op_stack
中.
举个例子, 对于input1
就将input1
中所有的数据读取出来并存放到input_token_nodes
中, 再将input_token_nodes
这一个批次的数据放入到栈中.
根据输入的逆波兰式@0,@1,add
,遇到的第一个节点是操作数是@0
, 所以栈op_stack
内的内存布局如下:
当根据顺序遇到第二个节点(op
)的时候, 操作数@1
的时候, 再将inputs
中的操作数读取出来并存放到input_token_nodes
中, 再将input_token_nodes
这一个批次的数据放入到栈中.
运算符处理的代码实现
// @0 @1 add
const int32_t op = token_node->num_index;
CHECK(op_stack.size() >= 2)
当节点(op
)类型为操作符号的时候, 首先弹出栈(op_stack
)内的两个批次操作数, 对于如上的情况input_node1
分别存放input1...4
, input_node2
分别存放input5...8
.
CHECK(input_node1.size() == input_node2.size());
std::vector output_token_nodes(batch_size);
for (uint32_t i = 0; i < batch_size; ++i) {
// 进行一个依次两两的相加
if (op == -int(TokenType::TokenAdd)) {
output_token_nodes.at(i) = ftensor::ElementAdd(input_node1.at(i), input_node2.at(i));
} else if (op == -int(TokenType::TokenMul)) {
output_token_nodes.at(i) = ftensor::ElementMultiply(input_node1.at(i), input_node2.at(i));
} else {
LOG(FATAL)
当获取大小长度为batch_size
的input_node1
和input_node2
后, 流程在for(int i = 0...batch_size)
中对两个输入进行两两操作, 操作类型定义于当前的op
中. 对于逆波兰式@0,@1,add
, 在如上处理完两个输入节点之后,当前的节点类型是add
.
output_token_nodes
保留了输出,
CHECK(op_stack.size() == 1);
std::vector output_node = op_stack.top();
op_stack.pop();
for (int i = 0; i < batch_size; ++i) {
CHECK(outputs.at(i) != nullptr & !outputs.at(i)->empty());
outputs.at(i) = output_node.at(i);
}
实验
TEST(test_expression, add) {
using namespace kuiper_infer;
const std::string &expr = "add(@0,@1)";
std::shared_ptr expression_op = std::make_shared(expr);
ExpressionLayer layer(expression_op);
std::vector inputs;
std::vector outputs;
int batch_size = 4;
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr input = std::make_shared(3, 224, 224);
input->Fill(1.f);
inputs.push_back(input);
}
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr input = std::make_shared(3, 224, 224);
input->Fill(2.f);
inputs.push_back(input);
}
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr output = std::make_shared(3, 224, 224);
outputs.push_back(output);
}
layer.Forwards(inputs, outputs);
for (int i = 0; i < batch_size; ++i) {
const auto &result = outputs.at(i);
for (int j = 0; j < result->size(); ++j) {
ASSERT_EQ(result->index(j), 3.f);
}
}
}
输入表达式为add(@0,@1)
,可以看到输入input1
中的数据均为1, 输入input2
中的数据均为2, 所以存放结果的每一个节点数据均为3.
TEST(test_expression, complex) {
using namespace kuiper_infer;
const std::string &expr = "add(mul(@0,@1),@2)";
std::shared_ptr expression_op = std::make_shared(expr);
ExpressionLayer layer(expression_op);
std::vector inputs;
std::vector outputs;
int batch_size = 4;
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr input = std::make_shared(3, 224, 224);
input->Fill(1.f);
inputs.push_back(input);
}
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr input = std::make_shared(3, 224, 224);
input->Fill(2.f);
inputs.push_back(input);
}
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr input = std::make_shared(3, 224, 224);
input->Fill(3.f);
inputs.push_back(input);
}
for (int i = 0; i < batch_size; ++i) {
std::shared_ptr output = std::make_shared(3, 224, 224);
outputs.push_back(output);
}
layer.Forwards(inputs, outputs);
for (int i = 0; i < batch_size; ++i) {
const auto &result = outputs.at(i);
for (int j = 0; j < result->size(); ++j) {
ASSERT_EQ(result->index(j), 5.f);
}
}
}
输入表达式为add(mul(@0,@1),@2)
,可以看到输入input1
中的数据均为1, 输入input2
中的数据均为2, 所以存放结果的每一个节点数据均为5.