Caffe2原始碼解析之core
寫在前面
在對Tensorflow的後端原始碼進行了拆解(參見tensorflow原始碼解析系列文章索引)之後,很想跟其它深度學習框架的實現進行對比,根據框架的流行程度,先選擇了Pytorch。Pytorch的後端核心是直接複用了Caffe2,因此本文針對Caffe2原始碼的core模組進行了簡單拆解。
目錄
- 資料儲存與表示
- storage
- tensor
- blob
- qtensor
- 操作
- observer observable
- operator
- 操作求導
- operator_schema
- context
- 計算圖
- graph
- net
- transform
- 執行時
- allocator
- db
- registry
- module
- scope_guard
- workspace
- init
1. 資料儲存與表示
1.1 storage
Caffe2中對資料儲存的最底層的描述是Storage,它實際上是指向StorageImpl的共享指標,後者包含資料型別、資料指標、容量、資料所在裝置等資訊。Storage的定義如下:
using Storage = std::shared_ptr<StorageImpl>; class StorageImpl { public: //... protected: using DataPtr = std::shared_ptr<void>; int64_t capacity_ = 0; DataType data_type_; DataPtr data_ptr_; DeviceType device_type_ = CPU; };
1.2 tensor
Caffe2中的資料統一使用Tensor表示,Tensor由TensorImpl實現,後者包含一個Storage。
graph LR
Tensor-->|包含|TensorImpl
TensorImpl-->|包含|Storage
Storage-->|指向|StorageImpl
TensorImpl的定義如下:
class TensorImpl { public: //... protected: using DimVector = std::vector<TIndex>; DimVector dims_; //張量的維度 TIndex size_ = -1; //張量中包含的元素數量 Storage storage_; //底層儲存 };
Tensor並非繼承自TensorImpl,而是在內部包含了一個指向TensorImpl的指標,如下:
class Tensor final {
protected:
using TensorImplPtr = c10::intrusive_ptr<TensorImpl, UndefinedTensorImpl>;
TensorImplPtr impl_;
//...
};
對Tensor的方法呼叫,通過重定向給TensorImpl實現。
1.3 blob
Blob是一個容器,包含了一個指標和這個指標指向記憶體的資料型別,在Caffe2中,大部分情況下Blob都包含一個指向Tensor的指標。
class Blob {
public:
//...
private:
TypeMeta meta_;
void* pointer_ = nullptr;
DestroyCall destroy_ = nullptr;
};
為了方便對Blob進行傳輸,定義了其序列化和反序列化的類,分別是BlobSerializerBase和BlobDeserializerBase,以及對應的為Tensor準備的序列化和反序列化類。
graph TB
BlobSerializerBase-->|派生|TensorSerializer
BlobDeserializerBase-->|派生|TensorDeserializer
1.4 qtensor
低精度的張量,為了便於快速進行低精度的整數乘法計算。具體的做法是,用更低的位數來表示整數,比如,用3個bit表示無符號整數,用4個bit表示有符號整數。低精度張量可以在略微損失模型精度的情況下,大大降低計算複雜度和儲存空間大小。
操作
2.1 Observer Observable
Caffe2使用ObserverBase和Observable兩個類實現了觀察者模式。ObserverBase是基礎觀察器,使用者可以通過繼承此類建立新的觀察器,而Observable是可被觀察屬性,使用者可以通過繼承此類獲得可觀察屬性。
ObserverBase提供了觀察器的統一介面,比較簡單:
class ObserverBase {
public:
virtual void Start() {}
virtual void Stop() {}
T* subject() const {
return subject_;
}
protected:
T* subject_;
};
其中,subject_表示被觀察物件的指標。
Observable封裝了可被觀察屬性,內部包含了一個觀察器的列表,結構如下:
class Observable {
public:
using Observer = ObserverBase<T>;
const Observer* AttachObserver(std::unique_ptr<Observer> observer){} //新增觀察器
std::unique_ptr<Observer> DetachObserver(const Observer* observer_ptr){} //解除觀察器
virtual size_t NumObservers() {
return num_observers_;
} //觀察器的數量
void StartAllObservers(){} //啟動所有觀察器
void StopAllObservers(){} //關閉所有觀察器
private:
Observer* observer_cache_;
size_t num_observers_ = 0;
protected:
std::vector<std::unique_ptr<Observer>> observer_list_; //觀察器列表
};
2.2 Operator
Operator代表操作的具體實現,相當於Tensorflow中的kernel。Operator繼承自OperatorBase,而後者繼承自Observable,所以在Caffe2中,“操作”本質上是一個可觀察的物件。
graph LR
Observable-->|派生|OperatorBase
OperatorBase-->|派生|Operator
OperatorBase類包含了操作需要的基本資料元素和介面:
class OperatorBase {
private:
Workspace* operator_ws_;
std::shared_ptr<const OperatorDef> operator_def_;
DeviceOption device_option_;
std::string engine_;
std::string type_;
vector<const Blob*> inputs_;
vector<Blob*> outputs_;
};
OperatorBase中包含了輸入和輸出的記憶體指標,可見,在Caffe2中,Operator本質上是一個執行時的物件,這與Tensorflow中Op的設計理念不同,在Tensorflow中,Op是一個編譯時物件,僅規定了操作的型別和目標,並不包含具體資料,具體的計算實際上是通過Kernel完成的。
Operator繼承自OperatorBase類:
class Operator : public OperatorBase {
public:
bool Run(int stream_id = 0) final {...}
bool RunAsync(int stream_id = 0) final {...}
virtual bool RunOnDevice() = 0;
};
實際上,Run和RunAsync最終都呼叫了RunOnDevice,完成實際的計算。
如果我們需要使用一些c10中定義的操作,需要將其轉換為在Caffe2中可以呼叫的操作,可以通過如下的巨集進行轉換:
REGISTER_C10_OPERATOR_FOR_CAFFE2_DISPATCH(C10Add, C2MyAddOpName)
上述例子中,我們把一個C10Add操作,包裝成C2MyAddOpName操作,供我們使用。為了實現這個功能,Caffe2還提供了一個包裝類,C10OperatorWrapper。
2.3 操作求導
為了對操作求導,Caffe2推出了一個導數操作生成類,GradientMakerBase,方便使用者定義對於某個操作的導數。類包含的資料成員如下:
//為密集和稀疏的blob提供統一的介面
struct GradientWrapper {
string dense_;
string indices_;
string values_;
inline bool IsDense(){}
inline bool IsSparse(){}
inline bool IsEmpty(){}
};
class GradientMakerBase {
protected:
const OperatorDef& def_;
const vector<GradientWrapper>& g_output_;
vector<GradientWrapper> g_input_;
};
可見,GradientMakerBase僅提供了輸入輸出,以及原操作。使用者可以根據原操作,定製導數。
2.3 operator_schema
OpSchema是對操作的靜態描述,相當於Tensorflow中的Op,包含的資訊如下:
class OpSchema {
private:
string type_;
string file_;
string doc_;
string onnx_schema_;
std::vector<Argument> args_{};
std::vector<std::pair<const char*, const char*>> input_desc_{};
std::vector<std::pair<const char*, const char*>> output_desc_{};
int line_ = 0;
int min_input_ = 0;
int max_input_ = std::numeric_limits<int>::max();
int min_output_ = 0;
int max_output_ = std::numeric_limits<int>::max();
bool private_ = false;
bool inputs_can_cross_devices_ = false;
std::function<bool(int)> num_inputs_allowed = [](int) { return true; }
std::function<bool(int)> num_outputs_allowed = [](int) { return true; }
std::function<bool(int,int)> num_inputs_outputs_allowed_ = [](int,int) { return true; }
std::function<int(int)> calculate_output_;
std::function<bool(int,int)> inplace_allowed_ = [](int,int){}
std::function<bool(int,int)> inplace_enforced_ = [](int,int){}
TensorInferenceFunctionType tensor_inference_function_ = {...}
std::unique_ptr<CostInferenceFunctionType> cost_inference_function_ = nullptr;
DeviceInferenceFunctionType device_inference_function_ = {...}
};
另外Caffe2也提供了一個對於OpSchema的註冊類OpSchemaRegistry,如下:
class OpSchemaRegistry {
private:
static CaffeMap<string, OpSchema>& map();
};
2.4 context
Caffe2中的context,其實就是Tensorflow中的OpKernelContext,為操作的實際計算提供通用的支援,主要包含記憶體拷貝的介面。所有實際的Context類必須繼承自BaseContext,而Caffe2為我們準備了一個標準的Context介面,CPUContext類。另外,也同樣為GPU準備了一個CUDAContext類。
graph LR
BaseContext-->|派生|CPUContext
BaseContext-->|派生|CUDAContext
3. 計算圖
3.1 graph
Graph表示圖的結構,圖包含節點,節點包含操作。
graph LR
Graph-->|包含|Node
Node-->|包含|OperatorDef
Node包含的資料成員:
class Node {
public:
OperatorDef op;
bool active = true; //操作是否被transformation刪除
std::map<int, std::vector<string>> parents;
std::vector<int, std::vector<string>> children;
}
Graph包含的私有資料成員:
class Graph {
private:
NetDef netdef_;
std::set<string> external_input_;
std::set<string> external_output_;
std::vector<Node> nodes_;
}
3.2 net
Net是一個可執行的Graph,包含了一個圖的所有“操作”,以及它們的上下文。它繼承自Observable,本質上是一個可觀察的物件。資料成員如下:
class NetBase : public Observable<NetBase>{
public:
virtual bool Run(){...}
virtual bool RunAsync();
protected:
vector<string> external_input_;
vector<string> external_output_;
string name_;
vector<const Event*> events_;
std::shared_ptr<const NetDef> net_def_;
};
NetBase派生出了三種子類,第一種是AsyncNetBase,它包含了非同步執行網路所必須的資料和介面:
class AsyncNetBase : public NetBase {
public:
bool RunAsync() override;
protected:
bool canSchedule(...);
std::vector<OperatorBase*> operators_;
std::vector<dag_utils::OperatorNode> operator_nodes_;
std::vector<std::vector<int>> chains_;
std::vector<dag_utils::OpGraphNode> chain_nodes_;
dag_utils::ExecutionChains execution_chains_;
};
第二種是SimpleNet,它表示了一種對圖的單執行緒的順序執行模式。 第三種是DAGNetBase,它表示了一種對圖的多執行緒的dag執行模式。 相關的net類形成了一個繼承體系:
graph TB
Observable-->|派生|NetBase
NetBase-->|派生|AsyncNetBase
AsyncNetBase-->|派生|AsyncSchedulingNet
NetBase-->|派生|DAGNetBase
DAGNetBase-->|派生|DAGNet
NetBase-->|派生|SimpleNet
DAGNetBase-->|派生|AsyncDAGNet
AsyncNetBase-->|派生|AsyncPollingNet
3.3 transform
transform是一種針對Caffe2的NetDef結構的操作,它將NetDef作為輸入,輸出新的經過變換的NetDef。它的工作步驟包括:
- 從舊的NetDef中構建一張圖,這張圖中儲存了節點的連線資訊;
- 在圖中匹配指定的模式,找到它想要更改的子圖;
- 用新的操作替換匹配到的子圖;
- 根據圖構建一個新的NetDef並返回;
Transform功能的實現,依賴於三個功能函式,如下:
- PatternRule(模式規則),它決定了對於一張子圖和一個節點,是否可以將這個節點加入這個子圖中;
- ValidatorRule(驗證規則),它決定了一張子圖是否是匹配的;
- ReplaceRule(替換規則),它對一張匹配的子圖進行替換;
常用的模式如下:
- CONNECTED_SUBGRAPH,連線子圖,它只能匹配連線的子圖。比如對於圖(1)-->(2)-->(3)-->(4),它能夠匹配到[2,3]和[4,3],但不能匹配到[2,4];
- SORTED_WRT_EXECUTION_ORDER,執行序模式,它只能匹配符合執行順序的子圖,節點之間不一定需要有連線,它比General模式要快,例如對於圖(1)-->(2)-->(3)-->(4),它可以匹配到[2,4],[3,4],但不能匹配到[3,1],[4,3];
- GENERAL,它可以匹配到任何子圖,比如,對於圖(1)-->(2)-->(3)-->(4)來說,它可以匹配到子圖[2,4],[3,4],[4,2,1]等;
4. 執行時
4.1 allocator
記憶體分配器,包含了CPU和GPU兩種。
graph TB
CPUAllocator-->|派生|DefaultCPUAllocator
CPUAllocator-->|派生|PinnedCPUAllocator
4.2 db
DB類是對kv儲存的抽象。包含了用於讀取DB資料的Cursor類,用於寫DB資料的Transaction類,DB讀取的包裹類DBReader,對DBReader進行序列化和反序列化的DBReaderSerializer和DBReaderDeserializer類。
graph TB
DB-->|讀資料時的遊標類|Cursor
DB-->|寫資料時的事務類|Transaction
DB-->|讀資料包裝|DBReader
DBReader-->|序列化|DBReaderSerilizer
DBReader-->|反序列化|DBReaderDeserilizer
4.3 registry
註冊類,key為字串,value可以為任意的類。結構如下:
class Registry {
private:
CaffeMap<SrcType, Creator> registry_;
CaffeMap<SrcType, string> help_message_;
};
4.4 module
檢視Caffe2已載入的模組,以及載入指定模組。模組指的是動態連結庫。
4.5 scope_guard
是“初始化即資源獲取”原語的實現,它保證了,如果不顯式說明,函式的執行就會離開當前的scope。
4.6 workspace
Workspace包含了所有的執行時物件,包括blob和net,它是所有這些物件的擁有者,負責對這些物件進行管理。
class Workspace {
private:
typedef CaffeMap<string, unique_ptr<Blob>> BlobMap;
BlobMap blob_map_;
typedef CaffeMap<string, unique_ptr<NetBase>> NetMap;
NetMap net_map_;
const string root_folder_;
const Workspace* shared_;
std::unordered_map<string, std::pair<const Workspace*, string>> forwarded_blobs_;
std::unique_ptr<ThreadPool> thread_pool_;
std::mutex thread_pool_creation_mutex_;
std::shared_ptr<Bookkeeper> bookkeeper_;
};
4.7 init
初始化整個Caffe2的執行環境,執行機制是,把需要在環境初始化中執行的函式註冊到註冊器中,初始化時,會在不同時期執行不同註冊器中的函式。核心的函式如下:
CAFFE2_API bool GlobalInit(int* pargc, char*** argv);
整個初始化過程分為三步:
- 先執行通過REGISTER_CAFFE2_EARLY_INIT_FUNCTION註冊的函式;
- 再解析Caffe的命令列引數,並啟動日誌記錄系統;
- 最後執行通過REGISTER_CAFFE2_INIT_FUNCTION註冊的函式;