• <sup id="mk476"></sup>
    <dl id="mk476"></dl>
  • <progress id="mk476"><tr id="mk476"></tr></progress>
    <div id="mk476"><tr id="mk476"></tr></div>
    <sup id="mk476"><ins id="mk476"></ins></sup>
  • <progress id="mk476"></progress>
    <div id="mk476"></div>
    <div id="mk476"><tr id="mk476"></tr></div>
  • <div id="mk476"></div>
    <dl id="mk476"><s id="mk476"></s></dl><dl id="mk476"></dl><div id="mk476"></div>
  • <div id="mk476"></div>
    <dl id="mk476"><ins id="mk476"></ins></dl>

    Caffe2源碼解析

    寫在前面

    上一篇文章對Caffe2中的core模塊進行了簡單拆解Caffe2源碼解析之core,本篇給出其它模塊的拆解,目的是大致了解每個模塊的內容和目標,進一步理解Caffe2的整體框架。內容不多,略做整理如下。

    目錄

    • core
    • proto
      • caffe2.proto
      • hsm.proto
      • metanet.proto
    • cuda_rtc
    • db
    • distributed
    • ideep
    • image
    • mkl
    • mobile
    • mpi
    • observers
    • onnx
    • operators
    • opt
    • perfkernels
    • predictor
    • queue
    • sgd
    • transform
    • util
    • python
    • contrib

    core

    參見Caffe2源碼解析之core

    proto

    包含了Caffe2中常用的protobuf定義,非常重要。我們按照所在文件進行介紹

    caffe2.proto

    首先是TensorProto,它表示張量序列化后的結果,包括了張量的維度、數據類型、數據值、名稱、所在設備等信息,如下:

    message TensorProto {
        repeated int64 dims = 1;
        optional DataType data_type = 2 [default = FLOAT];
        repeated float float_data = 3 [packed = true];
        repeated int32 int32_data = 4 [packed = true];
        optional bytes byte_data = 5;
        repeated bytes string_data = 6;
        repeated double double_data = 9 [packed = true];
        repeated int64 int64_data = 10 [packed = true];
        optional string name = 7;
        
        //張量序列化之前所在的設備
        optional DeviceOption device_detail = 8;
        //張量在chunks中的位置
        message Segment {
            required int64 begin = 1;
            required int64 end = 2;
        }
        optional Segment segment = 11;
    }
    

    在core模塊中講到,Caffe2為了支持低精度的模型訓練,設計了qtensor,當時沒有詳細介紹它的本質,實際上qtensor是對原張量進行了歸一化,即減去bias再除以scale,然后對結果進行低精度表示,節省存儲空間。因此在qtensor的序列化結果中,需要對歸一化的參數進行記錄,如下:

    message QTensorProto {
        repeated int64 dims = 1;
        required int32 precision = 2;
        required double scale = 3;
        required double bias = 4;
        required bool is_signed = 5;
        repeated int32 data = 6 [packed = true];
        optional string name = 7;
        optional TensorProto.DataType data_type = 8 [default = INT32];
    }
    

    對于多個Tensor,可以使用TensorProto的復數版本,TensorProtos來存儲。當然這只針對較小的張量,如果張量比較大,建議使用DB存儲。

    對于張量的形狀,也有一個結構來表示,TensorShape。記得在Tensorflow中,對于張量形狀的某些維度,在運行前可能并不是完全知道,因此這里在TensorShape的定義中,會添加參數對未知張量維度做處理。

    message TensorShape {
        repeated int64 dims = 1;
        optional TensorProto.DataType data_type = 2 [default = FLOAT];
        repeated int32 unknown_dims = 3;
        optional bool unknown_shape = 4 [default = false];
        optional string name = 5;
    }
    

    參數用于對操作的描述(詳見下文的OperatorDef定義),一個命名的參數要么包含單個的浮點型、整型或者字符串數據,要么包含上述類型的數組,如下:

    message Argument {
        optional string name = 1;
        optional float f = 2;
        optional int64 i = 3;
        optional bytes s = 4;
        optional NetDef n = 8;
        repeated float floats = 5;
        repeated int64 ints = 6;
        repeated bytes strings = 7;
        repeated NetDef nets = 9;
    }
    

    目前Caffe2支持的設備類型:

    enum DeviceType {
        CPU = 0;
        CUDA = 1;
        MKLDNN = 2;
        OPENGL = 3;
        OPENCL = 4;
        IDEEP = 5;
        HIP = 6;
        COMPILE_TIME_MAX_DEVICE_TYPES = 7;
        ONLY_FOR_TEST = 20901701;
    }
    

    目前Caffe2對于不同設備的描述proto還都是一致的,如果某個設備沒有包含其中的某個字段,那么這個字段將被忽略。

    message DeviceOption {
        optional int32 device_type = 1 [default = 0]; //0 is CPU
        optional int32 cuda_gpu_id = 2;
        optional uint32 random_seed = 3;
        optional string node_name = 4;
        optional int32 numa_node_id = 5;
        repeated string extra_info = 6;
        optional int32 hip_gpu_id = 7;
    }
    

    接下來是操作的定義:

    message OperatorDef {
        repeated string input = 1; //輸入的blob名稱
        repeated string output = 2; //輸出的blob名稱
        optional string name = 3;
        optional string type = 4; //操作的類型,從操作注冊器中創建操作對象時,需要這個信息
        optional string type = 4;
        repeated Argument arg = 5;
        optional DeviceOption device_option = 6; //操作運行所需要的設備
        
        //對于當前操作來說,如果對于指定的運行設備有多個計算引擎,這里可以指定一個具體的實現引擎。如果用戶指定了一個引擎,但這個引擎在Caffe2的二進制包中并不存在,那么使用默認引擎
        optional string engine = 7;
        
        //控制輸入,與Tensorflow中的控制輸入類似,表達運行的先后順序,而不是數據的輸入。它僅在調度時被Net類使用
        repeated string control_input = 8;
        
        //is_gradient_op參數僅在形狀推斷(shape inference,與Tensorflow中類似)時使用,沒有運行時的作用
        optional bool is_gradient_op = 9 [default = false];
        optional string debug_info = 10;
    }
    

    接下來NetDef的定義:

    message NetDef {
        optional string name = 1;
        repeated OperatorDef op = 2;
        optional string type = 3; //network的執行方式,默認是simple
        optional DeviceOption device_option = 5; //整個net上所有操作的設備信息,在這里設置可以避免給每個操作單獨設置
        repeated Argument arg = 6; //參數,包括num_workers,即當圖被并行執行的時候,worker的數量
        
        repeated string external_input = 7;
        repeated string external_output = 8;
    }
    

    Caffe2中也可以像Tensorflow那樣進行迭代計算,它使用了一個結構叫做ExecutionStep,如下:

    message ExecutionStep {
        optional string name = 1;
        
        //ExecutionStep要么可以包含一個substep的集合,要么可以包含一些要運行的network的名稱,但兩者不能同時被設置
        repeated ExecutionStep substep = 2;
        repeated string network = 3;
        
        //當前的迭代需要運行的輪次,substeps和networks需要被順序執行,每次執行被視為一輪迭代
        optional int64 num_iter = 4;
        
        //迭代執行結束的判斷條件
        optional string criteria_network = 5;
        
        //如果這個字段被設置,那么就周期性的執行
        optional int64 run_every_ms = 11;
        
        //對于sub-steps,是順序執行還是并行執行
        optional bool concurrent_substeps = 6;
        
        //一個用來判斷當前執行是否需要終結的標志
        optional string should_stop_blob = 9;
        
        //如果為真,則當前執行僅執行一次,注意僅當should_stop_blob有效時才有效
        optional bool only_once = 10;
        
        //是否為當前執行構建一個子workspace
        optional bool create_workspace = 12;
        
        //子執行的并行度
        optional int32 num_concurrent_instances = 13;
    }
    

    如果說一個ExecutionStep是一次迭代執行,那么Plan就是一個完整的執行計劃,后者包含前者:

    message PlanDef {
        optional string name = 1;
        repeated NetDef netowrk = 2;
        repeated ExecutionStep execution_step = 3;
    }
    

    對于那些內部并不是Tensor的Blob,Caffe2定義了如下的結構:

    message BlobProto {
        optional string name = 1;
        optional string type = 2;
        optional TensorProto tensor = 3;
        optional bytes content = 4;
        optional QTensorProto qtensor = 5;
        optional int32 content_num_chunks = 6;
        optional int32 content_chunk_id = 7;
    }
    

    最后,是對DBReader進行序列化的對象:

    message DBReaderProto {
        optional string name = 1;
        optional string source = 2;
        optional string db_type = 3;
        optional string key = 4;
    }
    

    hsm.proto

    Word2Vec是早年Google提出的一個模型,目的是根據語料庫獲得詞嵌入(embedding)。其中為了提高訓練的速度提出了兩種技術,一種是負采樣(Negative Sampling),另外一種就是Hierarchical Softmax。因此,Caffe2專門設計了一個HSM操作,這個文件里包含的就是與之相關的proto,我們僅給出proto名稱,含義比較顯然:

    message NodeProto;
    message TreeProto;
    message HierarchyProto;
    message PathProto;
    message PathNodeProto;
    

    metanet.proto

    MetaNetDef,顧名思義,包含了NetDef的元數據。其結構如下:

    message MetaNetDef {
        repeated BlobMap blobs = 1;
        repeated NetsMap nets = 2;
        optional ModelInfo modelInfo = 3;
        repeated PlanMap plans = 4;
        repeated StringMap applicationSpecificInfo = 5;
    }
    

    其中,對應的xxMap結構很簡單,都是鍵值對,ModelInfo相對比較復雜,我們看下詳細的定義:

    message ModelInfo {
        optional string project = 1;
        optional string modelClass = 2;
        optional string version = 3;
        optional string predictorTtype = 4;
        optional string modelId = 5;
    }
    

    cuda_rtc

    cuda核生成相關的輔助代碼。

    db

    在Caffe2的執行過程中,需要重復使用和共享的參數,會被記錄在一個db當中。在core模塊中我們介紹過,db就是一個kv存儲,這里包含了4種Caffe2中會用到的db,如下:

    graph TB db-->|派生|LevelDB db-->|派生|LMDB db-->|派生|ProtoDB db-->|派生|ZmqDB

    distributed

    Caffe2的分布式實現,依賴外部存儲來保存共享的參數。常用的外部存儲包括文件和redis。

    外部存儲的句柄用StoreHandler來表示,它包含了以下的核心API:

    class StoreHandler {
      public:
        virtual void set(...) = 0;
        virtual std::string get(...) = 0;
        virtual int64_t add(...) = 0;
        virtual bool check(...) = 0;
        virtual void wait(...) = 0;
    };
    

    對應到計算圖中,就有4個對store操作的op與之對應,如下:

    graph TB Operator-->|派生|StoreSetOp Operator-->|派生|StoreGetOp Operator-->|派生|StoreAddOp Operator-->|派生|StoreWaitOp

    剛才提到了,常用的存儲方式為文件存儲和redis存儲,對應有兩種存儲句柄:

    graph TB StoreHandler-->|派生|RedisStoreHandler StoreHandler-->|派生|FileStoreHandler

    另外,還有兩個創建存儲的操作,如下:

    graph TB Operator-->|派生|FileStoreHandlerCreateOp Operator-->|派生|RedisStoreHandler

    ideep

    目前還不清楚具體含義。

    image

    關于圖像的操作,其中最重要的是對于圖像讀取的操作,ImageInputOp,它繼承自PrefetchOperator,包含了圖像讀取的一系列功能。

    mkl

    MKL全稱是Intel Math Kernel Library,是英特爾提供的數學核心庫,它對大量的數學過程進行了處理器級別的優化。這里包括了MKL相關的操作定義。注意,Tensorflow中也用到了MKL去優化數學運算,只不過它是在圖優化的過程中,將MKL作為一種圖優化遍歷被引入,而Caffe2中將MKL直接融入到了操作內部。

    mobile

    針對移動平臺的特殊處理,具體還沒看。

    mpi

    Caffe2中的分布式計算,通過mpi實現。mpi的核心作用是在不同機器上的分布式進程中,進行數據傳輸和消息同步。針對mpi中的核心操作,比如Broadcast,Reduce等,Caffe2都給出了對應的操作來執行,具體如下:

    graph TB Operator-->|派生|MPICreateCommonWorldOp Operator-->|派生|MPIBroadcastOp Operator-->|派生|MPIReduceOp Operator-->|派生|MPIAllgatherOp Operator-->|派生|MPIAllreduceOp Operator-->|派生|MPISendTensorOp Operator-->|派生|MPIReceiveTensorOp

    observers

    給出了4種不同觀察器的定義,如下:

    • operator_attaching_net_observer,負責給net中的每一個operator添加觀察器;
    • profile_observer,負責對每個操作或整張圖的執行消耗進行觀察;
    • runcnt_observer,負責對每個操作或者整張圖的運行次數進行觀察;
    • time_observer,負責對每個操作或者整張圖的運行時間進行觀察;

    onnx

    目前還不清楚。

    operators

    操作的具體定義放在這里,代碼量巨大,沒來得及細看。

    opt

    優化相關的類和函數,與Tensorflow一樣,Caffe2也是通過對圖遍歷的方式實施優化,所有的優化遍歷類必須繼承自OptimizationPass,它的具體定義如下:

    class OptimizationPass {
      public:
        OptimizationPass(NNModule* nn) : nn_(nn) {}
        virtual void run() = 0;
        virtual ~OptimizationPass(){}
        
      protected:
        NNModule* nn_;
    };
    

    perfkernels

    性能優化相關的kernel。

    predictor

    一個predictor就是一個參數都確定好了的net。在深度學習中,我們通常會把待學習的模型表示為net,然后通過迭代的圖計算,確定模型參數,將net轉換為predictor。下面我們看下predictor的結構:

    class Predictor {
      public:
        Predictor(const NetDef& init_net, const NetDef& run_net, Workspace* parent = nullptr, bool run_init = true, int optimization = 1);
        Predictor(PredictorConfig config);
        
        //以下是對()的重載,給定輸入得到輸出
        bool operator()(const TensorMap& inputs, TensorList* outputs);
        bool operator()(const TensorMap& inputs, TensorList* outputs);
        bool operator()(const TensorMap& inputs, TensorMap* outputs);
        
        const NetDef& def() const {
            return *config_.predict_net;
        };
        
        Workspace* ws(){
            return config_.ws.get();
        };
        const std::vector<std::string>& input_names() const {
            return config_.input_names;
        }
        const std::vector<std::string>& output_names() const {
            return config_.output_names;
        }
      private:
        bool run_map_workspace(const TensorMap& inputs);
        PredictorConfig config_;
    };
    

    其中,Predictor類最重要的一個私有數據成員是config_,我們看下PredictorConfig的定義:

    struct PredictorConfig {
        std::shared_ptr<PredictorParameters> parameters;
        std::shared_ptr<NetDef> predict_net;
        std::vector<std::string> input_names;
        std::vector<std::string> output_names;
        std::vector<std::string> parameter_names;
        std::shared_ptr<Workspace> ws;
    };
    

    queue

    與Tensorflow類似,Caffe2也利用隊列對多個線程進行同步,比如在多線程讀取輸入數據的時候。對隊列的所有動作都必須通過“操作”來完成,因此Caffe2又定義了隊列相關的操作。

    先來看下BlobsQueue的定義:

    class BlobsQueue : public std::enable_shared_from_this<BlobsQueue> {
      public:
        bool blockingRead(...);
        bool blockingWrite(...);
        void close();
      private:
        size_t numBlobs_;
        std::mutex mutex_;
        std::condition_variable cv_;
        int64_t reader_{0};
        int64_t writer_{0};
        std::vector<std::vector<Blob*>> queue_; //核心隊列數據
        const std::string name_;
    };
    

    注意看其中的數據元素queue_,它就是BlobsQueue的核心隊列數據。

    另外,BlobsQueue,也可以被看做是一種db,因此Caffe2定義了BlobQueueDB:

    class BlobsQueueDB : public DB {
      public:
        BlobsQueueDB(...);
        void Close() override {}
        unique_ptr<Cursor> NetCursor() override{...}
        unique_ptr<Transaction> NewTransaction() override {...}
      private:
        std::shared_ptr<BlobsQueue> queue_;
        int key_blob_index_;
        int value_blob_index_;
        float timeout_secs_;
    };
    

    另外,Caffe2還針對BlobsQueue提出了提出了對隊列進行處理的“操作”,把常用的隊列處理方式,如入隊、出隊等,抽象為操作:

    graph TB Operator-->|派生|CreateBlobsQueueOp Operator-->|派生|EnqueueBlobsOp Operator-->|派生|DequeueBlobsOp Operator-->|派生|CloseBlobsQueueOp Operator-->|派生|SafeEnqueueBlobsOp Operator-->|派生|SafeDequeueBlobsOp Operator-->|派生|WeightedSampleDequeueBlobsOp

    另外,為了能支持一次多數據入隊,Caffe2設計了RebatchingQueue類,它的簡要結構如下:

    class RebatchingQueue {
      public:
        bool enqueueOne(...);
        bool enqueueMany(...);
        bool dequeue(...);
      private:
        std::vector<std::vector<TensorCPU>> queue_;
    };
    

    與BlobsQueue最大的區別有兩點,第一,核心數據queue_中存儲的是TensorCPU而不是Blob*,第二,擁有EnqueueOne和EnqueueMany兩種入隊操作。

    與BlobsQueue類似,Caffe2也為RebatchingQueue準備了對其進行處理的“操作”,與BlobsQueue類似,這里不再贅述。

    sgd

    包含了與隨機梯度下降有關的操作。基本上可以根據文件名猜測含義,這里僅列出文件名前綴,感興趣的讀者可以查閱源碼:

    adadelta_op
    adagrad_op
    adam_op
    clip_tensor_op
    fp16_momentum_sgd_op
    fp32_momentum_sgd_op
    ftrl_op
    gftrl_op
    iter_op
    lars_op
    learning_rate_adaption_op
    learning_rate_functors
    learning_rate_op
    momentum_sgd_op
    rmsprop_op
    wngrad_op
    yellowfin_op
    

    有機會可以仔細研讀下其中的細節。

    transform

    根據core模塊的內容我們知道,這里包含的是對圖進行變換的方法。主要包括4種:

    //公共子項消除,CSE,與Tensorflow類似
    common_subexpression_elimination
    
    //對卷積操作進行變換,提高效率
    conv_to_nnpack_transform
    
    //模式替換,允許你使用簡單的接口定義模式替換規則,只需定義一模式子圖和一個替換子圖,在原圖中尋找模式子圖,然后替換為替換子圖即可
    pattern_net_transform
    
    //單個操作的原地轉換
    single_op_transform
    

    這些類形成了如下的繼承體系:

    graph TB Transform-->|派生|CommonSubexpressionEliminationTransform Transform-->|派生|SingleOpTransform Transform-->|派生|PatternNetTransform SingleOpTransform-->|派生|ConvToNNPackTransform

    util

    應用類和函數,比較瑣碎,暫時沒有細看。

    python

    通過前面的介紹我們了解到,Caffe2的核心代碼是用"C++"實現的,為了方便在python中進行調用,需要一個工具,幫助python調用"C++"代碼。這樣的工具有很多,比如boost.python, swig,ctypes,pybind11等。Caffe2選擇了pybind11,因為它對"C++"11支持的比較好,而且API比較簡單。而Tensorflow中python前端調用"C++"后端使用的是swig,其實swig對"C++"11也能支持。兩種設計選擇的優劣目前的知識我們還不好評判。

    具體的接口文件,是_import_c_extention.py,它首先會嘗試載入gpu版本的Caffe2后端,如果失敗了,會嘗試載入CPU版本。其中,對于CPU后端的導入是通過如下的語句:

    from caffe2.python.caffe2_pybind11_state import *
    

    因此,在編譯完成后,caffe2/python目錄下會生成一個名為caffe2_pybind11_state.so的文件,是包含了Caffe2的"C++"后端的動態鏈接庫,可以被python載入。

    contrib

    同Tensorflow的contrib文件夾一樣,包含了第三方貢獻的、未正式加入Caffe2的模塊,這里面大部分代碼是用python開發的。隨著版本迭代,經測試穩定后,這些模塊會逐漸加入Caffe2的python模塊。

    寫在后面

    看過Tensorflow和Caffe2的核心代碼之后,講一講自己的感受。

    • 代碼模塊性,Tensorflow代碼的模塊性做的非常好,基礎框架、運行時、圖表示、圖優化、op、kernel都區分的清清楚楚,而Caffe2的代碼顯得有些混雜,操作到處都是,給代碼閱讀帶來了一點障礙。
    • 代碼規范性,Tensorflow代碼的規范性要好很多,雖然核心代碼是多個作者完成的,但代碼風格非常統一,文件頭的協議也非常一致。反觀Caffe2的代碼,協議混亂,代碼風格不統一,東拼西湊的感覺比較強烈,代碼在形式上的美感不足。
    • 架構合理性,Tensorflow的野心很大,它的終極目標是變成一個全新的、面向數據流圖計算的編程語言。這種編程語言基于op原語,利用op和kernel將編譯期和運行期明確的區分開來,同時,它對于同一個數據流圖的多線程并行執行機制,也像極了CPU流水線處理機制,因此,應該說,深度神經網絡只是Tensorflow的一個副產品,它的真實價值遠不止于此。反觀Caffe2,很多設計有些短視了(比如用redis為中介做分布式執行),在提供更多靈活性的同時,也限制了它的高度。

    當然,以上只是個人的一些猜測,隨著理解的深入,我也會及時回來修正自己的觀點,也歡迎大家來討論。

    最后,我在github上新建了一個repo,pytorch_notes,歡迎大家點星星。

    posted on 2018-09-22 23:33 jicanghai 閱讀(...) 評論(...) 編輯 收藏

    導航

    公告

    統計

    江苏11选5软件