zoukankan      html  css  js  c++  java
  • 从零开始山寨Caffe·叁:全局线程管理器

    你需要一个管家,随手召唤的那种,想吃啥就吃啥。

                           ——设计一个全局线程管理器

    一个机器学习系统,需要管理一些公共的配置信息,如何存储这些配置信息,是一个难题。

    设计模式

    MVC框架

    在传统的MVC编程框架中,通常采取设立数据中心的做法,将所有配置信息存在其中。

    同时,将数据中心指针共享至所有类,形成一个以数据为中心,多重引用的设计模式。

    如图,以MFC默认编程思路为例:

    这种编程框架,虽然思路清晰,但是需要将共享指针传来传去,显得相当赘余。

    全局静态框架

    这是一种新手程序员经常习惯干的事。

    不设立封装型的数据中心,而是将配置信息写在全局静态变量中。

    必要时,直接用Get()函数获取。

    Caffe恰恰使用了这种naive的做法,不过配上Boost之后,就相当powerful了。

    多线程

    一个线程除了需要些基础的配置信息,还需要什么?

    在机器学习系统中,还需要随机数发生器。

    随机数难题

    产生一个随机数很简单,time(0)看起来不错,但是波动很有规律,是质量很低的随机数种子。

    计算领域最常用的随机数发生器是 梅森旋转法 ,Caffe也使用了这是一个兼有速度和质量的发生器。

    ACM大神ACdreamers给出了一段模仿代码,ACM选手大概写了40行。

    在CPU串行情况下,这40行代码怎么跑都行,但是在异步多线程中,问题就来了。

    假设一个管理器中包含梅森旋转法实例对象,实例对象里有生成函数,且这个管理器只属于主进程,

    当一个线程A需要随机数时,它可以访问主进程的梅森旋转法实例对象,执行该实例对象的产生函数。

    梅森发生器每执行一次,其内部数据将有一次变动,我们可以将其视为对发生器的修改操作。

    这个修改操作的触发对象大致有两个来源:

    ①DataTransformer,它在一个线程中调用。

    ②所有Layer参数的初始化,它在主进程中调用。

    这样,如果梅森发生器只有一份,必然成为线程争夺的临界资源,它还包含修改操作。

    临界资源不加mutex是危险的,如果我们为其加mutex,又有两处不便:

    ① 编程复杂,写mutex需要一番精力。

    ② 对临界资源产生阻塞访问,一定程度上降低了多线程的效率。

    综合以上两点,随机数发生器最好设计成线程独立的资源。

    设备难题

    在多GPU情况下,我们需要为每一个GPU准备一个CPU线程来监督工作。

    全局管理器会包含GPU设备信息(set device、get device)。

    假设只有一个管理器,那么多个GPU该怎么去访问这个管理器,获得自己的设备编号?

    显然,如果将管理器设计成线程独立的,那么这个问题就很好解决了。

    同理,可以推广到root_solver这个属性。

    显然,在主进程中,root_solver应该为true,默认它调度GPU0。

    在监督其它GPU的CPU线程中,root_solver应该为false。

    如果只有一个管理器,显然也是不妥的。

    因而,随机数发生器必须具有线程独立性,进而,全局管理器必须有线程独立性。

    Boost库提供了一个线程独立性智能指针,方便了线程独立资源的设计。

    线程智能指针

    boost::thread_specific_ptr<Class>是较为特殊的一个智能指针,但它不属于智能指针组,

    位于"boost/thread/tss.hpp"下,属于boost::thread组。

    通常将thread_specific_ptr指针设为全局static变量,进程和线程访问该指针时,将提供不同的结果。

    其内部实现原理,应该是记录进程pid和线程tid,来做一个hash,以达到线程独立资源的管理。

    static boost::thread_specific_ptr<Dragon> thread_instance;
    Dragon& Dragon::Get(){
        if (!thread_instance.get()) thread_instance.reset(new Dragon());
        return *(thread_instance.get());
    }

    将类静态函数Get封装之后,我们可以获得线程独立的管理器对象Dragon。

    实例对象的代码空间将由Boost::thread控制,不在主进程的控制范围,

    这样,Dragon管理器里的复杂代码,在执行时不会因为异步而被截断。

    代码实战

    随机数系统设计

    建立rng.hpp,包含"boost/random/mersenne_twister.hpp"

    typedef boost::mt19937 rng_t;

    mt19937是梅森旋转法的一个32位实现版本,由boost提供,将至重命名为rng_t

    建立common.hpp,创建管理器类Dragon。

    ———————————————————————————————————————————————————————————

    首先,我们需要一个低质量的随机数种子,来初始化梅森旋转法。

    同时,这个随机数种子,还必须是进程相关而不是线程相关的,避免多线程造成梅森随机数数值波动。

    在Dragon管理器内部,声明静态成员函数:static int64_t cluster_seedgen();

    建立common.cpp,实现这个低质量随机数种子发生器:

    int64_t Dragon::cluster_seedgen(){
        int64_t seed, pid, t;
        pid = _getpid();
        t = time(0);
        seed = abs(((t * 181) *((pid - 83) * 359)) % 104729); //set it as you want casually
        return seed;
    }
    ★int64_t Dragon::cluster_seedgen()

    Caffe利用pid、和默认时间、以及几个奇怪的数计算了一个低质量的随机数种子,

    最外层的104729保证了种子的范围,你可以随便改成任何喜欢的数。

    ———————————————————————————————————————————————————————————

    在Dragon内部声明且定义RNG类:

    class RNG{
        public:
            RNG() { generator.reset(new Generator()); }
            RNG(unsigned int seed) {generator.reset(new Generator(seed));}
            rng_t* get_rng() { return generator->get_rng(); }
                class Generator{
                public:
                    //using pid generators a simple seed to construct RNG
                    Generator() :rng(new rng_t((uint32_t)Dragon::cluster_seedgen())) {} 
                    //assign a specific seed to construct RNG
                    Generator(unsigned int seed) :rng(new rng_t(seed)) {}
                    rng_t* get_rng() { return rng.get(); }
                private:
                    boost::shared_ptr<rng_t> rng;
                };
        private:
            boost::shared_ptr<Generator> generator;
        };
    ★class RNG

    RNG类内嵌一个Generator发生器类,Generator内部又封装一个rng_t。

    梅森发生器支持两种构造模式,指定种子或使用进程相关的低质量种子。

    梅森发生器以动态内存的形式管理rng_t,rng_t* get_rng()函数需要特别注意。

    Boost的rng_t,也就是boost::mt19937,本质是一个类,这个类内部设置了复制构造函数。

    复制构造函数的内容很有趣——重置梅森算法状态,这意味着,如下代码会是个灾难:

    rng_t a;
    rng_t b=a;

    如果梅森发生器a已经产生了一定随机数,那么将a赋值给b,b将重复a的前几个值,因为发生器被重置了。

    如果需要获得发生器a的副本,应当使用指针来获取,这是为什么get_rng()要返回指针的原因。

    在Dragon里定义一个静态成员函数的get_rng(),方便外部直接调用:

    static rng_t* get_rng(){
        if (!Get().random_generator){
            Get().random_generator.reset(new RNG());
        }
        rng_t* rng = Get().random_generator.get()->get_rng();
        return rng;
    }
    ★static rng_t* get_rng()

    它利用了线程独立管理器来构建管理器内部的RNG对象,并返回rng_t指针。

    获得一个随机数很简单:

    static unsigned int get_random_value(){
        rng_t* rng = get_rng();
        return (*rng)();
    }

    boost::mt19937同时重载了()函数,调用它直接可产生随机数,切记以指针解引用形式调用。

    数据结构

    class Dragon{
    public:
        Dragon();
        ~Dragon();
        static Dragon& Get();
        enum Mode{ CPU, GPU };
        static Mode get_mode() { return Get().mode; }
        static void set_mode(Mode mode) {Get().mode = mode;}
        static int get_solver_count() { return Get().solver_count; }
        static void set_solver_count(int val) { Get().solver_count = val; }
        static bool get_root_solver() {return Get().root_solver;}
        static void set_root_solver(bool val) {Get().root_solver = val;}
        static void set_random_seed(unsigned int seed);
        static void set_device(const int device_id);
        static rng_t* get_rng();
        static unsigned int get_random_value();
        static int64_t cluster_seedgen();
        class RNG{....}
    #ifndef CPU_ONLY
        static cublasHandle_t get_cublas_handle() { return Get().cublas_handle; }
        static curandGenerator_t get_curand_generator() {return Get().curand_generator;}
    #endif
    private:
        Mode mode;
        int solver_count;
        bool root_solver;
        boost::shared_ptr<RNG> random_generator;
    #ifndef CPU_ONLY
        cublasHandle_t cublas_handle;
        curandGenerator_t curand_generator;
    #endif
    };
    ★class Dragon

    成员变量包括:

    ★工作模式:mode

    ★solver相关:solver_count和root_solver

    ★RNG:random_generator

    ★CUDA相关:cublas句柄cublas_handle、curand发生器curand_generator

    成员函数包括:

    ★get系封装

    ★set系封装

    ★随机数系统相关

    ———————————————————————————————————————————————————————————

    比较难以理解的是solver_count和root_solver,这涉及到分布式计算上。

    新版Caffe允许多GPU间并行,与AlexNet不同,多GPU模式的内涵在于:“不共享数据,却共享网络”

    所以,允许多个solver存在,且应用到不同的GPU上去。

    直接使用solver_count的地方是DataReader,每一个DataLayer都有一个DataReader,

    DataReader工作在异步线程,我们允许在一个主程序上跑多个DataLayer,但是不可以有多个ConvLayer。

    关于多GPU的介绍请看第肆章。

    第一个solver会成为root_solver,第二、第三个solver就会成为shared_solver。

    root_solver有很大一部分特权,具体有以下几点:

    ★LOG(INFO)允许信息:显然我们不需要让几个GPU,产生几份重复的信息。

    ★测试:只有root_solver才能测试,猜测是为了减少冗余计算?

    ★统计结果:只有root_solver才能输出统计结果,这点同第一点。

    ———————————————————————————————————————————————————————————

    CUDA则需要做cublas和curand的初始化。

    默认提供了更改当前GPU设备的函数,set_device()。

    device更改的时候,会让当前cublas和curand无效,需要释放并且重新申请。

    common.hpp,如其名“通用”二字,我们可以将一些重要的通用宏放置其中。

    这些宏如下:

    //    MACRO: Instance a class
    //    more info see http://bbs.csdn.net/topics/380250382
    #define INSTANTIATE_CLASS(classname) 
      template class classname<float>; 
      template class classname<double>
    
    
    //    instance for forward/backward in cu file
    //    note that INSTANTIATE_CLASS is meaningless in NVCC complier
    //    you must INSTANTIATE again
    #define INSTANTIATE_LAYER_GPU_FORWARD(classname) 
      template void classname<float>::forward_gpu( 
          const vector<Blob<float>*>& bottom, 
          const vector<Blob<float>*>& top); 
      template void classname<double>::forward_gpu( 
          const vector<Blob<double>*>& bottom, 
          const vector<Blob<double>*>& top);
    
    #define INSTANTIATE_LAYER_GPU_BACKWARD(classname) 
      template void classname<float>::backward_gpu( 
          const vector<Blob<float>*>& top, 
          const vector<bool> &data_need_bp, 
          const vector<Blob<float>*>& bottom); 
      template void classname<double>::backward_gpu( 
          const vector<Blob<double>*>& top, 
          const vector<bool> &data_need_bp, 
          const vector<Blob<double>*>& bottom)
    
    #define INSTANTIATE_LAYER_GPU_FUNCS(classname) 
      INSTANTIATE_LAYER_GPU_FORWARD(classname); 
      INSTANTIATE_LAYER_GPU_BACKWARD(classname)
    ★宏

    宏的作用在第壹章已做详解。

    实现

    大部分简短实现都和成员函数声明写在了一起。

    这里完善一下Dragon管理器的构造和析构函数。

    #ifdef CPU_ONLY
    //    implements for CPU Manager
    Dragon::Dragon():
        mode(Dragon::CPU), solver_count(1), root_solver(true) {}
    Dragon::~Dragon() { }
    void Dragon::set_device(const int device_id) {}
    #else
    //    implements for CPU/GPU Manager
    Dragon::Dragon() :
        mode(Dragon::CPU), solver_count(1), root_solver(true),
        cublas_handle(NULL), curand_generator(NULL){
        if (cublasCreate_v2(&cublas_handle) != CUBLAS_STATUS_SUCCESS)
            LOG(ERROR) << "Couldn't create cublas handle.";
        if (curandCreateGenerator(&curand_generator, CURAND_RNG_PSEUDO_DEFAULT) != CURAND_STATUS_SUCCESS
            || curandSetPseudoRandomGeneratorSeed(curand_generator, cluster_seedgen()) != CURAND_STATUS_SUCCESS)
            LOG(ERROR) << "Couldn't create curand generator.";
    }
    
    Dragon::~Dragon(){
        if (cublas_handle) cublasDestroy_v2(cublas_handle);
        if (curand_generator) curandDestroyGenerator(curand_generator);
    }
    构造与析构

    CPU和GPU用宏隔开编译了,默认模式是CPU,solver_count为1,root_sovler为真

    GPU的构造和析构函数还需要追加cublas和curand的初始化和释放。

    void Dragon::set_device(const int device_id) {
        int current_device;
        CUDA_CHECK(cudaGetDevice(&current_device));
        if (current_device == device_id) return;
        // The call to cudaSetDevice must come before any calls to Get, which
        // may perform initialization using the GPU.
    
        //    reset Device must reset handle and generator???
        CUDA_CHECK(cudaSetDevice(device_id));
        if (Get().cublas_handle) cublasDestroy_v2(Get().cublas_handle);
        if (Get().curand_generator) curandDestroyGenerator(Get().curand_generator);
        cublasCreate_v2(&Get().cublas_handle);
        curandCreateGenerator(&Get().curand_generator, CURAND_RNG_PSEUDO_DEFAULT);
        curandSetPseudoRandomGeneratorSeed(Get().curand_generator, cluster_seedgen());
    }

    当CUDA人工强制切换设备后(通常不建议这么做),原有的cublas句柄和curand发生器会失效。

    因为它们是绑定GPU的,这时候需要销毁重新构造,绑定新的GPU。

    另外,在Windows上,短时间内关闭打开,多次启动程序,绑定cublas句柄频率过快,也可能导致绑定失败。

    完整代码

    common.hpp:

    https://github.com/neopenx/Dragon/blob/master/Dragon/include/common.hpp

    common.cpp:

    https://github.com/neopenx/Dragon/blob/master/Dragon/src/common.cpp

  • 相关阅读:
    手风琴效果
    开关门
    常用的一些获取尺寸的方法
    话话js中的事件监听
    数组去重
    百度地图 之 热力图
    小程序 之 百度智能小程序(第二章)
    小程序 之 百度智能小程序(第一章)
    javascript 之 继承与闭包
    浅谈prototype与__proto__、constructor三者之间的关系
  • 原文地址:https://www.cnblogs.com/neopenx/p/5194224.html
Copyright © 2011-2022 走看看