caffe注册机制浅析——宏与类的使用

简介: 一、楔子15年那会儿,我刚入坑深度学习。那时候几大主流框架caffe,theano,和torch7(不是pytorch)分别代表了C++、python、Lua几大语言。其中尤以caffe最为风靡也最受欢迎。因为我本科是电子工程,不是计算机科班出身,在甫一阅读caffe的代码,尤其是看到众多我不熟悉的概念,如常引用,模板类,protobuf,glog,gtest等等时,自然是一脸懵逼,直接劝退。当然

一、楔子

15年那会儿,我刚入坑深度学习。那时候几大主流框架caffe,theano,和torch7(不是pytorch)分别代表了C++、python、Lua几大语言。其中尤以caffe最为风靡也最受欢迎。因为我本科是电子工程,不是计算机科班出身,在甫一阅读caffe的代码,尤其是看到众多我不熟悉的概念,如常引用,模板类,protobuf,glog,gtest等等时,自然是一脸懵逼,直接劝退。当然后面的tensorflow,pytorch等框架横空出世,编程便不再成为科研问题,这是后话。

工作后,由于接手的代码全为C++,加之我个人是个很执着的人,我竟又想到了当年的“白月光”,不得不说,没把caffe玩明白一直是我心里一大遗憾。所以也想借着休息时间,再乘时光机回到6年前,读一读当时没有读懂的代码,也了了一桩心愿罢。

二、简介

1. 源码位置

本文主要关注的是caffe一个小的方面,即注册机制(Registration)。在caffe中,涉及到注册的部分主要有:

  • /include/caffe/solver_factory.hpp
  • /include/caffe/layer_factory.hpp
  • /tools/caffe.cpp

2. 何为注册

简单谈谈注册。其实很简单,以python语言为例,我的理解就是有一个全局的字典dict,每写一个新类,就把(类的名字,类)给添加到dict里面去。这样后面给出一个字符串string,我就能得到字符串对应的类实例,如:

REGISTERER = dict()

# Define
class A(object):
    def run(self):
        print(f"Run class: {self.__class__.__name__}")
# Register
REGISTERER[A.__name__] = A


# Define
class B(object):
    def run(self):
        print(f"Run class: {self.__class__.__name__}")
# Register
REGISTERER[B.__name__] = B


if __name__ == "__main__":
    instance = REGISTERER["B"]()
    instance.run()

从上面这个例子可以看出,如果我把和字典相关的几行撤掉,换做 if - elif - else,可能也没啥问题。是的,注册就是完成这么一回事情。当然在C++中代码有不同的写法,下面我们来看看C++是如何实现的。

三、源码分析

主要分析layer_factory.hpp中的写法,会顺便带一下solver_factory.hpp和caffe.cpp中的东西,都是一样的原理。有条件的同学打开代码地址,边看源码边看分析。

首先,layer_factory.hpp是一个头文件,所有的声明都包含在caffe这个namespace下。

其次,头文件中包含了两个模板类LayerRegistry和LayerRegisterer,以及两个宏REGISTER_LAYER_CREATOR和REGISTER_LAYER_CLASS。这是代码的基本结构。下面分别介绍这4个东东。

1. LayerRegistry

先说第一个模板类LayerRegistry,这个类是主要干活的类。

template <typename Dtype>
class LayerRegistry {
 public:
  typedef shared_ptr<Layer<Dtype> > (*Creator)(const LayerParameter&);
  typedef std::map<string, Creator> CreatorRegistry;

  static CreatorRegistry& Registry() {
    static CreatorRegistry* g_registry_ = new CreatorRegistry();
    return *g_registry_;
  }

  // Adds a creator.
  static void AddCreator(const string& type, Creator creator) {
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 0)
        << "Layer type " << type << " already registered.";
    registry[type] = creator;
  }

  // Get a layer using a LayerParameter.
  static shared_ptr<Layer<Dtype> > CreateLayer(const LayerParameter& param) {
    if (Caffe::root_solver()) {
      LOG(INFO) << "Creating layer " << param.name();
    }
    const string& type = param.type();
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 1) << "Unknown layer type: " << type
        << " (known types: " << LayerTypeListString() << ")";
    return registry[type](param);
  }

  static vector<string> LayerTypeList() {
    CreatorRegistry& registry = Registry();
    vector<string> layer_types;
    for (typename CreatorRegistry::iterator iter = registry.begin();
         iter != registry.end(); ++iter) {
      layer_types.push_back(iter->first);
    }
    return layer_types;
  }

 private:
  // Layer registry should never be instantiated - everything is done with its
  // static variables.
  LayerRegistry() {}

  static string LayerTypeListString() {
    vector<string> layer_types = LayerTypeList();
    string layer_types_str;
    for (vector<string>::iterator iter = layer_types.begin();
         iter != layer_types.end(); ++iter) {
      if (iter != layer_types.begin()) {
        layer_types_str += ", ";
      }
      layer_types_str += *iter;
    }
    return layer_types_str;
  }
};

看着有点长,我们一点点地来。

typedef shared_ptr<Layer<Dtype> > (*Creator)(const LayerParameter&);
typedef std::map<string, Creator> CreatorRegistry;
  • 首先使用typedef定义了两个类型。Creator是一个函数指针,它代表一类函数,接收LayerParameter类对象,返回一个Layer<Dtype>的smart pointer;其次,定义了一种字典类型CreatorRegistry,它的key是字符串,value是一个函数指针。怎么样?是不是和上面python代码里面的字典对上号了?

static CreatorRegistry& Registry() {
    static CreatorRegistry* g_registry_ = new CreatorRegistry();
    return *g_registry_;
}
  • LayerRegistry中的所有成员方法均为静态方法,也就是说这些函数并不与类实例对象所绑定,而是直接绑在了类上,此其一。注意到Registry函数内部是定义了一个静态的CreatorRegistry*的变量。静态变量有个特点,即它的生存期是持续到程序结束的,这是因为它并不存在与堆区或者栈区,在可重定向文件,也就是.o文件中,会单独为静态变量以及全局变量建立符号表。

      

    回过来,这个函数的意思是,如果第一次执行那么new一个对象,而如果是第二次即以后执行,那么就使用之前new出来的对象,很明显,这就是一种典型的单例模型的写法。还要竹弈一个细节,这个函数的返回值是一个引用,也就是我们就是想拿到函数内作用域的那个对象,而非进行拷贝,这点也很关键。

// Adds a creator.
  static void AddCreator(const string& type, Creator creator) {
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 0)
        << "Layer type " << type << " already registered.";
    registry[type] = creator;
  }
  • AddCreator仍然是一个静态的成员方法,首先调用上面的Registry函数,拿到字典,然后就是把字符串type和函数指针creator保存到字典里面。

// Get a layer using a LayerParameter.
  static shared_ptr<Layer<Dtype> > CreateLayer(const LayerParameter& param) {
    if (Caffe::root_solver()) {
      LOG(INFO) << "Creating layer " << param.name();
    }
    const string& type = param.type();
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 1) << "Unknown layer type: " << type
        << " (known types: " << LayerTypeListString() << ")";
    return registry[type](param);
  }
  • CreateLayer的函数实现的目的是,给定LayerParameter类对象,拿到这个parameter里面的type,去之前的字典里面查询有无,如果有,那么就把函数指针registry[type]给取出来,再把param传给这个函数,返回最终的Layer对象的smart pointer。

[小结]

基本上就3个东西,新建字典,插入字典,查询字典。

2. LayerRegisterer

template <typename Dtype>
class LayerRegisterer {
 public:
  LayerRegisterer(const string& type,
                  shared_ptr<Layer<Dtype> > (*creator)(const LayerParameter&)) {
    // LOG(INFO) << "Registering layer type: " << type;
    LayerRegistry<Dtype>::AddCreator(type, creator);
  }
};

非常小的一个类,只有一个构造函数,意味着只在实例化时才起作用。其实实际上也完全可以不用类的方式,而是简单地写个函数达到同样效果。

可以看到它真正的作用是调用了AddCreator,也就是上面所说的把字符串和函数指针给加入到字典里面去。如果是第一次执行,那么LayerRegistry会自动new一个字典出来,后面再执行,都是在那个老字典里面add了。

3. 两个宏

#define REGISTER_LAYER_CREATOR(type, creator)                                  \
  static LayerRegisterer<float> g_creator_f_##type(#type, creator<float>);     \
  static LayerRegisterer<double> g_creator_d_##type(#type, creator<double>)    \

#define REGISTER_LAYER_CLASS(type)                                             \
  template <typename Dtype>                                                    \
  shared_ptr<Layer<Dtype> > Creator_##type##Layer(const LayerParameter& param) \
  {                                                                            \
    return shared_ptr<Layer<Dtype> >(new type##Layer<Dtype>(param));           \
  }                                                                            \
  REGISTER_LAYER_CREATOR(type, Creator_##type##Layer)

两个宏的关系,下面的REGISTER_LAYER_CLASS调用了上面的REGISTER_LAYER_CREATOR,并且是对外使用的宏。宏的两个基本只是是单警号#变成字符串,双井号##黏贴左右。

那么从下面开始看起,首先声明并了一个函数,举个例子,此处的type为Concat,那么函数名字是Creator_ConcatLayer。这个函数就是把param传到了类ConcatLayer里面new出一个实例,然后返回对应的smart pointer。

然后进入到上面的宏。声明了两个静态全局变量,这两个静态全局变量都是LayerRegisterer类型的,前面已经说过,这个类型除了实例化的时候起点作用,其他时候不齐作用,所以变量本身不重要,重要的是把字符串“Concat”(也就是#type)和模板实例化后的函数指针creator<float>作为类参传给LayerRegisterer,也就能传给LayerRegistry里面的字典拉,也就能注册啦。

嚯,好家伙!绕了这么一大圈,终于给盘明白了。其实它的逻辑和我写的那个简陋的python代码并无二致,就是没字典建字典,当你新写了一个类,你把这个类名字和类本身,当成一条记录插入到字典里面去,后面别人就好查询了呗~

至于solver_factory完全是一模一样的东西。而caffe.cpp也就是定义了一大堆函数,定义完每个函数后,将函数名和函数指针给存到字典里面去,需要时再查询,再用。

四、使用流程

最后再简单地聊一聊这个注册机制的使用,仍以ConcatLayer为例(src/caffe/layers/concat_layer.cpp),定义完Layer类的各个方法以后,会执行下面2行代码:

INSTANTIATE_CLASS(ConcatLayer);
REGISTER_LAYER_CLASS(Concat);

其中,宏INSTANTIATE_CLASS定义在:/include/caffe/common.hpp,

// Instantiate a class with float and double specifications.
#define INSTANTIATE_CLASS(classname) \
  char gInstantiationGuard##classname; \
  template class classname<float>; \
  template class classname<double>

定义了一个字符(没明白这一块),然后用float和double实例化了两个模板类,在这个例子里面,我们把ConcatLayer<Dtype>给实例化成ConcatLayer<float>和ConcatLayer<double>。

那么第2行的REGISTER_LAYER_CLASS在前文已经分析的比较清楚了,我们把“Concat”和对应的返回ConcatLayer类实例的函数,添加到字典中。

最后,在Net的初始化里面(src/caffe/net.cpp),

template <typename Dtype>
void Net<Dtype>::Init(const NetParameter& in_param) {
    // ... ...
    for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id) {
        // ... ...
        // Setup layer.
        const LayerParameter& layer_param = param.layer(layer_id);
        layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));  // vector<shared_ptr<Layer<Dtype> > > layers_;
        layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);

有选择地截取了net.cpp里面的一些东西,可以看到,在Init方法里面,通过对layer_id的for循环,我们拿到对应的layer_param,然后传到了CreateLayer里面去创建Layer类实例的smart pointer,一并存储到layer_这个vector里面,后面就拿layers_这个数据结构做setup,forward,backward等等一系列事啦。

五、总结

简单地看了一下caffe中的注册机制,其实很多时候读不懂代码,一方面固然是基础薄弱,但更重要的还是不知道他想去干什么。如果能把顶层的逻辑捋清,相信一切都能够迎刃而解~~

目录
相关文章
|
8月前
|
缓存 Go API
Go 实现一个支持多种过期、淘汰机制的本地缓存的核心原理
本文旨在探讨实现一个支持多种 过期、淘汰 机制的 go 本地缓存的核心原理,我将重点讲解如何支持多样化的过期和淘汰策略。
174 0
|
存储 运维 Dubbo
Nacos 注册中心的设计原理:让你的应用轻松实现高效注册与发现!
Nacos 注册中心的设计原理:让你的应用轻松实现高效注册与发现!
188 0
|
3月前
|
机器学习/深度学习 人工智能 算法框架/工具
5.Caffe
Caffe是由伯克利人工智能研究所以及社区贡献者们共同开发的一款深度学习框架。它在深度学习领域发挥了巨大的推动作用,并以其优秀的结构、性能和代码质量成为了该领域的标志性工具。Caffe不仅降低了学习和开发的难度,还将深度学习的所有细节透明化。主要应用于视频和图像处理,核心语言为C++,并兼容命令行、Python和MATLAB接口,同时支持CPU和GPU运行,具备出色的通用性和性能。其快速上手和高速运行的特点使得即使是复杂模型和大规模数据也能轻松应对,用户可以利用多种预设层类型来自定义模型。
51 5
|
8月前
|
微服务
注册中心机制
【2月更文挑战第16天】注册中心机制
62 5
|
负载均衡 网络协议 Java
Nacos-手写注册中心基本原理
手写注册中心基本原理
224 0
|
并行计算 Ubuntu 算法框架/工具
|
存储 缓存 JavaScript
缓存集成
缓存的概念 缓存就是数据交换的缓冲区(称作:Cache),当用户要获取数据的时候,会先从缓存中去查询获取数据,如果缓存中有就会直接返回给用户,如果缓存中没有,则会发请求从服务器重新查询数据,将数据返回给用户的同时将数据放入缓存,下次用户就会直接从缓存中获取数据。 缓存其实在很多场景中都有用到,比如: 场景 作用 操作系统磁盘缓存 减少磁盘机械操作 数据库缓存 减少文件系统的IO操作 应用程序缓存 减少对数据库的查询 Web服务器缓存 减少对应用服务器请求次数 浏览器缓存 减少与后台的交互次数 缓存的优点 ​ 1.减少数据传输,节省网络流量,加快响应速度,提升用户体验; ​ 2.减轻
43 0
|
存储 负载均衡 容灾
Nacos架构与原理 - 注册中心的设计原理
Nacos架构与原理 - 注册中心的设计原理
192 0
|
Java Nacos
nacos服务注册底层源码详解
nacos服务注册底层源码详解
219 0
|
负载均衡 Dubbo Java
分布式注册的中心的实现原理|学习笔记
快速学习分布式注册的中心的实现原理
226 0
分布式注册的中心的实现原理|学习笔记