從零開始山寨Caffe·貳:主存模型
本文轉自:https://www.cnblogs.com/neopenx/p/5190282.html
從硬體說起
物理之觴
大部分Caffe原始碼解讀都喜歡跳過這部分,我不知道他們是什麼心態,因為這恰恰是最重要的一部分。
記憶體的管理不擅,不僅會導致程式的立即崩潰,還會導致記憶體的洩露,當然,這隻針對傳統CPU程式而言。
由於GPU的引入,我們需要同時操縱倆種不同的儲存體:
一個受北橋控制,與CPU之間架起地址匯流排、控制匯流排、資料匯流排。
一個受南橋控制,與CPU之間僅僅是一條可憐的PCI匯流排。
一個傳統的C++程式,在作業系統中,會被裝載至記憶體空間上。
一個有趣的問題,你覺得CPU能夠訪問視訊記憶體空間嘛?你覺得你的預設C++程式碼能訪問視訊記憶體空間嘛?
結果顯然是否定的,問題就在於CPU和GPU之間只存在一條資料匯流排。
沒有地址匯流排和控制匯流排,你除了讓CPU傳送資料拷貝指令外,別無其它用處。
這不是NVIDIA解決不了,AMD就能解決的問題。除非計算機體系結構再一次迎來變革,
AMD和NVIDIA的工程師聯名要求在CPU和GPU之間追加複雜的通訊匯流排用於異構程式設計。
當然,你基本是想多了。
環境之艱
可憐的資料匯流排,加大了異構程式設計的難度。
於是我們看到,GPU的很大一部分時鐘週期,用在了和CPU互相交換資料。
也就是所謂的“記憶體與視訊記憶體之間友好♂關係”。
你不得不接受一個事實:
GPU最慢的儲存體,也就是片外視訊記憶體,得益於鎂光的GDDR技術,目前家用遊戲顯示卡的訪存速度也有150GB/S。
而我們可憐的記憶體呢,你以為配上Skylake後,DDR4已經很了不起了,實際上它只有可憐的48GB/S。
那麼問題來了,記憶體如何去彌補與視訊記憶體的之間頻寬的差距?
答案很簡單:分時、非同步、多執行緒。
換言之,如果GPU需要在接下來1秒內,獲得CPU的150GB資料,那麼CPU顯然不能提前一秒去複製。
它需要提前3秒、甚至4秒。如果它當前還有其它序列任務,你就不得不設個執行緒去完成它。
這就是新版Caffe增加的新功能之一:多重預緩衝。
設置於DataLayer的分支執行緒,在GPU計算,CPU空閒期間,為視訊記憶體預先緩衝3~4個Batch的資料量,
來解決記憶體視訊記憶體頻寬不一致,導致的GPU時鐘週期浪費問題,也增加了CPU的利用率。
最終,你還是需要牢記一點:
不要嘗試以預設的C++程式碼去訪問視訊記憶體空間,除非你把它們複製回記憶體空間上。
否則,就是一個毫無提示的程式崩潰問題(準確來說,是被CPU硬體中斷了【微機原理或是計算機組成原理說法】)
程式設計之繁
在傳統的CUDA程式設計裡,我們往往經歷這樣一個步驟:
->計算前
cudaMalloc(....) 【分配視訊記憶體空間】
cudaMemset(....) 【視訊記憶體空間置0】
cudaMemcpy(....) 【將資料從記憶體複製到視訊記憶體】
->計算後
cudaMemcpy(....) 【將資料從視訊記憶體複製回記憶體】
這些步驟相當得繁瑣,你不僅需要反覆敲打,而且如果忘記其中一步,就是毀滅性的災難。
這還僅僅是GPU程式設計,如果考慮到CPU/GPU異構設計,那麼就更麻煩了。
於是,聰明的人類就發明了主存管理自動機,按照按照一定邏輯設計狀態轉移程式碼。
這是Caffe非常重要的部分,稱之為SyncedMemory(同步儲存體)。
主存模型
狀態轉移自動機
自動機共有四種狀態,以列舉型別定義於類SyncedMemory中:
enum SyncedHead { UNINITIALIZED, HEAD_AT_CPU, HEAD_AT_GPU, SYNCED };
這四種狀態基本會被四個應用函式觸發:cpu_data()、gpu_data()、mutable_cpu_data()、mutable_gpu_data()
在它們之上,有四個狀態轉移函式:to_cpu()、to_gpu()、mutable_cpu()、mutable_gpu()
前兩個狀態轉移函式用於未進入Synced狀態之前的狀態機維護,後兩個用於從Synced狀態中打破出來。
具體細節見後文,因為Synced狀態會忽略to_cpu和to_gpu的行為,打破Synced狀態只能靠人工賦值,切換狀態頭head。
後兩個mutable函式會被整合在應用函式裡,因為它們只需要簡單地為head賦個值,沒必要大費周章寫個函式封裝。
★UNINITIALIZED:
UNINITIALIZED狀態很有趣,它的生命週期是所有狀態裡最短的,將隨著CPU或GPU其中的任一個申請記憶體而終結。
在整個記憶體週期裡,我們並非一定要遵循著,資料一定要先申請記憶體,然後在申請視訊記憶體,最後拷貝過去。
實際上,在GPU工作的情況下,大部分主儲存體都是直接申請視訊記憶體的,如除去DataLayer的前向/反向傳播階段。
所以,UNINITIALIZED允許直接由to_gpu()申請視訊記憶體。
由此狀態轉移時,除了需要申請記憶體之外,通常還需要將記憶體置0。
★HEAD_AT_CPU:
該狀態表明最近一次資料的修改,是由CPU觸發的。
注意,它只表明最近一次是由誰修改,而不是誰訪問。
在GPU工作時,該狀態將成為所有狀態裡生命週期第二短的,通常自動機都處於SYNCED和HEAD_AT_GPU狀態,
因為大部分資料的修改工作都是GPU觸發的。
該狀態只有三個來源:
I、由UNINITIALIZED轉移到:說白了,就是欽定你作為第一次記憶體的載體。
II、由mutable_cpu_data()強制修改得到:都要準備改資料了,顯然需要重置狀態。
cpu_data()及其子函式to_cpu(),只要不符合I條件,都不可能轉移到改狀態(因為訪問不會引起資料的修改)
★HEAD_AT_GPU:
該狀態表明最近一次資料的修改,是由GPU觸發的。
幾乎是與HEAD_AT_CPU對稱的。
★SYNCED:
最重要的狀態,也是唯一一個非必要的狀態。
單獨設立同步狀態的原因,是為了標記記憶體視訊記憶體的資料一致情況。
由於類SyncedMemory將同時管理兩種主存的指標,
如果遇到HEAD_AT_CPU,卻要訪問視訊記憶體。或是HEAD_AT_GPU,卻要訪問記憶體,那麼理論上,得先進行主存複製。
這個複製操作是可以被優化的,因為如果記憶體和視訊記憶體的資料是一致的,就沒必要來回複製。
所以,使用SYNCED來標記資料一致的情況。
SYNCED只有兩種轉移來源:
I、由HEAD_AT_CPU+to_gpu()轉移到:
含義就是,CPU的資料比GPU新,且需要使用GPU,此時就必須同步主存。
II、由HEAD_AT_GPU+to_cpu()轉移到:
含義就是,GPU的資料比CPU新,且需要使用CPU,此時就必須同步主存。
在轉移至SYNCED期間,還需要做兩件準備工作:
I、檢查當前CPU/GPU態的指標是否分配主存,如果沒有,就重新分配。
II、複製主存至對應態。
處於SYNCED狀態後,to_cpu()和to_gpu()將會得到優化,跳過內部全部程式碼。
自動機將不再運轉,因為,此時僅需要返回需要的主存指標就行了,不需要特別維護。
這種安寧期會被mutable字首的函式打破,因為它們會強制修改至HEAD_AT_XXX,再次啟動自動機。