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