1. 程式人生 > >從零開始山寨Caffe·陸:IO系統(一)

從零開始山寨Caffe·陸:IO系統(一)

你說你學過作業系統這門課?寫個無Bug的生產者和消費者模型試試!

                              ——你真的學好了作業系統這門課嘛?

在第壹章,展示過這樣圖:

其中,左半部分構成了新版Caffe最惱人、最龐大的IO系統。

也是歷來最不重視的一部分。

第伍章又對左半部分的獨立性進行了分析,我是這麼描述到:

Datum和Blob(Batch)不是上下文相關的。

Blob包含著正向傳播的shape資訊,這些資訊只有初始化網路在初始化時才能確定。

而Datum則只是與輸入樣本有關。

所以,Datum的讀取工作可以在網路未初始化之前就開始,這就是DataReader採用執行緒設計的內涵。

所以,左半部分又可以分為左左半部分,和左右半部分。

阻塞佇列

生產者與消費者

第伍章講到,在一個機器學習系統中,生產者和消費者的執行週期是不一樣的。

為了平衡在週期上的差異,節約計算資源,我們顯然需要對生產者做一定限制。

儲存生產資源,可以用陣列,也可以用STL容器。

再考慮生產者和消費者的行為:

①不存在隨機訪問:

顯然,消費者是按照固定順序訪問緩衝區的。

我們沒有必要考慮隨機訪問的情況。

②不存在隨機寫入:

顯然,生產者每次只需要將資源放置於緩衝區兩端。

我們沒有必要考慮線上性表中間位置寫入的情況。

由於vector底層由順序表實現,其訪問速度隨著元素數量的遞增而遞減,

而queue底層由鏈式表實現,其訪問速度不隨元素數量的遞增而遞減,且沒有隨機寫入/訪問的情況。

所以,選擇queue作為緩衝區是比較優異的。

為了限制生產者的行為,我們需要在STL提供的queue基礎上,改進出一種新的資料結構——Blocking Queue。

互斥鎖

第肆章簡單提到了mutex問題,這是阻塞佇列除了Blocking之外,需要考慮的第二大問題。

並且已經證明了:生產者和消費者之間必然是非同步的。

我們以佇列的push和pop操作為例,分析一下,為什麼在多執行緒情況下,需要加mutex。

假設執行緒A預備執行push操作,所以它是一個生產者;

假設執行緒B預備執行pop操作,所以它是一個消費者;

設有臨界緩衝區佇列Q,在某時刻T,執行緒A發出push操作,在T+1時候,執行緒B發出pop操作,

且push需要10個單位時間,pop只需要一個單位時間,問T+2時刻,pop出去的資源你敢用嘛?

顯然,沒人敢用這個執行push的半成品。

發生上述問題的癥結在於,兩個非同步執行緒對於同一個資源,產生了爭奪行為。

解決方案就是:在push時,鎖住資源,禁止pop;在pop時,鎖住資源,禁止push。

廣義上,我們可以認為,需要將push和pop函式變成原子函式,即:執行期間不可中斷的函式。

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

另外,需要注意的是,mutex與blocking是兩個概念。

在廣義上,mutex會將多個執行緒對同一個資源的非同步並行操作,拉成一個序列執行佇列,序列等待執行。

而blocking則是將執行緒休眠,CPU會暫時放棄對其控制。

在程式設計師界,雖然有時候會把mutex和blocking都稱為阻塞,但其原理和內涵是完全不同的。

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

boost提供不俗的mutex功能,使用前需要 #include "boost/thread/mutex.hpp"

你可以將一個boost::mutex物件嵌入到一個類當中,這樣,允許每一個類物件擁有一把鎖。

由於對一個queue物件,主要是鎖住來自該物件的push和pop操作,

所以,mutex理應當是以類物件為一個單位的,參考程式碼如下:

template <typename T>
class BlockingQueue{
public:
    void push(const T& t){
        boost::mutex::scoped_lock lock(mutex);
        Q.push(t);
    }
    T pop(){
        boost::mutex::scoped_lock lock(mutex);
        T t = Q.front();
       Q.pop();
    return t;
    }
private:
    boost::mutex mutex;
    queue<T> Q;
};    

boost::mutex::scoped_lock lock提供區域性鎖定功能。

它與boost::scoped_ptr有類似的效果,scoped_ptr在作用域結束後,就立即釋放物件。

scoped_lock在作用域結束後,會立即解鎖,如果不用scoped_lock,我們可以這麼寫:

void push(const T& t){
        mutex.lock();
        Q.push(t);
        mutex.unlock();
}

條件阻塞與啟用

前面幾章說了那麼久的阻塞,其中大部分指的應該是blocking。

mutex大部分情況下,都只是在鎖一個區域性函式,阻塞週期非常短。

唯一的例外是Layer的正向傳播函式forward,mutex鎖住的週期非常長。

blocking和mutex的唯一不同在於:

blocking之後,作業系統會唆使CPU放棄對執行緒的處理。

這是非常危險的一個行為,因為該執行緒被家長趕去睡覺了,而且不能反抗家長的命令。

除非家長通知它:噢,你可以活動了。在此之前,該執行緒將永遠處於無效狀態。

上面的例子有兩個重點:

①CPU放棄執行緒

②不可主動啟用

既然如此,為了啟用這個執行緒,模型就必須設計成“對偶模型”,而生產者和消費者,恰恰正是對偶的。

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

boost::condition_variable提供了簡單的blocking功能,為了統一控制,可以將其與mutex捆在一起:

template <typename T>
class BlockingQueue
{
public:
    class Sync{
    public:
        boost::mutex mutex;
        boost::condition_variable condition;
    };
private:
    queue<T> Q;
    boost::shared_ptr<Sync> sync;
};

現在考慮一下,何時需要登出、阻塞一個執行緒,大致有兩種情況:

①緩衝區空,此時消費者不能消費,拒絕pop操作之後,可以交出CPU控制權。

②緩衝區滿,此時生產者不能生產,拒絕push操作之後,可以交出CPU控制權。

為了啟用彼此,就需要模型是對偶的:

①經歷緩衝區空之後,突然push了一個元素,此時應當由生產者啟用消費者執行緒。

②經歷緩衝區滿之後,突然pop了一個元素,此時應當由消費者啟用生產者執行緒。

看起來,我們可以將程式碼寫成這樣:

void BlockingQueue<T>::push(const T& t){
    boost::mutex::scoped_lock lock(sync->mutex);
    while (Q.full()){
        sync->condition.wait(lock); //suspend, spare CPU clock
    }
    Q.push(t);
    sync->condition.notify_one();
}

template<typename T>
T BlockingQueue<T>::pop(const string& log_waiting_msg){
    boost::mutex::scoped_lock lock(sync->mutex);
    while (Q.empty()){
        sync->condition.wait(lock); //suspend, spare CPU clock
    }
    T t = Q.front();
    Q.pop();
    sync->condition.notify_one();
    return t;
}

其中,sync->condition.wait(lock)表示使用當前mutex為標記,交出CPU控制權。

sync->condition.notify_one()則表示啟用一個執行緒的CPU控制權。

可以看到,blocking和activating的程式碼是完全對偶的,blocking自己,activating對方。

雙阻塞佇列

上節程式碼是不可能實現的,因為沒有Q.full()這個函式。

在傳統生產者、消費者程式中,通常會使用單緩衝佇列。

使用單緩衝佇列是沒有問題的,因為在這種簡單的程式碼結構中,我們很容易知道緩衝佇列的上界。

比如,設定緩衝佇列大小為20,在程式設計中,可以通過檢測 if(count==20)來達到。

當代碼結構複雜時,比如,緩衝佇列大小變數通常在非常上層上層上層的位置,而處於底層的緩衝佇列,

是無法探知何謂“緩衝佇列滿”的含義的,這就為程式設計帶來很大的難題。

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

解決方案是,使用雙緩衝佇列組方案,我們設定兩個阻塞佇列,一個叫free,一個叫full。

兩者組成一個QueuePair:

class QueuePair{
public:
    QueuePair(const int size);
    ~QueuePair();
    BlockingQueue<Datum*> free; // as producter queue
    BlockingQueue<Datum*> full; // as consumer queue
};

為了避免檢測緩衝佇列的上界,我們可以先放置與上界數量等量的空元素指標到free佇列。

每次生產者生產時,從free佇列中pop一個空Datum元素,填充,再扔進full佇列。

這樣,BlockingQueue的push操作就不需要檢測上界了。

原理很簡單,生產者想要push,之前必須pop,pop可以通過檢測緩衝佇列空來實現。

這樣,就用檢測一個緩衝佇列的空,模擬且替代了檢測另一個緩衝佇列的滿。

對於上層程式碼而言,我們僅僅需要預先填充N個元素至free佇列中即可,非常方便。

這部分是DataReader的設計核心。

程式碼實戰

★資料結構

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

建立blocking_queue.hpp。

template <typename T>
class BlockingQueue
{
public:
    BlockingQueue();
    void push(const T& t); 
    T pop(const string& log_waiting_msg="");
    T peek();
    size_t size();
    // try_func return false when need blocking
    // try_func for destructor
    bool try_pop(T* t);
    bool try_peek(T* t);
    class Sync{
    public:
        boost::mutex mutex;
        boost::condition_variable condition;
    };
private:
    queue<T> Q;
    boost::shared_ptr<Sync> sync;
};
★class BlockingQueue

除了push和pop之外,追加佇列第三個常用操作——peek。

peek目的是取出隊首元素,但是不從佇列裡pop掉。

peek用於實驗性讀取Datum,為DataTransfomer初始化所用。

除了通過返回值之外獲取之外,我們還要準備try系列函式。

try除了獲取元素外,同時返回一個bool值,表明成功或者失敗。

主要用於對Datum的析構,這也是所有程式碼裡,唯一一處對protobuff數值的析構。

★實現

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

建立blocking_queue.cpp。

整體程式碼沒有什麼好說的,細節以及在上文講解了。

template<typename T>
BlockingQueue<T>::BlockingQueue() :sync(new Sync()) {}

template<typename T>
void BlockingQueue<T>::push(const T& t){

    //    function_local mutex and unlock automaticly
    //    cause another thread could call pop externally
    //    when this thread is calling push pop&peer at the same time

    boost::mutex::scoped_lock lock(sync->mutex);
    Q.push(t);

    //    must wake one opposite operation avoid deadlock
    //  formula: wait_kind_num = notify_kind_num
    //  referring Producter-Consumer Model and it's semaphore setup method
    sync->condition.notify_one();
}

template<typename T>
T BlockingQueue<T>::pop(const string& log_waiting_msg){
    boost::mutex::scoped_lock lock(sync->mutex);
    while (Q.empty()){
        if (!log_waiting_msg.empty()){ LOG_EVERY_N(INFO, 1000) << log_waiting_msg; }
        sync->condition.wait(lock); //suspend, spare CPU clock
    }
    T t = Q.front();
    Q.pop();
    return t;
}

template<typename T>
T BlockingQueue<T>::peek(){
    boost::mutex::scoped_lock lock(sync->mutex);
    while (Q.empty())
        sync->condition.wait(lock);
    T t = Q.front();
    return t;
}

template<typename T>
bool BlockingQueue<T>::try_pop(T* t){
    boost::mutex::scoped_lock lock(sync->mutex);
    if (Q.empty()) return false;
    *t = Q.front();
    Q.pop();
    return true;
}

template<typename T>
bool BlockingQueue<T>::try_peek(T* t){
    boost::mutex::scoped_lock lock(sync->mutex);
    if (Q.empty()) return false;
    *t = Q.front();
    return true;
}

template<typename T>
size_t BlockingQueue<T>::size(){
    boost::mutex::scoped_lock lock(sync->mutex);
    return Q.size();
}
實現

模板例項化

在第壹章,我們提到了INSTANTIATE_CLASS(classname)巨集的作用。

本段將重點解釋,出現在blocking_queue.cpp最後的例項化程式碼。

模板機制與編譯空間

template<typename T>可以說是整個Caffe裡出現頻率最高的程式碼了。

C++編譯器有個好玩的特性,就是對於在cpp檔案裡出現的模板定義程式碼,

只檢查最基本的語法錯誤,比如標點符號之類的。甚至你把變數名拼錯了,編譯仍然能通過。

所以,我在最初山寨Caffe的時候,寫了一堆錯誤的程式碼,編譯器都沒告訴我。

後來在醫院體檢時,偶然轉了幾圈,大概猜到了編譯器應該是為模板程式碼開了獨立的編譯檢查空間。

為了便於理解,參考圖如下:

由於C/C++是強型別檢查語言,型別檢查處於編譯先鋒位置。

而未確定型別的模板定義程式碼,將不會進行大部分詞法分析、語法分析、語義分析。

標頭檔案與原始檔

奇怪的是,如果你將模板定義程式碼寫在標頭檔案裡,那麼它就會被上升到普通編譯空間。

原理大致如下:

編譯器不會對未include的標頭檔案進行最終編譯。

這意味著,如果你要使用一個模板型別,比如A<int> a;

必然處於include下,此時必然是指定型別的,編譯器就不必將程式碼push到模板空間。

或者,存在一種轉移,編譯器將定義程式碼由模板空間轉到普通空間,進行下一步分析。

然而,如果我們將模板定義程式碼寫在原始檔A.cpp裡,然後在B.cpp裡,使用A<int> a,

此時編譯器應該去哪裡找模板類A的定義程式碼?按照編譯鏈追溯,應該是到A.hpp裡,

再由A.hpp,找到A.cpp。

這種思路在模板定義於A.cpp是不可能實現的,如圖所示:

這是兩種空間本質區別,由於模板空間的分析沒有結束,C++不會讓你由hpp找到cpp中的定義程式碼的。

例項化

為了能讓編譯A.cpp時,從模板空間遷移到普通空間,我們必須為其提供明確的型別。

比如在blocking_queue.cpp的結尾,你應該新增以下程式碼:

template class BlockingQueue<Batch<float>*>;
template class BlockingQueue<Batch<double>*>;
template class BlockingQueue < Datum* > ;
template class BlockingQueue < boost::shared_ptr<QueuePair> > ;

這四行程式碼枚舉了BlockingQueue中可能出現的所有具體型別,此時編譯器才會對A.cpp進行完整的編譯。

在common.hpp中的例項化巨集則要簡單的多,

#define INSTANTIATE_CLASS(classname) \
  template class classname<float>; \
  template class classname<double>

該巨集用於Blob、Layer、Net和Solver四大資料結構,因為它們的型別,除了float,就是double。

特殊化

模板機制中存在模板特殊化的概念,它在功能上等效於例項化。

模板特殊化在math_functions.cpp中將會大量存在。

比如此函式:

template<>
void dragon_cpu_gemm<double>(const CBLAS_TRANSPOSE transA, const CBLAS_TRANSPOSE transB,
    const int M, const int N, const int K, const double alpha, const double* A, const double* B,
    const double beta, double *C){
    int lda = (transA == CblasNoTrans) ? K : M;
    int ldb = (transB == CblasNoTrans) ? N : K;
    cblas_dgemm(CblasRowMajor, transA, transB, M, N, K, alpha, A, lda, B, ldb, beta, C, N);
}

注意例項化與特殊化template附近的區別,特殊化需要新增<>。

模板特殊化必須要明確給出指定型別的程式碼,而例項化則不必給出。

模板例項化本質是模板特殊化的特例,條件是:所有型別,執行相同的程式碼。

而這份相同的程式碼,以下述形式給出:

template<typename T>
XXX<T>::Y(){
    ......
    ......
}

你可以將例項化視為宣告,特殊化視為定義。

兩者給出其一,就能讓編譯器完整編譯分離的模板定義程式碼,前提是,必須寫在cpp檔案中。

CUDA與NVCC編譯器

NVCC編譯cu檔案時,會無視A.cpp裡的任何例項化、特殊化程式碼。

Caffe中給出的解決方案是,追加對cu檔案中函式的特別例項化。

由以下幾個巨集實現:

#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)

更多參考

完整程式碼

blocking_queue.hpp

blocking_queue.cpp

相關推薦

開始山寨Caffe·IO系統()

你說你學過作業系統這門課?寫個無Bug的生產者和消費者模型試試!                               ——你真的學好了作業系統這門課嘛? 在第壹章,展示過這樣圖: 其中,左半部分構成了新版Caffe最惱人、最龐大的IO系統。 也是歷來最不重視的一部分。 第伍章又對左半

開始山寨Caffe·捌IO系統(二)

生產者 雙緩衝組與訊號量機制 在第陸章中提到了,如何模擬,以及取代根本不存的Q.full()函式。 其本質是:除了為生產者提供一個成品緩衝佇列,還提供一個零件緩衝佇列。 當我們從外部給定了固定容量的零件之後,生產者的產能就受到了限制。 由兩個阻塞佇列組成的QueuePair,並不是Caffe的獨創,

開始山寨Caffe·拾IO系統(三)

資料變形 IO(二)中,我們已經將原始資料緩衝至Datum,Datum又存入了生產者緩衝區,不過,這離消費,還早得很呢。 在消費(使用)之前,最重要的一步,就是資料變形。 ImageNet ImageNet提供的資料相當Raw,不僅影象尺寸不一,ROI焦點內容比例也不一,如圖: [Krizhev

開始山寨Caffe·貳主存模型

本文轉自:https://www.cnblogs.com/neopenx/p/5190282.html 從硬體說起 物理之觴 大部分Caffe原始碼解讀都喜歡跳過這部分,我不知道他們是什麼心態,因為這恰恰是最重要的一部分。 記憶體的管理不擅,不僅會導致程式的立即崩潰,還會導致記憶體的

開始山寨Caffe·柒KV資料庫

你說你會關係資料庫?你說你會Hadoop? 忘掉它們吧,我們既不需要網路支援,也不需要複雜關係模式,只要讀寫夠快就行。                                         ——論資料儲存的本質 淺析資料庫技術 記憶體資料庫——STL的map容器 關係資料庫橫行已久,似乎大

開始山寨Caffe·玖BlobFlow

聽說Google出了TensorFlow,那麼Caffe應該叫什麼?                           ——BlobFlow 神經網路時代的傳播資料結構 我的程式碼 我最早手寫神經網路的時候,Flow結構是這樣的: struct Data { vector<d

開始山寨Caffe·伍Protocol Buffer簡易指南

你為Class外訪問private物件而苦惱嘛?你為設計序列化格式而頭疼嘛?                             ——歡迎體驗Google Protocol Buffer 面向物件之封裝性 歷史遺留問題 面向物件中最矛盾的一個特性,就是“封裝性”。 在上古時期,大牛們無聊地設計了

開始山寨Caffe·拾貳IO系統(四)

消費者 回憶:生產者提供產品的介面 在第捌章,IO系統(二)中,生產者DataReader提供了外部消費介面: class DataReader { public: ......... BlockingQueue<Datum*>& free() const

開始caffe(七)利用GoogleNet實現影象識別

一、準備模型 在這裡,我們利用已經訓練好的Googlenet進行物體影象的識別,進入Googlenet的GitHub地址,進入models資料夾,選擇Googlenet 點選Googlenet的模型下載地址下載該模型到電腦中。 模型結構 在這裡,我們利用之前講

開始caffe(十)caffe中snashop的使用

在caffe的訓練期間,我們有時候會遇到一些不可控的以外導致訓練停止(如停電、裝置故障燈),我們就不得不重新開始訓練,這對於一些大型專案而言是非常致命的。在這裡,我們介紹一些caffe中的snashop。利用snashop我們就可以實現訓練的繼續進行。 在之前我們訓練得到的檔案中,我們發現

開始caffe(九)在Windows下實現影象識別

本系列文章主要介紹了在win10系統下caffe的安裝編譯,運用CPU和GPU完成簡單的小專案,文章之間具有一定延續性。 step1:準備資料集 資料集是進行深度學習的第一步,在這裡我們從以下五個連結中下載所需要的資料集: animal flower plane hou

開始caffe(八)Caffe在Windows環境下GPU版本的安裝

之前我們已經安裝過caffe的CPU版本,但是在MNIST手寫數字識別中,我們發現caffe的CPU版本執行速度較慢,訓練效率不高。因此,在這裡我們安裝了caffe的GPU版本,並使用GPU版本的caffe同樣對手寫MNIST數字集進行訓練。 step1: 安裝CUDA

開始caffe(四)mnist手寫數字識別網路結構模型和超引數檔案的原始碼閱讀

下面為網路結構模型 %網路結構模型 name: "LeNet" #網路的名字"LeNet" layer { #定義一個層 name: "mnist" #層的名字"mnist" type:

開始caffe(二)caffe在win10下的安裝編譯

環境要求 作業系統:64位windows10 編譯環境:Visual Studio 2013 Ultimate版本 安裝流程 step1:檔案的下載 從GitHub新增連結描述中下載Windows版本的caffe,並進行解壓到電腦中。 step2:檔案修改 將壓縮包

開始系列-Caffe入門到精通之一 環境搭建

python 資源暫時不可用 強制 rec htm color 查看 cpu blog 先介紹下電腦軟硬件情況吧: 處理器:Intel? Core? i5-2450M CPU @ 2.50GHz × 4 內存:4G 操作系統:Ubuntu Kylin(優麒麟) 16.04

Redis開始學習教程三key值的有效期

圖片 com edi 數據 key值 一次 時間 inf 系統 Redis 是一種存儲系統,類似數據庫,和緩存的差別是,緩存有有效期,而Redis默認無有效期,或者說,默認有效期為永久 但是Redis可以當做緩存使用。這時候需要針對各個key設置有效期。 有效期單位默認為S

【視訊】Kubernetes1.12開始(六)程式碼編譯到自動部署

作者: 李佶澳   轉載請保留:原文地址   釋出時間:2018/11/10 16:14:00 說明 kubefromscratch-ansible和kubefromscratch介紹 使用前準備

開始理解caffe網路的引數

LeNet網路介紹 LeNet網路詳解 網路名稱 name: "LeNet" # 網路(NET)名稱為LeNet mnist層-train layer {

開始學習Servlet(1) 作用和生命週期

Servlet 作用 Servlet 是實現了 javax.servlet.Servlet 介面的 Java 類, 負責處理客戶端的 HTTP 請求。是客戶端 與 資料庫或後臺應用程式之間互動的媒介 。功能: 1. 讀取客戶端傳送的資料 2. 處理

ubuntu 14.04 開始安裝caffe

一、前言 很多人不太喜歡看官方教程,但其實 caffe 的官方安裝指導做的非常好。我在看到 2) 之前,曾根據官方指導在 OSX 10.9, 10.10, Ubuntu 12.04, 14.04 下安裝過 10 多次不同版本的 caffe,都成功了。 本文有不少內容參考了 1)和 2),但又有一些內容