從零開始山寨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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM