在深度學習模型訓練中,每次迭代過程當中都涉及到Tensor的建立和銷燬,伴隨着的是內存的頻繁 malloc
和free
操做,可能對模型訓練帶來沒必要要的 overhead。git
在主流的深度學習框架中,會藉助 chunk 機制的內存池管理技術來避免這一點。經過實事先統一申請不一樣 chunk size 的內存,並記錄到內存池中。建立一個Tensor時,若內存池中存在知足需求的可用內存,則直接分配。銷燬一個Tensor時,並不立刻free
掉還給系統,而是標記爲可用狀態,放在內存池供下個Tensor使用。github
經過內存池管理技術,能夠有效減小頻繁的malloc
和free
操做,避免沒必要要的overhead。框架
每一個chunk表明一段連續的存儲空間。不一樣的chunk按照地址升序組成雙向鏈表。每一個chunk只有兩種狀態:空閒、已佔用。不存在部分使用的中間態。函數
在Paddle中,內存池統一經過 BuddyAllocator
類來管理,下面逐一剖析相關實現。成員變量包括:學習
private: /* * 默認的內存分配器,支持CPUAllocator、GPUAllocator、CUDAPinnedAllocator。 */ std::unique_ptr<SystemAllocator> system_allocator_; // 用於表示一個內存段的信息 using IndexSizeAddress = std::tuple<size_t, size_t, void*>; // 藉助有序的 set 存放可用的內存段 using PoolSet = std::set<IndexSizeAddress>; PoolSet pool_; // 內存池,存放可用的不一樣 chunk size的內存信息 PoolSet chunks_; // 內存池。存放從系統從新申請的內存塊
從BuddyAllocator
的成員變量能夠看出,不一樣BuddyAllocator
對象能夠管理不一樣類型的內存池,好比 CPU內存池、GPU內存池、CUDAPinned內存池。ui
構造函數顯式須要一個SystemAllocator
來初始化:操作系統
public: BuddyAllocator(std::unqiue_ptr<SystemAllocator> system_allocator, size_t min_chunk_size, size_t max_chunk_size);
BuddyAllocator
如何避免內存頻繁的malloc
和free
操做呢?指針
申請內存時:code
void* BuddyAllocator::Alloc(size_t unaligned_size){ // step 1: 作內存對齊,保證申請的內存大小都是 min_chunk_size的整數倍 size_t size = align(unaligned_size+sizeof(MemoryBlock::Desc), min_chunk_size_); // 加鎖 std::lock_guard<std::mutex> lock(mutex_); // step 2: 若是申請內存超過 max_chunk_size_, 則交由system_allocator完成 if(size > max_chunk_size_){ return SystemAlloc(size); } // step 3: 不然,去內存池查找是否有知足大小的可用內存塊 auto it = FindExistChunk(size); // step 4: 若找不到,則向系統申請新內存塊,並記錄到內存池中 if(it == pool_.end()){ it = RefillPool(size); if(it == pool_.end()){ return nullptr; } }else{ VLOG(10)<<; } // step 5: 更新內存池 size 相關信息 total_used_ += size; total_free_ -= size; // step 6: 若申請的size小於內存塊實際大小,則把多餘的部分切分掉,新建一個內存塊放到內存池中 return reinterpret_cast<MemoryBlock*>(SplitToAlloc(it, size))->Data(); }
此處並不是真正的將內存歸還給系統,而是將內存塊從佔用狀態標記爲可用狀態,並放到內存池中開放出去。對象
void BuddyAllocator::Free(void* p){ // step 1: 將指針轉換爲內存塊指針 auto block = static_cast<MemoryBlock*>(p)->MetaData(); std::lock_guard<std::mutex> lock(mutex_); // step 2: 獲取內存塊的詳細元信息,釋放內存須要 auto* desc = cache_.LoadDesc(block); if(desc->get_type() == MemoryBlock::HUGE_CHUNK){ // 在前面申請大內存時,也是交由system_allocator完成的,解鈴還須繫鈴人 system_allocator_->Free(block, desc->get_totoal_size(), desc->get_index()); // 刪除內存塊對應的元信息 cache_.Invalidate(block); return; } // step 3: 若待釋放內存塊大小在[min_chunk_size_, max_chunk_size_]之間 block->MarkAsFree(&cache_); // 修改元信息,標記爲 可用 狀態 // step 4: 更新總內存信息 total_used_ -= desc->get_total_size(); total_free += desc->get_total_size(); // step 5: 看是否能夠將此內存塊與左右空閒的內存塊合併,避免內存碎片 MemoryBlock* right_buddy = block->GetRightBuddy(&cache_); if(right_buddy){ auto rb_desc = cache_.LoadDesc(right_buddy); if(rb_desc->get_type() == MemoryBlock::FREE_CHUNK){ pool_.erase(IndexSizedAddress(rb_desc->get_index(), rb_desc->get_total_size(), right_buddy)); block->Merge(&cache_, right_buddy); } } MemoryBlock* left_buddy = block->GetLeftBuddy(&cache_); // .... (省略對前序內存塊的合併操做) // step 6: 將合併後的內存塊放入到可用內存池中 pool_.insert(IndexSizeAddress(desc->get_index(), desc->get_total_size(), block)); }
此階段纔是真正的將內存歸還給操做系統,此過程分爲兩個步驟:
system_allocator_
申請的內存 free
掉(調用Release
函數)BuddyAllocator
對象時,對內存池剩餘的內存 free
掉(調用析構函數)咱們先看第一階段 Release
邏輯:
uint64_t BuddyAllocator::Release(){ // 先加鎖 std::lock_guard<std::mutex> lock(mutex_); int num = 0; // 標記後來新增申請的內存塊 uint64_t bytes = 0; // 統計總共可釋放的內存 bool del_flag = false; // step 1: 有序遍歷可用內存池中的每一個內存塊 for(auto iter = pool_.begin(); iter != pool_.end()){ auto remain_size = std::get<1>(*iter); auto remain_ptr = std::get<2>(*iter); for(auto& chunk : chunks_){ auto init_size = std::get<1>(chunk); auto init_ptr = std::get<2>(chunk); // step 2: 若在以前的chunks_記錄中找到地址同樣,空間同樣的chunk if(init_size = remain_size && init_ptr == remain_ptr){ ++num; bytes += init_size; total_free_ -= init_size; auto block = static_cast<MemoryBlock*>(init_ptr); // step 3: 則歸還內存給系統,標記爲此內存塊爲可回收狀態 system_allocator_->Free(init_ptr, init_size, std::get<0>(chunk)); cache_.Invalidate(block); del_flag = true; break; } } // step 4: 對於標記爲可回收狀態的內存塊,從內存池中移除 if(del_flag){ iter = pool_.erase(iter); }else{ iter++; } } return bytes; }
Release
支持被顯式調用,以歸還未用到的內存給操做系統。
當BuddyAllocator
對象在模型訓練結束後,會被析構掉。析構時須要保證以前申請的內存必須正確的歸還給操做系統,不然會致使內存泄露。
BuddyAllocator::~BuddyAllocator(){ while(!pool.empty()){ // step 1: 遍歷內存池中全部的內存塊 auto block = static_cast<MemoryBlock*>(std::get<2>(pool_.begin())); auto desc = cache_.LoadDesc(block); // step 2: Free掉,歸還給系統 system_allocator_->Free(block, desc->get_total_size(), desc->get_index()); // step 3: 刪除元信息 cache_.Invalidata(block); pool_.erase(pool_.begin()); } }