自制深度學(xué)習(xí)推理框架-第五課-起飛!框架中的算子注冊機制
我們的課程主頁
https://github.com/zjhellofss/KuiperInfer 歡迎pr和點贊
為什么要有注冊機制
KuiperInfer內(nèi)部維護(hù)了一個注冊表,用于查找Layer對應(yīng)的初始化函數(shù)。這里的Layer是KuiperInfer中的算子具體執(zhí)行者,例如我們在上一節(jié)課中我們講過的ReluLayer,用于具體執(zhí)行Relu方法,我們這里回顧一下Layer的定義:
class Layer {
public:
explicit Layer(const std::string &layer_name);
virtual void Forwards(const std::vector<std::shared_ptr<Tensor<float>>> &inputs,
std::vector<std::shared_ptr<Tensor<float>>> &outputs);
virtual ~Layer() = default;
private:
std::string layer_name_;
};
Layer注冊機制的實現(xiàn)
注冊機制的原理
今天要講的這一注冊機制用到了設(shè)計模式中的工廠模式和單例模式,所以這節(jié)課也是對兩大設(shè)計模式的一個合理應(yīng)用和實踐。KuiperInfer的注冊表是一個map數(shù)據(jù)結(jié)構(gòu),維護(hù)了一組鍵值對,key是對應(yīng)的OpType,用來查找對應(yīng)的value,value是用于創(chuàng)建該層的對應(yīng)方法(Creator)。我們可以看一下KuiperInfer中的Layer注冊表實現(xiàn):
typedef std::map<OpType, Creator> CreateRegistry;
創(chuàng)建該層的對應(yīng)方法相當(dāng)于一個工廠(Creator),Creator如下的代碼所示,是一個函數(shù)指針類型,我們將存放參數(shù)的Oprator類傳入到該方法中,然后該方法根據(jù)Operator內(nèi)的參數(shù)返回具體的Layer.
typedef std::shared_ptr<Layer> (*Creator)(const std::shared_ptr<Operator> &op);
向注冊表中注冊Layer
如果我們將Layer的注冊機制當(dāng)成一個注冊表的話,那么就會有存入的階段也有取出的階段,什么時候?qū)ayer的注冊機制存入到注冊表呢?以如下的代碼ReluLayer作為一個例子:
ReluLayer::ReluLayer(const std::shared_ptr<Operator> &op) : Layer("Relu") {
CHECK(op->op_type_ == OpType::kOperatorRelu) << "Operator has a wrong type: " << int(op->op_type_);
ReluOperator *relu_op = dynamic_cast<ReluOperator *>(op.get());
CHECK(relu_op != nullptr) << "Relu operator is empty";
this->op_ = std::make_unique<ReluOperator>(relu_op->get_thresh());
}
void ReluLayer::Forwards(const std::vector<std::shared_ptr<Tensor<float>>> &inputs,
std::vector<std::shared_ptr<Tensor<float>>> &outputs) {
CHECK(this->op_ != nullptr);
CHECK(this->op_->op_type_ == OpType::kOperatorRelu);
const uint32_t batch_size = inputs.size();
for (int i = 0; i < batch_size; ++i) {
CHECK(!inputs.at(i)->empty());
const std::shared_ptr<Tensor<float>> &input_data = inputs.at(i);
input_data->data().transform([&](float value) {
float thresh = op_->get_thresh();
if (value >= thresh) {
return value;
} else {
return 0.f;
}
});
outputs.push_back(input_data);
}
}
std::shared_ptr<Layer> ReluLayer::CreateInstance(const std::shared_ptr<Operator> &op) {
std::shared_ptr<Layer> relu_layer = std::make_shared<ReluLayer>(op);
return relu_layer;
}
LayerRegistererWrapper kReluLayer(OpType::kOperatorRelu, ReluLayer::CreateInstance);
LayerRegistererWrapper kReluLayer(OpType::kOperatorRelu, ReluLayer::CreateInstance), 完成了ReluLayer定義后的注冊,其中key為kOperatorRelu, value 為ReluLayer::CreateInstance. CreateInstance是一個具體的工廠方法,用來在之后對ReluLayer進(jìn)行初始化,我們接下來看看這里注冊機制的具體實現(xiàn):
class LayerRegistererWrapper {
public:
LayerRegistererWrapper(OpType op_type, const LayerRegisterer::Creator &creator) {
LayerRegisterer::RegisterCreator(op_type, creator);
}
};
LayerRegistererWrapper是一個類
我們在調(diào)用kReluLayer(OpType::kOperatorRelu, ReluLayer::CreateInstance)的時候,LayerRegistererWrapper的構(gòu)造方法再調(diào)用了RegisterCreator。所以到目前為止,調(diào)用鏈?zhǔn)沁@樣的:
ReluLayer定義完成--->LayerRegistererWrapper ---> RegisterCreator
接下來看看RegisterCreator這個注冊方法的實現(xiàn):
void LayerRegisterer::RegisterCreator(OpType op_type, const Creator &creator) {
CHECK(creator != nullptr) << "Layer creator is empty";
CreateRegistry ®istry = Registry();
CHECK_EQ(registry.count(op_type), 0) << "Layer type: " << int(op_type) << " has already registered!";
registry.insert({op_type, creator});
}
再強調(diào)一遍,方法中的op_type是算子的類型,作為Layer注冊表的key使用,creator是創(chuàng)建具體層的工廠方法,作為Layer注冊表的value. 目前為止,調(diào)用鏈?zhǔn)沁@樣的:
ReluLayer定義完成 --->LayerRegistererWrapper ---> RegisterCreator --->Registry返回注冊表 --->存入實現(xiàn)方法
單例注冊表的實現(xiàn)
可以看到CreateRegistry ®istry =Registry() 這里返回注冊表的實例,此處的Layer注冊表是全局唯一的,全局唯一的實現(xiàn)方法是單例設(shè)計模式的應(yīng)用,我們看一下下方的具體實現(xiàn):
LayerRegisterer::CreateRegistry &LayerRegisterer::Registry() {
static CreateRegistry *kRegistry = new CreateRegistry();
CHECK(kRegistry != nullptr) << "Global layer register init failed!";
return *kRegistry;
}
此處的static CreateRegistry *kRegistry =newCreateRegistry() , 使得Layer注冊表在全局有且只有一個,無論我們調(diào)用了多少次Registry(), 該方法都會返回同一個Layer注冊表。
向注冊表中取出Layer
在完成Layer注冊之后,我們就可以根據(jù)OpType來取出用于實例化一個Layer的工廠函數(shù),比如上面我們完成了ReluLayer的注冊后,系統(tǒng)中的Layer注冊表中是這樣的:
{kOperatorRelu:ReluLayer::CreateInstance}
我們在需要的時候時候根據(jù)kOperator來取出ReluLayer::CreateInstance, 我們再以Relu中的工廠函數(shù)為例子看看一個具體工廠函數(shù)的實現(xiàn):
std::shared_ptr<Layer> ReluLayer::CreateInstance(const std::shared_ptr<Operator> &op) {
std::shared_ptr<Layer> relu_layer = std::make_shared<ReluLayer>(op);
return relu_layer;
}
可以看到這里的工廠函數(shù)比較簡單,直接傳入ReluOperator完成對ReluLayer的初始化。
一個例子
TEST(test_layer, forward_relu2) {
using namespace kuiper_infer;
float thresh = 0.f;
std::shared_ptr<Operator> relu_op = std::make_shared<ReluOperator>(thresh);
std::shared_ptr<Layer> relu_layer = LayerRegisterer::CreateLayer(relu_op);
std::shared_ptr<Tensor<float>> input = std::make_shared<Tensor<float>>(1, 1, 3);
input->index(0) = -1.f;
input->index(1) = -2.f;
input->index(2) = 3.f;
std::vector<std::shared_ptr<Tensor<float>>> inputs;
std::vector<std::shared_ptr<Tensor<float>>> outputs;
inputs.push_back(input);
relu_layer->Forwards(inputs, outputs);
ASSERT_EQ(outputs.size(), 1);
for (int i = 0; i < outputs.size(); ++i) {
ASSERT_EQ(outputs.at(i)->index(0), 0.f);
ASSERT_EQ(outputs.at(i)->index(1), 0.f);
ASSERT_EQ(outputs.at(i)->index(2), 3.f);
}
}
我們可以看到std::shared_ptr<Operator> relu_op = std::make_shared<ReluOperator>(thresh), 初始化了一個ReluOperator, 其中的參數(shù)為thresh=0.f.
因為我們已經(jīng)在ReluLayer的實現(xiàn)中完成了注冊,{kOperatorRelu:ReluLayer::CreateInstance} , 所以現(xiàn)在可以使用LayerRegisterer::CreateLayer(relu_op)得到我們ReluLayer中的實例化工廠方法,我們再來看看CreateLayer的實現(xiàn):
std::shared_ptr<Layer> LayerRegisterer::CreateLayer(const std::shared_ptr<Operator> &op) {
CreateRegistry ®istry = Registry();
const OpType op_type = op->op_type_;
LOG_IF(FATAL, registry.count(op_type) <= 0) << "Can not find the layer type: " << int(op_type);
const auto &creator = registry.find(op_type)->second;
LOG_IF(FATAL, !creator) << "Layer creator is empty!";
std::shared_ptr<Layer> layer = creator(op);
LOG_IF(FATAL, !layer) << "Layer init failed!";
return layer;
}
可以看到傳入的參數(shù)為op, 我們首先取得op中的op_type, 此處的op_type為kOperatorRelu, 根據(jù)registry.find(op_type), 就得到了層的初始化方法creator, 隨后使用傳入的op去初始化layer并返回實例。值得注意的是此處也調(diào)用了CreateRegistry ®istry =Registry()返回了我們所說的全局有且唯一的Layer注冊表. 此處的creator(op)就相當(dāng)于調(diào)用了ReluLayer::CreateInstance.
如何使用我們已經(jīng)實現(xiàn)并注冊的算子
請看視頻部分
本節(jié)課的作業(yè)
作業(yè)地址
git clone https://github.com/zjhellofss/KuiperCourse/tree/fouthtwo/source/layer
git checkout fouthtwo
備用地址: https://gitee.com/fssssss/KuiperCourse/
具體步驟
請實現(xiàn)一個sigmoid算子, sigmoid的算子由如下的公式定義:
請你以Relu Layer相同的辦法, 去實現(xiàn)并注冊這個算子并完成test_sigmoid.cpp下TEST(test_layer, forward_sigmoid) 測試用例的測試. 代碼的整體部分已經(jīng)給出, 繼續(xù)完成核心部分即可.
