跑python gpu利用率低_训练效率低?GPU利用率上不去?快来看看别人家的tricks吧~...
前言
首先,如果你現(xiàn)在已經(jīng)很熟悉tf.data+estimator了,可以把文章x掉了╮( ̄▽ ̄””)╭
但是!如果現(xiàn)在還是在進(jìn)行session.run(..)的話!尤其是苦惱于GPU顯存都塞滿了利用率卻上不去的童鞋,這篇文章或許可以給你打開新世界的大門噢( ̄? ̄)
如果發(fā)現(xiàn)經(jīng)過一系列改良后訓(xùn)練效率大大提高了,記得回來給小夕發(fā)小紅包( ̄? ̄)
不過,這并不是一篇怒貼一堆代碼,言(三)簡(言)意(兩)賅(語)就結(jié)束的CSDN文風(fēng)的文章。。。所以伸手黨們也可以X掉了╮( ̄▽ ̄””)╭
緣起
很早很早之前,在小夕剛接觸tensorflow和使用GPU加速計算的時候,就產(chǎn)生過一個疑惑。為什么顯卡的顯存都快滿了,GPU利用率還顯示這么低呢?好浪費呀,但是又無可奈何。當(dāng)時GPU利用率100%的情況基本是僅存于一塊顯卡塞4、5個不費顯存的小任務(wù)的情況。
在比較極端的情況下,甚至GPU的利用率會降到10%以下,就像這樣:
而大部分情況下寫出來的代碼train起來后是這樣的:
可以看到,雖然顯卡的顯存都塞滿了,但是顯卡功率(最左邊那一欄,114W和69W)和利用率(最右邊那一欄,35%和38%)卻遠(yuǎn)遠(yuǎn)沒有達(dá)到極限。大部分人的想法是,算了算了這不重要,我去做實驗了再見【wei笑】
然而!如果你在做大型實驗,train一次跑幾天呢?這個細(xì)節(jié)會極大的影響你的實驗效率和DDL到來前的實驗次數(shù)!想一下,完全一樣的model和設(shè)置,你的代碼要train一周,然而隔壁老王只需要train三天╮( ̄▽ ̄””)╭
路人甲:我有256張顯卡小夕:好了這篇文章你可以X掉了
那么,我們有沒有可能一直這樣呢:
是不是這功率和利用率看起來不可思議!不要懷疑這是PS的圖!這只是小夕的日常截圖!tricks用的好GPU利用率掉不下來99%,然鵝代碼寫的足夠蠢,也可以上不去5%!
那么問題來了,到底是什么導(dǎo)致的這個差異呢?
不要急,我們來放大一下那些gpu利用率只有30%幾的代碼在訓(xùn)練時的gpu利用率的變化情況(好像句子有點長
watch -n 0.1 nvidia-smips:(可能掉幀太嚴(yán)重了看著不連貫╮( ̄▽ ̄"")╭,建議在自己的機(jī)器上試一下,會直觀的多~)看!是不是一下子就發(fā)現(xiàn)問題啦?可以看到,其實gpu利用率并不是一直在比較低的水平,而是很有規(guī)律的周期性的從0漲到接近100再跌到0,再重新漲到100再跌回0。如果同時開著打印日志的窗口,你就會發(fā)現(xiàn)這個周期恰好跟每個訓(xùn)練step的時長一致!也就是說,在每個step,其實有一些時間并沒有花在GPU里,那當(dāng)然就是花在cpu里啦。
那在cpu里干什么的呢?當(dāng)然就是load下一個batch、預(yù)處理這個batch以及在gpu上跑出結(jié)果后打印日志、后處理、寫summary甚至保存模型等,這一系列的花銷都要靠cpu去完成。回顧一下我們常寫的代碼:
create_graph() create_model_saver() create_summary_writer() create_session() do_init() for i in range(num_train_steps):load_batch(...) # cpupreprocess(...) # cpufeed_dict = {...} # cpufetch_list = [...] # cpubuf = session.run(fetch_list, feed_dict) # gpupostprocess(buf) # cpuprint(...) # cpuif i % x == 0:summary_writer.write(...) # cpuif i % xx == 0:model_saver.save(...) # cpu看,尤其是preprocess(…)任務(wù)比較重的話就容易導(dǎo)致代碼在cpu里也要跑好一段時間,gpu利用率自然就會上不去而且呈現(xiàn)周期性變化啦。
那么有沒有什么辦法降低cpu時間,提高gpu時間呢?
一個很自(愚)然(蠢)的想法就是把一切訓(xùn)練代碼都用tf的api重寫不就好啦,甚至最外層的那個for i in range(num_train_steps)其實都可以用tf.while_loop重寫呀。嗯,小夕還真的這么嘗試過,然后發(fā)現(xiàn)
TF api這特喵的都是些什么鬼!各種跟numpy和python內(nèi)置函數(shù)重名卻行為不一致是什么鬼!臥槽這個api少了個參數(shù)我該怎么辦?python里就一行代碼就能搞定的事情我為什么寫了幾十行??
所以除了函數(shù)式編程的大牛,小夕極力的不建議重蹈覆轍!尤其是我們這些遇到匯編會哭,看到Lisp會崩潰的90后小仙女!
所以沒辦法把整個train loop都描述進(jìn)計算圖了?
別怕別怕,好在后來其實tensorflow已經(jīng)封裝了一個特別好(多)用(坑)的上層API來把整個train loop都能輕松的封裝在計算圖中,從而實現(xiàn)超級高的GPU利用率和訓(xùn)練效率!
Estimator
不用管它為啥叫Estimator,只需要知道,它把我們剛才想做的事情基本都給封裝好了就行。把剛才的那個經(jīng)典的寫法搬過來
1. create_model() 2. create_model_saver() 3. create_summary_writer() 4. create_session() 5. do_init() 6. for i in range(num_train_steps): 7. load_batch(...) # cpu 8. preprocess(...) # cpu 9. feed_dict = {...} # cpu 10. fetch_list = [...] # cpu 11. buf = session.run(fetch_list, feed_dict) # gpu 12. postprocess(buf) # cpu 13. print(...) # cpu 14. if i % x == 0: 15. summary_writer.write(...) # cpu 16. if i % xx == 0: 17. model_saver.save(...) # cpu1-5行在estimator中都封裝好啦,你只需要把相關(guān)配置塞進(jìn)estimator的RunConfig就可以啦~
7-9行也封裝好啦,你只需要把數(shù)據(jù)集載入和預(yù)處理的相關(guān)代碼的函數(shù)塞給estimator.train的input_fn~
第10行也封裝好啦,你只需要把要fetch的loss、train_op丟進(jìn)estimator的EstimatorSpec~
第11行也封裝好啦,你只需要把描述模型計算圖的函數(shù)塞給estimator的model_fn~
第12-13行不用操心細(xì)節(jié)了,global_step和loss自動完成了,剩下的丟給tf.Print和LoggingTensorHook吧~
第14-17行不用你寫了,自動完成了
╮(╯▽╰)╭
經(jīng)過這么一頓折騰,我們發(fā)現(xiàn)GPU利用率大大提高啦~直逼80%甚至90%。那么還有沒有可以壓榨的空間呢?
其實這時仔細(xì)一分析就會發(fā)現(xiàn)雖然estimator把大部分的代碼寫進(jìn)計算圖里了,但是從數(shù)據(jù)的載入和預(yù)處理依然是在cpu里串行進(jìn)行呀,而且比如一個batch有128個樣本,那么estimaor內(nèi)部在run每個step的時候還是要等著這128個樣本串行的處理完才行。這顯然就是最后的瓶頸啦!有沒有辦法消除掉呢?·當(dāng)然有,那就是
tf.data
TF的dataset API可以說讓人又愛又恨了,它確實看似提供了一種把整個預(yù)處理都搬進(jìn)計算圖進(jìn)行并行化處理的途徑,但是!如果你真的完全用tensorflow API來做復(fù)雜的預(yù)處理的話,真的會讓人瘋掉的QAQ因此,這里在用tf.data之前,小夕極力的建議先把數(shù)據(jù)集盡可能的transform成預(yù)處理后的樣子,包括做分詞、做截斷、做word2id等,不過padding和input_mask可以留在TF里面做,畢竟都只需要一行。
那做完這些預(yù)處理后,數(shù)據(jù)該怎么存儲會更方便后續(xù)的讀取和處理呢?最最最建議的方式還是使用tf.records來存儲,磁盤、內(nèi)存的存儲和IO效率都會相比傳統(tǒng)方式更快一些,x和y也不用分開了。當(dāng)然這樣的唯一的壞處就是不能直接打開看數(shù)據(jù)集╮( ̄▽ ̄””)╭畢竟數(shù)據(jù)集被做成了二進(jìn)制文件。
但是實在比較懶不想用tf.record的話,那么小夕極力建議把x和y分開存儲,并且盡量讓tf.data在讀取數(shù)據(jù)的時候做完上面的那些必要的預(yù)處理,以避開難用的字符串基礎(chǔ)操作API并且減輕訓(xùn)練時的cpu和內(nèi)存壓力。
tf.data還有一個很大的好處就是可以很天然的支持以streaming的方式讀取數(shù)據(jù),這樣在面對大數(shù)據(jù)集時就不會發(fā)生數(shù)據(jù)load完后發(fā)現(xiàn)顯卡被占的尷尬事件了╮( ̄▽ ̄””)╭
好像講了這么久,還是沒講怎么用tf.data加速Q(mào)AQ,來來來進(jìn)入正題啦。
想想哈,沒用tf.data的時候,我們寫出來的代碼實際跑起來就是這個樣子的:
這也是文章開頭小夕解釋的為什么gpu利用率上不去并且周期性變化的重要原因。那么我們可以不可以消除idle,像下面這樣讓prepare和train的過程并行進(jìn)行呢?
當(dāng)然可以!那就是
prefetch
從prefetch的意思就可以理解,那就是預(yù)先獲取下一個step要load的batch。使用tf.data里面的叫做prefetch的神奇api就可以輕松完成啦,這個api里的參數(shù)buffer_size就是講的是額外的fetch多少份,比如buffer_size=1,然后我們要prefetch的是batch的話,那么模型每次prepare完一個batch后,就會自動再額外的prepare一個batch,這樣下一個train step到來的時候就可以直接從內(nèi)存中取走這個事先prepare好的batch啦。(詳情見后面)
等下,看上圖的話,有木有發(fā)現(xiàn),如果prepare一個batch耗時很短的話確實兩全齊美,但是如果耗時比較久,尤其一下子prefetch好幾個batch的話,一旦prepare的用時超過了train一個step的用時,那么每個train step的性能就會受限于prepare的效率啦。放大一下這個問題的話如下圖所示
看,prepare用時太久反而會導(dǎo)致train完一個step后gpu空閑了(雖然其實下個step的batch可能已經(jīng)prepare好了)
那么能不能確保prepare階段的用時小于train階段的用時呢?
parallel mapping
一個很簡單的想法當(dāng)然就是讓樣本并行處理啦~如果batch size是128,prefetch size=1,那么準(zhǔn)備一個batch要串行的跑128*2=256次的預(yù)處理,但是如果我們開4個線程去跑,是不是就看起來快多啦。幸運的是我們也不用自己手?jǐn)]多線程了,tf.data.Dataset在map(預(yù)處理)函數(shù)里有一個參數(shù)num_parallel_calls,給這個參數(shù)賦值就可以并行parse啦。如圖,
這樣的話只要prefetch的buffer_size和map的num_parrellel_calls取得合適,基本就可以實現(xiàn)不間斷的train啦,也就是幾乎達(dá)到100%的GPU利用率!
好啦,思想明白了,代碼就容易理解啦。不使用tf.record,直接從預(yù)處理好的純文本格式的數(shù)據(jù)集load數(shù)據(jù)時的典型過程如下
def build_input(..):x = tf.data.XXDataset(..)x = x.map(..., num_parallel_calls=N) # parellely = tf.data.XXDataset(..)y = y.map(..., num_parallel_calls=N)dataset = tf.data.Dataset.zip((x, y))dataset = dataset.repeat(num_epochs) if is_train:dataset = dataset.shuffle(..)dataset = dataset.batch(batch_size)dataset = dataset.prefetch(buffer_size=1) # prefetchiterator = dataset.make_xx_iterator()return iterator.get_next()當(dāng)然,如果用上tf.record后,就不用分別從x和y倆文件中讀數(shù)據(jù)啦,感興趣的童鞋可自行去了解一下。
補充福利
當(dāng)然,剛從傳統(tǒng)的代碼遷移到tf.data+estimator的時候可能會不太適應(yīng),最主要的還是debug的方式,不能像之前一樣直接session.run(debug_tensor)了,那怎么辦呢?
一般來說我們打印tensor有兩種情況,一種是計算圖出錯時需要打印一次或幾次來定位問題,一種是像global_step,loss等需要周期性check。對于這兩種情況,之前是習(xí)慣session.run的時候把要打印的tensor也run出來,而現(xiàn)在這兩種情況可以區(qū)分對待啦。
對于第一種,小夕感覺最高效的還是直接在計算圖里插tf.Print(..),使用非常方便,debug能力很強(qiáng)大!如果打印還需要配合global step,加一條tf.cond就搞定啦。對于第二種,其實global step和loss的話estimator默認(rèn)就會打印出來,如果是其他需要周期性打印的tensor,那么就用tf.train.LoggingTensorHook包裝一下然后丟進(jìn)estimator.train里吧~習(xí)慣之后竟然還感覺挺方便的m(_ _)m
最后,愿天下沒有空閑的顯卡
更多精彩文章歡迎關(guān)注小夕的微信訂閱號【夕小瑤的賣萌屋】 (?ω< )★
總結(jié)
以上是生活随笔為你收集整理的跑python gpu利用率低_训练效率低?GPU利用率上不去?快来看看别人家的tricks吧~...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python基本程序结构有几种_pyth
- 下一篇: python条形图颜色设置_python