海量小文件场景下训练加速优化之路
作者:星辰算力平臺(tái)
1. 背景
隨著大數(shù)據(jù)、人工智能技術(shù)的蓬勃發(fā)展,人類對(duì)于算力資源的需求也迎來大幅度的增長。在騰訊內(nèi)部,星辰算力平臺(tái)以降本增效為目標(biāo),整合了公司的GPU訓(xùn)練卡資源,為算法工程師們提供統(tǒng)一的底層GPU算力服務(wù)。借助于虛擬化、算力挖掘等技術(shù),平臺(tái)服務(wù)公司內(nèi)各BG的AI訓(xùn)練場景,GPU利用率業(yè)界領(lǐng)先。同時(shí),通過云原生任務(wù)化的方式,對(duì)接了內(nèi)部各大業(yè)務(wù),促進(jìn)了AI技術(shù)研究效率的提升和創(chuàng)新研究。
當(dāng)下,由于AI訓(xùn)練時(shí)的高性能計(jì)算設(shè)備(如NVIDIA GPU)成本高昂,如果任務(wù)在訓(xùn)練過程中不能保證數(shù)據(jù)IO的速度,將會(huì)導(dǎo)致計(jì)算設(shè)備低載甚至空載,這無疑在時(shí)間和資源上都是一種極大的浪費(fèi)。
在星辰算力平臺(tái)內(nèi)部,用戶的訓(xùn)練數(shù)據(jù)大多存放在平臺(tái)提供的CephFS中,訓(xùn)練時(shí)將對(duì)應(yīng)的CephFS目錄掛載至容器內(nèi)部,從而使用戶在訓(xùn)練時(shí)能夠像使用本地文件系統(tǒng)一樣使用CephFS。但在平臺(tái)運(yùn)營過程中我們發(fā)現(xiàn),在訓(xùn)練數(shù)據(jù)集文件數(shù)較多時(shí),訓(xùn)練任務(wù)使用CephFS會(huì)使訓(xùn)練速度變得異常緩慢。基于這個(gè)普遍存在的問題,本文剖析其產(chǎn)生的原理,然后介紹相應(yīng)的優(yōu)化方案。最后,通過延伸思考來發(fā)散思維,簡要介紹了不同場景下AI訓(xùn)練加速的技術(shù)。
2. 基本概念
2.1. CephFS IO流程
CephFS IO流程如下圖所示。
CephFS IO路徑當(dāng)客戶端進(jìn)行文件系統(tǒng)調(diào)用時(shí)(如open、read、readdir等),需要先從元數(shù)據(jù)服務(wù)器(Metadata Server, MDS)中獲取請(qǐng)求文件的元數(shù)據(jù)信息,元數(shù)據(jù)信息主要包括文件的Inode號(hào)、權(quán)限、uid、gid和訪問更改時(shí)間等。為了加快元數(shù)據(jù)的訪問效率,MDS將大部分熱點(diǎn)元數(shù)據(jù)都緩存在自己的內(nèi)存中,從而避免低效地通過訪問RADOS(Reliable, Autonomic Distributed Object Store)層來獲取元數(shù)據(jù)。客戶端在從MDS中獲取元數(shù)據(jù)后,通過計(jì)算的方式(CRUSH算法)得到數(shù)據(jù)在RADOS中的位置,最后與遠(yuǎn)程的存儲(chǔ)設(shè)備進(jìn)行交互。
從這個(gè)架構(gòu)來看,CephFS是一個(gè)元數(shù)據(jù)和用戶數(shù)據(jù)分離的文件系統(tǒng)。文件的元數(shù)據(jù)和數(shù)據(jù)存儲(chǔ)在RADOS中的不同Pool中,客戶端需要先與MDS進(jìn)行元數(shù)據(jù)交互,再與RADOS進(jìn)行數(shù)據(jù)交互。
2.2. Ceph-FUSE
Ceph-FUSE是CephFS客戶端的一種形式,通過用戶空間文件系統(tǒng)(Filesystem in Userspace, FUSE)的方式來實(shí)現(xiàn)CephFS客戶端的功能。FUSE是一個(gè)面向類Unix計(jì)算機(jī)操作系統(tǒng)的軟件接口,它使無特權(quán)的用戶能夠無需編輯內(nèi)核代碼而創(chuàng)建自己的文件系統(tǒng)。目前Linux通過內(nèi)核模塊對(duì)此進(jìn)行支持。通過這種方式,我們可以編寫用戶態(tài)的應(yīng)用程序,只需要實(shí)現(xiàn)Linux定義的一組文件系統(tǒng)接口,即可在用戶態(tài)實(shí)現(xiàn)一個(gè)完整的文件系統(tǒng)。
當(dāng)用戶需要與CephFS進(jìn)行交互時(shí),客戶端的整個(gè)IO流程如下:
用戶程序通過syscall或glibc庫進(jìn)行系統(tǒng)調(diào)用
進(jìn)程陷入內(nèi)核態(tài),文件系統(tǒng)操作請(qǐng)求到達(dá)Linux虛擬文件系統(tǒng)(Virtual Filesystem, VFS)
VFS根據(jù)請(qǐng)求類型,從Dentry Cache、Inode Cache和Page Cache中分別查找dentry、inode和頁緩存,若緩存命中可直接返回
若緩存不命中,則將請(qǐng)求轉(zhuǎn)發(fā)至FUSE Driver
Ceph-FUSE進(jìn)程通過libfuse監(jiān)聽到來自于/dev/fuse的請(qǐng)求,與Ceph集群進(jìn)行交互并返回結(jié)果。
當(dāng)用戶態(tài)程序發(fā)起FUSE請(qǐng)求時(shí),Ceph-FUSE在經(jīng)過處理后會(huì)將元數(shù)據(jù)信息緩存在內(nèi)存中,提升后續(xù)訪問的性能。同時(shí),Linux的Dentry Cache、Inode Cache和Page Cache也會(huì)分別緩存該文件的dentry、inode和頁,提升熱點(diǎn)數(shù)據(jù)的讀取性能。
3. 問題
3.1. 問題源起
星辰算力平臺(tái)服務(wù)了公司內(nèi)部各個(gè)BG和部門的AI算法工程師,因此平臺(tái)上運(yùn)行的訓(xùn)練任務(wù)場景也各不相同。在運(yùn)營過程中我們發(fā)現(xiàn),有用戶反映某些任務(wù)中CephFS的讀取速度較慢,使整個(gè)訓(xùn)練的時(shí)間拉長,其中屬CV類的任務(wù)較為明顯。
平臺(tái)上CV類的任務(wù)數(shù)據(jù)集,一般都是海量的圖片文件。這類數(shù)據(jù)集的特點(diǎn)是:
文件個(gè)數(shù)多,小數(shù)據(jù)集達(dá)到十萬級(jí)別,大數(shù)據(jù)集達(dá)到百萬、千萬甚至上億級(jí)別。
單個(gè)文件占用空間不大,大多是小文件。
3.2. 理論分析
AI訓(xùn)練場景與許多復(fù)雜的文件操作場景不同,其數(shù)據(jù)讀寫的邏輯較為簡單。一般來說,用戶會(huì)在每個(gè)epoch訓(xùn)練相同的數(shù)據(jù),然后訓(xùn)練多個(gè)epoch直至模型達(dá)到收斂條件。因此,AI訓(xùn)練場景下,訓(xùn)練文件在訓(xùn)練過程中保持不變,且被讀取的頻率相對(duì)固定,同時(shí)寫文件的頻率較低。
針對(duì)這種特點(diǎn),由于Ceph-FUSE會(huì)對(duì)訪問過的元數(shù)據(jù)進(jìn)行緩存,同時(shí)Linux的Dentry Cache、Inode Cache和Page Cache也會(huì)充分緩存讀取過的文件元數(shù)據(jù)和文件數(shù)據(jù)。通常來說,在第二個(gè)epoch開始時(shí),由于數(shù)據(jù)集文件在第一個(gè)epoch已被訪問過,訓(xùn)練時(shí)的IO速度應(yīng)當(dāng)有非常明顯的提升。然而,事與愿違,對(duì)于較多數(shù)量的文件,我們發(fā)現(xiàn)訓(xùn)練速度沒有明顯提升,且每個(gè)epoch的訓(xùn)練速度都很慢。
為了查出其中的原因,接下來我們復(fù)制一個(gè)一模一樣的任務(wù),打開Ceph-FUSE日志進(jìn)行分析。
3.3. 原因排查
3.3.1. Ceph-FUSE日志分析
在訓(xùn)練任務(wù)開始時(shí),打開母機(jī)上的Ceph-FUSE日志進(jìn)行查看。
疑點(diǎn)現(xiàn)象:
在第一個(gè)epoch接近末尾時(shí),發(fā)現(xiàn)出現(xiàn)了日志trim_caps mds.x max xxx caps xxx。
每次trim_caps執(zhí)行,清除的dentry個(gè)數(shù)為5000。
該日志每隔5s會(huì)打印一次,往后的訓(xùn)練過程中會(huì)一直持續(xù)。
注:CAPS是指capabilities,MDS用CAPS授予客戶端對(duì)不同文件進(jìn)行操作的許可,因此MDS需要實(shí)時(shí)維護(hù)每個(gè)客戶端文件操作的CAPS。這就意味著,如果客戶端端持有了某個(gè)文件的CAPS并進(jìn)行了緩存,MDS需要知道每個(gè)客戶端緩存了哪些文件。
3.3.2. 提出猜想
根據(jù)疑點(diǎn)現(xiàn)象大概能夠提出以下的猜想:
在第一個(gè)epoch結(jié)束時(shí)發(fā)生了trim_caps現(xiàn)象,且多次測試結(jié)果均是如此,猜測可能是緩存數(shù)量到達(dá)了某個(gè)閾值。
日志每隔5s會(huì)打印一次,可能是定時(shí)器觸發(fā)了trim_caps。
MDS需要維護(hù)每個(gè)客戶端的CAPS,當(dāng)客戶端讀取文件數(shù)較多時(shí),MDS的cache總會(huì)達(dá)到oversize的狀態(tài),必定會(huì)觸發(fā)trim_caps。
3.3.3. 代碼驗(yàn)證
根據(jù)上述猜想,可以在茫茫的Ceph源碼中直奔主題,分別找出MDS和Ceph-FUSE的關(guān)鍵代碼。
3.3.3.1. MDS端
根據(jù)現(xiàn)象2,在MDS中的tick函數(shù)內(nèi)找到如下代碼:
void?MDSRankDispatcher::tick() {......if?(is_active()?||?is_stopping())?{server->recall_client_state(nullptr,?Server::RecallFlags::ENFORCE_MAX);?//?選中該MDS下持有較多caps數(shù)量的客戶端,執(zhí)行caps回收mdcache->trim();mdcache->trim_client_leases();mdcache->check_memory_usage();?//?當(dāng)內(nèi)存使用量過大時(shí),選中該MDS下所有客戶端,執(zhí)行caps回收(recall_client_state)mdlog->trim();}...... }從中可以看出,MDS端定時(shí)對(duì)客戶端的CAPS進(jìn)行回收,如果回收后內(nèi)存使用量仍然過高,就對(duì)所有客戶端再執(zhí)行一次CAPS回收。在check_memory_usage函數(shù)中會(huì)根據(jù)cache試用情況決定是否再執(zhí)行recall_client_state。
void?MDCache::check_memory_usage() {......if?(cache_toofull())?{mds->server->recall_client_state(nullptr);}...... }進(jìn)入關(guān)鍵函數(shù)recall_client_state進(jìn)行查看。
/***?Call?this?when?the?MDCache?is?oversized,?to?send?requests?to?the?clients*?to?trim?some?caps,?and?consequently?unpin?some?inodes?in?the?MDCache?so*?that?it?can?trim?too.*/ std::pair<bool,?uint64_t>?Server::recall_client_state(MDSGatherBuilder*?gather,?RecallFlags?flags) {......const?bool?enforce_max?=?flags&RecallFlags::ENFORCE_MAX;const?auto?max_caps_per_client?=?g_conf->get_val<uint64_t>("mds_max_caps_per_client");?//?默認(rèn)為1_Mconst?auto?min_caps_per_client?=?g_conf->get_val<uint64_t>("mds_min_caps_per_client");?//?默認(rèn)為100const?auto?recall_max_caps?=?g_conf->get_val<uint64_t>("mds_recall_max_caps");?//?默認(rèn)為5000....../*?trim?caps?of?sessions?with?the?most?caps?first?*/std::multimap<uint64_t,?Session*>?caps_session;auto?f?=?[&caps_session,?enforce_max,?max_caps_per_client](Session*?s)?{auto?num_caps?=?s->caps.size();?//?當(dāng)前caps總量//?當(dāng)flags為RecallFlags::ENFORCE_MAX時(shí),只把caps數(shù)量超過max_caps_per_client的客戶端找出來,否則找出所有客戶端if?(!enforce_max?||?num_caps?>?max_caps_per_client)?{caps_session.emplace(std::piecewise_construct,?std::forward_as_tuple(num_caps),?std::forward_as_tuple(s));}};mds->sessionmap.get_client_sessions(std::move(f));......for?(const?auto?p?:?boost::adaptors::reverse(caps_session))?{......//?計(jì)算每個(gè)客戶端的最大caps數(shù)量uint64_t?newlim;if?(num_caps?<?recall_max_caps?||?(num_caps-recall_max_caps)?<?min_caps_per_client)?{newlim?=?min_caps_per_client;}?else?{newlim?=?num_caps-recall_max_caps;}if?(num_caps?>?newlim)?{/*?now?limit?the?number?of?caps?we?recall?at?a?time?to?prevent?overloading?ourselves?*/uint64_t?recall?=?std::min<uint64_t>(recall_max_caps,?num_caps-newlim);?//?這里可以看出,每次最多回收mds_recall_max_caps個(gè)newlim?=?num_caps-recall;......auto?m?=?new?MClientSession(CEPH_SESSION_RECALL_STATE);?//?新建一個(gè)類型為CEPH_SESSION_RECALL_STATE的請(qǐng)求m->head.max_caps?=?newlim;?//?設(shè)置客戶端的最大caps數(shù)量mds->send_message_client(m,?session);?//?向客戶端發(fā)送請(qǐng)求......}......}...... }從上述代碼基本可以確定CAPS被清除的原因,MDS每隔5s執(zhí)行了一次recall_client_state。由于mds_max_caps_per_client默認(rèn)被設(shè)置為1_M(也就是1048576),當(dāng)訓(xùn)練程序讀取文件個(gè)數(shù)達(dá)到1_M后該客戶端就會(huì)被加入caps_session隊(duì)列發(fā)起CAPS回收請(qǐng)求。由于recall_max_caps默認(rèn)被設(shè)置為5000,所以每次CAPS回收的個(gè)數(shù)為5000。
3.3.3.2. Ceph-FUSE端
首先,根據(jù)MDS端發(fā)起的類型為CEPH_SESSION_RECALL_STATE的請(qǐng)求,找到客戶端接受請(qǐng)求的代碼。
void?Client::handle_client_session(MClientSession?*m)? {......switch?(m->get_op())?{......case?CEPH_SESSION_RECALL_STATE:trim_caps(session,?m->get_max_caps());?//?max_caps,值為上述的newlimbreak;......}...... }Ceph-FUSE接收到MDS的請(qǐng)求后,進(jìn)入trim_caps函數(shù)。
void?Client::trim_caps(MetaSession?*s,?uint64_t?max) {mds_rank_t?mds?=?s->mds_num;size_t?caps_size?=?s->caps.size();?//?客戶端caps總量......uint64_t?trimmed?=?0;auto?p?=?s->caps.begin();std::set<Dentry?*>?to_trim;?//?將需要執(zhí)行caps回收的Dentry放入其中等待回收//?以下內(nèi)容通過迭代器p將caps清理至max以下,將需要清理的Dentry放入to_trim中while?((caps_size?-?trimmed)?>?max?&&?!p.end())?{......}for?(const?auto?&dn?:?to_trim)?{trim_dentry(dn);?//?執(zhí)行Ceph-FUSE內(nèi)的dentry緩存}to_trim.clear();caps_size?=?s->caps.size();if?(caps_size?>?max)_invalidate_kernel_dcache();?//?這是關(guān)鍵函數(shù),調(diào)用了Linux的remount操作來清理所有的dentriesCeph-FUSE接收到MDS的請(qǐng)求后,會(huì)將CAPS總量清理至max以下(本例中就是清理5000個(gè)CAPS)。同時(shí),將這些CAPS對(duì)應(yīng)的dentry緩存全部清除,并調(diào)用操作系統(tǒng)命令來清除Dentry Cache、Inode Cache和Page Cache,執(zhí)行命令為:
static?int?remount_cb(void?*handle) {//?used?for?trimming?kernel?dcache.?when?remounting?a?file?system,?linux?kernel//?trims?all?unused?dentries?in?the?file?systemchar?cmd[1024];CephFuse::Handle?*cfuse?=?(CephFuse::Handle?*)handle;snprintf(cmd,?sizeof(cmd),?"mount?-i?-o?remount?%s",?cfuse->opts.mountpoint);?//?調(diào)用remount,清理文件系統(tǒng)的緩存int?r?=?system(cmd);...... }3.4. 小結(jié)
至此,基本真相大白。整體流程如下圖所示:
訓(xùn)練程序啟動(dòng),開始讀取文件。
在第一個(gè)epoch訓(xùn)練后期,Ceph-FUSE擁有的CAPS達(dá)到1_M。
MDS定時(shí)器觸發(fā),對(duì)持有CAPS超過1_M的客戶端執(zhí)行發(fā)起回收CAPS請(qǐng)求,回收個(gè)數(shù)為5000。
Ceph-FUSE接收到CEPH_SESSION_RECALL_STATE請(qǐng)求,從caps隊(duì)列中清除5000個(gè)CAPS并將這些CAPS對(duì)應(yīng)的dentry從cache中清除。
Ceph-FUSE調(diào)用Linux的remount命令來清除Linux文件系統(tǒng)的cache。
MDS檢查自身內(nèi)存使用情況,若超過閾值則重復(fù)上述回收操作。
訓(xùn)練程序第二個(gè)epoch后,由于文件系統(tǒng)的cache被清除,導(dǎo)致緩存失效。
4. 解決方案
從上述分析來看,最直觀的改進(jìn)方法就是將MDS端的參數(shù)mds_max_caps_per_client增大,可以使得MDS能夠維護(hù)更多的CAPS。然而,這是一種治標(biāo)不治本的方法。接下來提出一種Ceph-FUSE客戶端緩存的方案,避免客戶端CAPS清除導(dǎo)致訓(xùn)練速度變慢。
4.1. 元數(shù)據(jù)緩存方案
4.1.1. 元數(shù)據(jù)緩存
Ceph針對(duì)的是通用場景,設(shè)計(jì)復(fù)雜的CAPS機(jī)制來保證多客戶端對(duì)同一文件讀寫時(shí)的一致性。但在我們的場景中,讀寫方式卻較為固定。主要表現(xiàn)為:
訓(xùn)練過程中讀取的數(shù)據(jù)集在訓(xùn)練過程中不會(huì)發(fā)生改變,且讀取頻率很高。
寫文件的頻率較低,主要是ckpt和log文件,且不會(huì)讀。
在這個(gè)特殊的場景下,可以部分犧牲一致性來獲取性能上的提升。具體表現(xiàn)為,Ceph-FUSE側(cè)可以將以只讀方式打開的文件進(jìn)行元數(shù)據(jù)緩存,減少與MDS的交互,同時(shí)在trim_caps發(fā)生時(shí)不去真正刪除這部分元數(shù)據(jù)對(duì)應(yīng)的緩存。核心改造如下所示:
當(dāng)Ceph-FUSE接收到open請(qǐng)求時(shí),如果以只讀方式打開,則將其標(biāo)記為I_CACHED狀態(tài)。在該狀態(tài)下的文件操作不會(huì)請(qǐng)求MDS獲取CAPS,可以直接從本地cache中讀取元數(shù)據(jù),大大減少了與MDS的交互。
如果一個(gè)文件被只讀打開后,將無法被讀寫打開,這是為了保證寫數(shù)據(jù)的一致性。
當(dāng)trim_caps發(fā)生時(shí),Ceph-FUSE將CAPS被回收的Inode標(biāo)記為I_ORPHAN狀態(tài),然后請(qǐng)求MDS刪除這些CAPS。此時(shí),MDS上已不存在這些Inode的緩存但是本地Ceph-FUSE并沒有真正進(jìn)行CAPS回收,與此同時(shí)也不去清除Linux文件系統(tǒng)的cache,充分保證了元數(shù)據(jù)的緩存。
以上優(yōu)化建立的前提是:只讀方式打開的文件不會(huì)進(jìn)行修改。在我們的AI訓(xùn)練場景下,訓(xùn)練任務(wù)完美契合了這個(gè)條件。
4.1.2. 緩存淘汰算法
Ceph-FUSE會(huì)將元數(shù)據(jù)緩存在本地,但其緩存淘汰算法是一種帶高低優(yōu)先級(jí)的LRU算法。LRU算法核心思想是如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高,但這種思想不符合AI訓(xùn)練的場景。在大多任務(wù)訓(xùn)練過程中,訓(xùn)練數(shù)據(jù)文件會(huì)被均勻地訪問,每一個(gè)epoch中被訪問過的文件反而是這個(gè)epoch中不會(huì)再被讀取的文件。采用LRU算法會(huì)使緩存隊(duì)列中即將被用到的文件元數(shù)據(jù)被刪除,如下圖所示。
LRU淘汰方式下圖模擬了LRU淘汰策略下訓(xùn)練數(shù)據(jù)集命中率分布曲線。
LRU淘汰策略下訓(xùn)練數(shù)據(jù)集命中率分布從該圖中可以看出,LRU淘汰策略下緩存隊(duì)列長度越接近數(shù)據(jù)集大小,命中率提升才越明顯。當(dāng)隊(duì)列長度只有數(shù)據(jù)集大小的一半時(shí),命中率只有15%左右。
在AI訓(xùn)練的場景下,采用不替換策略(Not Replacement, NR)將是命中率最高的算法。在訓(xùn)練的第一個(gè)epoch時(shí),Ceph-FUSE將元數(shù)據(jù)放到緩存中。當(dāng)緩存隊(duì)列已滿時(shí),Ceph-FUSE將不替換現(xiàn)有緩存的數(shù)據(jù),保持緩存不變。在第二個(gè)epoch時(shí),Ceph-FUSE從緩存隊(duì)列中讀取文件元數(shù)據(jù),若未命中則請(qǐng)求MDS獲取。
NR算法4.1.3. 優(yōu)化結(jié)果
結(jié)合兩點(diǎn)針對(duì)Ceph-FUSE的優(yōu)化改動(dòng),我們對(duì)示例任務(wù)進(jìn)行了測試,得到如下的性能測試數(shù)據(jù)。
訓(xùn)練任務(wù)測試結(jié)果從圖中可以看出,經(jīng)過優(yōu)化后針對(duì)海量小文件訓(xùn)練場景,訓(xùn)練速度的提升非常明顯。在第二個(gè)epoch后,元數(shù)據(jù)緩存優(yōu)化版本的訓(xùn)練速度提升為原來的3~4倍,且訓(xùn)練速度較為穩(wěn)定。相比于之前的版本,經(jīng)過優(yōu)化后的Ceph-FUSE能夠充分利用Linux文件系統(tǒng)的cache,且避免了每個(gè)epoch與MDS之間的交互。經(jīng)過優(yōu)化后的版本訓(xùn)練速度能與本地SSD較為貼近。
4.2. 文件緩存方案
文件緩存方案實(shí)際上是一種在元數(shù)據(jù)緩存優(yōu)化的基礎(chǔ)上,利用本地SSD對(duì)文件進(jìn)行緩存的方案。針對(duì)文件數(shù)量特別多,利用Linux文件系統(tǒng)cache但是內(nèi)存不充足的情況,該方法會(huì)有一定效果。
訓(xùn)練程序在第一個(gè)epoch訓(xùn)練時(shí),Ceph-FUSE在處理完read請(qǐng)求后將文件寫入本地SSD中。為了避免海量小文件直接寫入本地造成較多的lookup操作,同時(shí)也為了避免任務(wù)完成后文件緩存難以進(jìn)行清理的問題,考慮將所有讀取后的文件進(jìn)行聚合緩存至一個(gè)本地Cache大文件中,由Ceph-FUSE來記錄每個(gè)文件在本地Cache文件中的偏移。
文件緩存方案的詳細(xì)步驟如下所示:
文件緩存命中:
從Metadata Cache中找出文件在本地Cache文件中的偏移。
通過pread從本地SSD緩存文件中讀取指定范圍的字節(jié)。
文件緩存不命中:
按照正常流程,與Ceph集群進(jìn)行交互,得到讀取的字節(jié)流。
寫本地Cache文件,并記錄該文件在其中的偏移。
更新Metadata Cache,將文件元數(shù)據(jù)和偏移量加入其中。
該方案雖然能夠充分利用本地SSD,但也有一些缺點(diǎn),具體表現(xiàn)為:
由于第一個(gè)epoch讀取文件時(shí),Ceph-FUSE會(huì)寫本地Cache文件,可能會(huì)使得第一個(gè)epoch訓(xùn)練速度變慢。但當(dāng)epoch數(shù)較多時(shí)這部分時(shí)間犧牲是值得的。
IO路徑變得更長,Ceph-FUSE需要讀本地文件。
4.3. 方案對(duì)比
| 原版 | 在訓(xùn)練過程中需要修改數(shù)據(jù)集 |
| 元數(shù)據(jù)緩存 | 在訓(xùn)練過程中不修改只讀打開的文件 |
| 元數(shù)據(jù)緩存 + 文件緩存 | 內(nèi)存緊張,無法充分使用文件系統(tǒng)緩存 |
5. 延伸方案
上述分析和方案主要針對(duì)的是海量小文件的IO密集型計(jì)算場景,接下來發(fā)散思維,簡要介紹一下多種AI加速的解決方案。
我們將AI訓(xùn)練任務(wù)分為IO密集型、GPU計(jì)算密集型和CPU計(jì)算密集型三類任務(wù)。
延伸思考5.1. IO密集型任務(wù)
IO密集型任務(wù)指的是訓(xùn)練瓶頸在數(shù)據(jù)IO上的任務(wù)。這類任務(wù)一般會(huì)讀取較多的數(shù)據(jù)集文件,數(shù)據(jù)量較大,GPU由于數(shù)據(jù)IO的瓶頸一直處于饑餓狀態(tài),因此GPU利用率較低。總結(jié)以下幾種解決方案:
元數(shù)據(jù)緩存
元數(shù)據(jù)緩存方案能夠?qū)⒆x取過的文件元數(shù)據(jù)緩存在內(nèi)存中。在元數(shù)據(jù)和用戶數(shù)據(jù)分離的文件系統(tǒng)中,高效的元數(shù)據(jù)性能對(duì)整個(gè)系統(tǒng)性能至關(guān)重要。在數(shù)據(jù)集只讀場景下,元數(shù)據(jù)緩存可以在FUSE側(cè)完成,也可以在用戶側(cè)完成。該方案一方面能夠大大較少與元數(shù)據(jù)服務(wù)器之間的交互,緩存熱點(diǎn)元數(shù)據(jù),同時(shí)也能降級(jí)元數(shù)據(jù)服務(wù)器的壓力。
文件緩存
文件緩存方案充分利用了本地SSD進(jìn)行文件緩存。在數(shù)據(jù)集只讀場景下,文件緩存仍然是可以在FUSE側(cè)完成,也可以在用戶側(cè)完成。通過緩存文件元數(shù)據(jù)并聚合小文件進(jìn)行本地存儲(chǔ),能使訓(xùn)練任務(wù)的IO方式從網(wǎng)絡(luò)IO逐漸演變?yōu)楸镜豂O。
聚合數(shù)據(jù)集文件
聚合數(shù)據(jù)集文件方案主要指的是lmdb、TFRecord等技術(shù)。在這種方案下,文件數(shù)目大大減少,可以有效地緩解深度學(xué)習(xí)場景下數(shù)據(jù)存取的問題,進(jìn)而提高集群資源利用率。但文件聚合存儲(chǔ)的方式對(duì)場景有一些限制,比如:數(shù)據(jù)更新修改會(huì)相對(duì)麻煩;數(shù)據(jù)集全局shuffle比較困難,只能做部分的shuffle。
GPUDirect Storage
GPUDirect Storage是NVIDIA公司在2019年推出的有關(guān)GPU顯存和存儲(chǔ)設(shè)備之間直接進(jìn)行交互的技術(shù)。傳統(tǒng)方式下磁盤中的數(shù)據(jù)需要先加載至內(nèi)存中,再拷貝到GPU顯存進(jìn)行訓(xùn)練。在這項(xiàng)技術(shù)下,可以繞過CPU讓GPU直接與存儲(chǔ)設(shè)備進(jìn)行交互,在本地或遠(yuǎn)程存儲(chǔ)(NVMe磁盤)與GPU顯存之間建立直接的數(shù)據(jù)IO路徑。該方案一方面可以避免主存內(nèi)數(shù)據(jù)冗余副本的產(chǎn)生,另一方面也緩解了CPU和內(nèi)存的壓力。
5.2. GPU計(jì)算密集型任務(wù)
GPU計(jì)算密集型任務(wù)指的是訓(xùn)練瓶頸在GPU計(jì)算上的任務(wù),通常需要保證數(shù)據(jù)IO和梯度同步的低延時(shí),使得GPU時(shí)刻處于忙碌狀態(tài)。簡要介紹以下幾種解決方案:
數(shù)據(jù)預(yù)取
數(shù)據(jù)預(yù)取是最容易實(shí)現(xiàn)的方案。在每一個(gè)iteration計(jì)算過程中,事先對(duì)下一個(gè)或幾個(gè)iteration所需的數(shù)據(jù)進(jìn)行預(yù)取并預(yù)處理,保證下一個(gè)iteration開始時(shí)特征已處于就緒狀態(tài)。
GPUDirect RDMA(Remote direct memory access)
GPUDirect RDMA從Kepler GPU和CUDA 5.0期間被提出,現(xiàn)在已得到較為廣泛的支持。在多機(jī)訓(xùn)練過程中,這項(xiàng)技術(shù)能讓多個(gè)GPU之間直接進(jìn)行通信,同樣也是避免了主存內(nèi)數(shù)據(jù)冗余副本的產(chǎn)生,減少數(shù)據(jù)拷貝環(huán)節(jié)。配合Mellanox RDMA設(shè)備,數(shù)據(jù)可以從GPU顯存經(jīng)RDMA網(wǎng)卡發(fā)送出去,經(jīng)另一臺(tái)設(shè)備的RDMA網(wǎng)卡后傳輸至GPU,大大較少了IO路徑。目前Horovod等分布式訓(xùn)練工具均以提供對(duì)GPUDirect RDMA的支持。
5.3. CPU計(jì)算密集型任務(wù)
CPU計(jì)算密集型任務(wù)指的是訓(xùn)練瓶頸在CPU計(jì)算上的任務(wù),這類任務(wù)通常的計(jì)算瓶頸在于數(shù)據(jù)的預(yù)處理。此類任務(wù)CPU處于高負(fù)載狀態(tài),但GPU利用率和磁盤IO可能并不高。有以下幾種解決方案:
NVIDIA DALI(Data Loading Library)
NVIDIA DALI是一個(gè)經(jīng)過優(yōu)化的數(shù)據(jù)加載的開源庫,提供數(shù)據(jù)從磁盤加載到訓(xùn)練的完整pipeline。同時(shí)該庫中還提供了音頻處理、圖像處理、視頻處理等預(yù)處理方法,能夠?qū)⒃贑PU上執(zhí)行等預(yù)處理步驟放到GPU上快速執(zhí)行,從而加速AI訓(xùn)練輸入數(shù)據(jù)的預(yù)處理。
特征存儲(chǔ)
特征存儲(chǔ)方式是一種直觀有效的方案,本質(zhì)是進(jìn)行CPU-GPU算力分離。對(duì)于某些大規(guī)模數(shù)據(jù)集,事先利用CPU算力對(duì)原始數(shù)據(jù)進(jìn)行預(yù)處理,將樣本特征打包后寫入云存儲(chǔ)設(shè)備中,然后多個(gè)GPU任務(wù)均可共享這些樣本特征數(shù)據(jù)。但這類方法缺點(diǎn)在于當(dāng)特征選取發(fā)生變化時(shí),需要重新進(jìn)行預(yù)處理。
6. 總結(jié)與展望
本文從實(shí)際訓(xùn)練場景出發(fā),首先簡單介紹了CephFS相關(guān)的基本概念,接著通過現(xiàn)象和源碼分析訓(xùn)練過程中讀取文件緩存失效的原因,然后給出了相應(yīng)的解決方案。經(jīng)過優(yōu)化后,測試任務(wù)的訓(xùn)練速度能提升至原來的3~4倍。最后,通過延伸思考來發(fā)散思維,簡要介紹了不同場景下AI訓(xùn)練加速的技術(shù)。
未來,針對(duì)IO密集型任務(wù),利用GPUDirect Storage和Ceph的RADOS API等技術(shù),結(jié)合本地SSD的高速緩存,可以在用戶側(cè)探索更極致的加速方案。這種方式理論上能夠擁有更快的文件讀取速度,能在用戶側(cè)對(duì)文件的元數(shù)據(jù)和數(shù)據(jù)進(jìn)行充分緩存,減少用戶態(tài)和內(nèi)核態(tài)轉(zhuǎn)換,是未來可以繼續(xù)研究的方向。
歡迎關(guān)注騰訊程序員視頻號(hào)
總結(jié)
以上是生活随笔為你收集整理的海量小文件场景下训练加速优化之路的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统与存储:解析Linux内核全新异
- 下一篇: 网络 IO 演变发展过程和模型介绍