1. 程式人生 > >從零開始山寨Caffe·柒:KV資料庫

從零開始山寨Caffe·柒:KV資料庫

你說你會關係資料庫?你說你會Hadoop?

忘掉它們吧,我們既不需要網路支援,也不需要複雜關係模式,只要讀寫夠快就行。

                                        ——論資料儲存的本質

淺析資料庫技術

記憶體資料庫——STL的map容器

關係資料庫橫行已久,似乎大家已經忘了早些年那些簡陋的資料儲存模式。

在ACM選手中,流傳著“手艹資料庫”的說法,即利用map<string,type>或者map<int,type>,

按照自己編碼規則,將資料暫存起來,等待呼叫。

這就是KV資料庫,最簡陋的資料庫,也是最實用的資料庫。

STL的map容器,底層實現由紅黑樹完成,訪問複雜度$O(logn)$,修改複雜度$O(logn)$。

在記憶體中,具有優良的速度,是非常廉價的記憶體資料庫實現方式。

硬碟資料庫——更復雜的B+樹

B樹是經典的多叉搜尋樹,相比於在記憶體中使用的二叉搜尋紅黑樹,在硬碟物理結構上訪問更具有優勢。

現代關係資料庫,底層大部分都是由B+樹實現,由於原始的B樹只支援單鍵,關係資料庫利用複雜的編碼,

由單鍵模擬出了多鍵,在IO效率上,是嚴重的倒退。

應用資料庫更關注複雜的資料關係,但是對於機器學習系統來說,顯然是多餘的。

單機資料庫——暴力、小而輕便

不是所有的資料庫都像Oracle、MySQL、SQL Server、Hadoop一樣,需要遠端技術支援。

實際上,單機資料庫歷來在程式開發中,使用廣泛。

Android開發中,通常會使用SQLite,在後來序列化APP中複雜的資料結構。

對於簡單的桌面程式而言,早期更是有手寫序列化資料儲存格式的習慣,這種習慣至今還在遊戲開發界保留著。

一個龐大的單機遊戲,比如我手裡佔用空間達35G的巫師3,主程式僅僅40M。

龐大的遊戲資源,其本質就是設計者人工設計的單機資料庫,沒什麼稀奇的。

再看Google Protocol Buffer

資料庫需要做的最後一步是儲存,儲存之前必須解決一個問題:如何儲存?

對於一個機器學習系統而言,其內部充斥著大量複雜的資料結構,如何儲存更是一個難題。

這裡大致有兩個方案:

①仿照關係資料庫,將資料結構與資料關係直接儲存。

②將複雜資料結構,編碼成簡單資料結構,間接儲存。

可以說,這兩種方案各有優劣。

對於①來說,優勢是無須後處理,讀取後完整復現資料結構,劣勢是IO緩慢。

對於②來說,優勢是IO飛快,劣勢是IO之前,分別需要解碼和編碼。

從計算機效能角度分析,我們不難發現,這兩種方案是IO與CPU的權衡。

①所需CPU壓力很小,但是在計算系統設計中,IO容易成瓶頸。

②所需CPU壓力很大,可以說是犧牲CPU來救IO。

So,在機器學習系統設計中,究竟是①合適,還是②合適?很難說。

經典機器學習系統可能更傾向①,但深度學習系統顯然毫無爭議地選擇②。

因為複雜計算都被移到了GPU上,CPU淪為了保姆,保姆就要做好本職工作,專心輔助。

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

Protocol Buffer的使用,實際上也是不推薦我們使用①的。

Protocol Buffer所有message結構,都提供了一個核心函式SerializeToString,能夠將任意複雜的資料結構,編碼成單字串。

這就為最暴力的單鍵單值KV資料庫提供了可能,在單鍵單值情況下,IO的速度可以說達到了極致。

KV資料庫

LevelDB

Caffe早期使用的KV資料庫,Jeff Dean出品。從百度的科普文章來看,應當是借用了Jeff大神的Bigtable技術。

LevelDB的設計目標是硬碟資料庫,而不是記憶體資料庫,因而在硬碟IO方面做了不少優化,不得不佩服Jeff大神。

MapReduce(Hadoop)的部分技術似乎也被植入其中,Google宣稱支援十億級別規模的大資料。

LMDB(Lighting Memory DB)

大多數人估計不知道LMDB的全稱,M指的是Memory,顯然這玩意是瞄準了記憶體資料庫方向設計的。

與傳統記憶體資料庫不同,它並不是真正在用實體記憶體,而用的是虛擬記憶體。

虛擬記憶體,又名作業系統分頁檔案,在Linux下,又叫做交換分割槽(Swap分割槽)。

虛擬記憶體的檔案結構是被作業系統優化過的,速度介於普通硬碟介質緩衝檔案(LevelDB)和實體記憶體之間。

得益於此,LMDB的整體IO能力較LevelDB有所提升,似乎國外友人認為LMDB是LevelDB的Killer。

如何選擇?

預設情況下,你應該選擇LMDB而不是LevelDB,這是新版Caffe主導的一個概念。

LMDB對虛擬記憶體(交換分割槽)大小有一定要求,如果你不喜歡設定虛擬記憶體分頁檔案,LevelDB或許是你的選擇。

注意,虛擬記憶體是用你的硬碟(SSD更佳)轉化的空間,和實體記憶體沒有任何關係。

設定虛擬記憶體,需要長期佔用你的寶貴儲存空間,使用前需要三思。

預設情況下,應該保證虛擬記憶體在4G以上,對於ImageNet等更大資料集,則看情況繼續加大。

教程

本教程本著與時俱進和燒硬體的原則,不對LevelDB介面實現,請自行參考Caffe原始碼。

LMDB

體系結構

LMDB的主體分為三個部分,資料庫、遊標、事務。

資料庫為基層,首先必須開啟,根據開啟方式的不同,分為以下兩種操作:

①讀操作:依賴遊標的偏移,獲取資料。

②寫操作:依賴資料介面,填充資料。

LMDB內部提供了四種結構負責:MDB_env、MDB_dbi、MDB_txn,MDB_cursor

Caffe所有程式碼,都是參考自LMDB開發文件,這四個東西講起來是沒有意義的。

程式碼實戰

通用介面

Caffe預設需要相容兩種資料庫,另外LMDB的API實在是比較難用,所以設計一個通用介面是個不錯的主意。

建立db.hpp

class DB{
public:
    enum Mode { NEW, READ, WRITE };
    DB() {}
    virtual ~DB() {}
    virtual void Open(const string& source, Mode mode) = 0;
    virtual void Close() = 0;
    virtual Cursor* NewCursor() = 0;
    virtual Transaction* NewTransaction() = 0;

};

在上圖中,我們發現,無論是Cursor,還是Transaction,工作都需要txn控制代碼。

而txn控制代碼,需要由DB的env建立,可以視為是與DB建立靈魂連結。

所以在邏輯結構上,DB應當包含Cursor與Transaction。

另外,需要注意,對於一個DB而言,可以有多個Cursor和Transaction。

無論是LevelDB,還是LMDB,多個Cursor將變成並行讀,多個Transaction將變成並行寫。

這也是資料庫系統(DBMS)應當提供的核心功能,要不然人人都能寫資料庫系統了。

class Cursor{
public:
    Cursor() {}
    virtual ~Cursor() {}
    virtual void SeekToFirst() = 0;
    virtual void Next() = 0;
    virtual string key() = 0;
    virtual string value() = 0;
    virtual bool valid() = 0;
};

Cursor在嵌入式關係資料庫程式設計中,是經常見到的,如其名“遊標”,負責在資料庫中亂跑。

儘管我們使用的是KV資料庫,但實際上對於深度學習迭代資料過程而言,Key幾乎是沒用的。

大部分情況下,資料都是序列Read。一遍讀完之後,遊標移動到檔案頭,重新再讀。

所以,預設的Cursor並沒有提供按Key讀取的介面,讀者可以自行翻閱LMDB開發文件實現。

序列讀取,核心函式只需要Next和SeekToFirst,以及基於當前遊標下,對Key和Value的訪問介面。

還有一個判斷檔案尾EOF的函式vaild,每次遇到EOF之後,應該呼叫SeekToFirst,讓大俠重新來過。

class Transaction{
public:
    Transaction() {}
    virtual ~Transaction() {}
    virtual void Put(const string& key, const string& val) = 0;
    virtual void Commit() = 0;
};

Transaction相當簡陋,實際上,它只會用資料轉換階段,比如官方原始碼著名的convert_cifar10_data.cpp。

Put介面用於資料灌入,以LMDB為例,Put後首先會被轉移到虛擬記憶體,當最後執行Commit,才封裝成檔案。

LMDB介面

該部分大部分源於LMDB開發文件,不做過多解釋。

建立db_lmdb.hpp

class LMDB :public DB{
public:
    LMDB() :mdb_env(NULL) {}
    virtual ~LMDB() { Close(); }
    virtual void Open(const string& source, Mode mode);
    virtual void Close(){
        if (mdb_env != NULL){
            mdb_dbi_close(mdb_env, mdb_dbi);
            mdb_env_close(mdb_env);
            mdb_env = NULL;
        }
    }
    virtual LMDBCursor* NewCursor();
    virtual LMDBTransaction* NewTransaction();
private:
    MDB_env* mdb_env;
    MDB_dbi  mdb_dbi;
};

 從DB介面派生過來,注意Close之後,需要先釋放dbi,再釋放env。

同時注意,dbi不是指標,是實體。

class LMDBCursor :public Cursor{
public:
    LMDBCursor(MDB_txn *txn, MDB_cursor *cursor) :
        mdb_txn(txn), mdb_cursor(cursor), valid_(false) {SeekToFirst(); }
    virtual ~LMDBCursor(){
        mdb_cursor_close(mdb_cursor);
        mdb_txn_abort(mdb_txn);
    }
    virtual void SeekToFirst(){ Seek(MDB_FIRST); }
    virtual void Next() { Seek(MDB_NEXT); }
    virtual string key(){
        return string((const char*)mdb_key.mv_data, mdb_key.mv_size);
    }
    virtual string value(){
        return string((const char*)mdb_val.mv_data, mdb_val.mv_size);
    }
    virtual bool valid() { return valid_; }
private:
    void Seek(MDB_cursor_op op){
        int mdb_status = mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_val, op);
        if (mdb_status == MDB_NOTFOUND) valid_ = false;
        else{ MDB_CHECK(mdb_status); valid_ = true; }
    }
    MDB_txn* mdb_txn;
    MDB_cursor* mdb_cursor;
    MDB_val mdb_key, mdb_val;
    bool valid_;
};

LMDBCurosr在構造時,需要傳入MDB_txn和MDB_cursor,控制代碼和遊標的初始化都要依賴DB本身。

Key和Value中,mdb_val預設返回的是void*,需要強轉換為char*,再用string封裝。

Seek函式中,檢測是否到達檔案尾EOF,修改vaild狀態。SeekToFirst將在外部被呼叫,重置遊標位置。

解構函式我是看不懂的,官方文件即視感。

class LMDBTransaction : public Transaction{
public:
    LMDBTransaction(MDB_dbi *dbi,MDB_txn *txn):mdb_dbi(dbi), mdb_txn(txn) {}
    virtual void Put(const string& key, const string&val);
    virtual void Commit() { MDB_CHECK(mdb_txn_commit(mdb_txn)); } 
    MDB_dbi* mdb_dbi;
    MDB_txn* mdb_txn;
};

LMDBTransaction同樣需要傳入MDB_txn和MDB_dbi。

實現

建立db_lmdb.cpp

const size_t LMDB_MAP_SIZE = 1099511627776;        //1 TB
void LMDB::Open(const string& source, Mode mode){
    MDB_CHECK(mdb_env_create(&mdb_env));
    MDB_CHECK(mdb_env_set_mapsize(mdb_env, LMDB_MAP_SIZE));
    if (mode == NEW) CHECK_EQ(_mkdir(source.c_str()), 0);
    int flags = 0;
    if (mode == READ) flags = MDB_RDONLY | MDB_NOTLS;
    int rc = mdb_env_open(mdb_env, source.c_str(), flags, 0664);
#ifndef ALLOW_LMDB_NOLOCK
    MDB_CHECK(rc);
#endif
    if (rc == EACCES){
        LOG(INFO) << "Permission denied. Trying with MDB_NOLOCK\n";
        mdb_env_close(mdb_env);
        MDB_CHECK(mdb_env_create(&mdb_env));
        flags |= MDB_NOLOCK;
        MDB_CHECK(mdb_env_open(mdb_env, source.c_str(), flags, 0664));
    }
    else MDB_CHECK(rc);
    LOG(INFO) << "Open lmdb file:" << source;
}

LMDB的Open介面,我覺得是整個Caffe裡面寫的最爛的函式,爛在兩點:

①讓人看不懂的LMDB的Lock鎖

②用了OS相關的API,而且很爛。

先說說Lock鎖,預設是以Lock訪問的,這意味著,一個DB只能被同時開啟一次。

如果要並行開啟,並且包含寫入操作,那麼這樣非常危險,但並不是不可以(NO_LOCK訪問)。

所以,後半部分程式碼整體就在嘗試切換NO_LOCK訪問。如果你嫌麻煩,可以刪掉,預設就用NO_LOCK。

再說這個很爛API函式的mkdir,首先它在Linux和Windows下,寫法略有不同,標頭檔案也不一樣。

其次,mkdir返回值只有倆種:建立失敗和建立成功。實際上我們更需要第三種:目錄是已存在。

很多fresher在玩Caffe的時候,轉化資料都會失敗,被GLOG巨集給Check到:

if (mode == NEW) CHECK_EQ(_mkdir(source.c_str()), 0);

當指定目錄存在時,就會被CHECK到。取消這個CHECK巨集又不妥,不能排除錯誤路徑的情況。

Linux提供opendir檢測目錄是否存在,建議改寫這步;Windows則沒有,不太好辦。

為此,使用第三方庫是個好主意,Boost的filesystem封裝了跨平臺的檔案系統解決方案。

先做include:

#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>

然後做替換:

void LMDB::Open(const string& source, Mode mode){
    ......
    // if (mode == NEW) CHECK_EQ(_mkdir(source.c_str()), 0);
    boost::filesystem::path db_path(source);
    if (!boost::filesystem::exists(db_path)){
        if (mode == READ)
            LOG(FATAL) << "Specified DB path is illegal [Read Operation].";
        if (mode == NEW){
            if (!boost::filesystem::create_directory(db_path))
                LOG(FATAL) << "Specified DB path is illegal [NEW Operation].";
        }
    }else{
        //    delete old dir and create new dir
        if (mode == NEW){
            boost::filesystem::remove_all(db_path);
            boost::filesystem::create_directory(db_path);
        }
    }
    ......
}

這樣,資料庫部分就能擺脫OS的依賴了,感謝Boost庫。

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

env的環境建立,需要指定最大虛擬記憶體緩衝區容量,預設是1TB,這造成了LMDB在Windows的唯一Bug。

NTFS分割槽不允許1TB這種容量存在,所以LMDB預設原始碼在Windows下會提示空間不足。

willyd大神的LMDB專案修正了NTFS分割槽下的問題。

但是修正之後,建立資料時,你還是能看到,臨時檔案佔用了1TB,儘管你的分割槽沒有1TB,不知道是什麼原理。

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

LMDBCursor* LMDB::NewCursor(){
    MDB_txn* txn;
    MDB_cursor* cursor;
    MDB_CHECK(mdb_txn_begin(mdb_env, NULL, MDB_RDONLY, &txn));
    MDB_CHECK(mdb_dbi_open(txn, NULL, 0, &mdb_dbi));
    MDB_CHECK(mdb_cursor_open(txn, mdb_dbi, &cursor));
    return new LMDBCursor(txn, cursor);
}

LMDBTransaction* LMDB::NewTransaction(){
    MDB_txn *txn;
    MDB_CHECK(mdb_txn_begin(mdb_env, NULL, 0, &txn));
    MDB_CHECK(mdb_dbi_open(txn, NULL, 0, &mdb_dbi));
    return new LMDBTransaction(&mdb_dbi, txn);
}

void LMDBTransaction::Put(const string& key, const string& val){
    MDB_val mkey, mval;
    mkey.mv_data = (void*)key.data();
    mkey.mv_size = key.size();
    mval.mv_data = (void*)val.data();
    mval.mv_size = val.size();
    MDB_CHECK(mdb_put(mdb_txn, *mdb_dbi, &mkey, &mval, 0));
}

這些實現幾乎就是套文件,沒什麼需要注意的。

最後建立db.cpp,利用C++的多型性,提供DB的獲取介面:

DB* GetDB(const string& backend){
    if (backend == "leveldb"){
        NOT_IMPLEMENTED;
    }
    if (backend == "lmdb"){
        return new LMDB();
    }
    return new LMDB();
}

直接用基類指標DB,指向LMDB,多型性的經典應用之一。

完整程式碼

db.hpp

db_lmdb.hpp

db.cpp

db_lmdb.cpp

相關推薦

開始山寨Caffe·KV資料庫

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

開始山寨Caffe·貳主存模型

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

開始山寨Caffe·玖BlobFlow

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

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

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

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

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

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

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

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

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

開始山寨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),但又有一些內容