tensorflow性能調(diào)優(yōu)實踐

工具篇

tensorboard的使用

graph的可視化, 以及獲取必要的運行時的統(tǒng)計數(shù)據(jù), 請參考: 官方教程, 通過對graph以及運行時的統(tǒng)計數(shù)據(jù)的可視化,我們可以看看了解更多的更加直觀的信息. 下圖是一個例子:

image.png

運行時的統(tǒng)計信息統(tǒng)計的是每一個step(或者一次運行)過程中, 每個op的耗時. 結(jié)合compute time 圖, 我們可以分析一個圖中不同的op大概的耗時是什么樣子的, 這樣可以定位出熱點, 針對性的優(yōu)化. 通過結(jié)合device圖, 觀察在不同的device上op是怎么分布的, tensor是怎么在不同device上流動的, 是否有跨設(shè)備的大的tensor流動. 結(jié)合memory圖, 我們可以看看內(nèi)存/顯存的使用情況.

這個圖可以給出一個定性的觀察, 讓你確定真實的model和你心目中的model沒有太大的差別. 如果發(fā)現(xiàn)了一些預(yù)料之外的數(shù)據(jù)流動, 或者預(yù)料之外的耗時的點, 又或是一些op的device不如所期望, 那么, 你可能發(fā)現(xiàn)了一個優(yōu)化的點.

這個圖的問題在于, 不夠細致和全面. 比如, 這個圖里面就沒有體現(xiàn)不同設(shè)備之間的數(shù)據(jù)的send/recv的op和耗時, 不同設(shè)備的帶寬不同, 所以僅僅從tensor大小并不足以體現(xiàn)時間的消耗. 也沒有辦法分辨出哪些op是并行的. 在這種情況下, 去找整個運行的關(guān)鍵路徑比較麻煩. 如果想要知道這些op在不同設(shè)備上是怎么依次執(zhí)行的, 就需要timeline的幫助.

timeline的使用

timeline是一個時序圖, 將每個op的運行開始時間, 結(jié)束時間, 運行設(shè)備, op的前置op, 后置op都在圖上列舉出來了. 怎樣在代碼里面生成timeline信息, 請參考HowTo profile TensorFlow.

下面以單機的一個timeline的例子來說明怎么看timeline圖:

image.png

從上圖可以看出, 這個step耗時約為1400ms. 其中,gpu的利用率比較低, 因為gpu只有一小段時間在運行計算. cpu的主要的一個耗時的操作為一個叫InTopK的op, 這個op消耗了大部分的時間. 里面還有一些Recv op, 這些op的意思是從別的設(shè)備(不同的機器/同一臺機器不同的設(shè)備, 在這里是從內(nèi)存)接收數(shù)據(jù), 這種op雖然顯示耗時很長, 但是因為這些op都是從step的一開始就運行, 然后等待數(shù)據(jù)ready, 所以在timeline上的耗時并不一定是真實的接收數(shù)據(jù)的耗時, 而是 等待+接收數(shù)據(jù) 的耗時. 另一個需要注意的是名為:/job:localhost/replica:0/task:0/gpu:0 compute的信息, 這個部分雖然標記了gpu, 它實際上是運行在cpu上, 作用是把運算發(fā)送到gpu上計算. 真是的gpu計算在/gpu:0/stream:all compute部分.

點擊一個op, 比如圖中的QueueDequeueManyV2, 我們可以得到這個op的信息(注意下圖和上面的圖不是同一個timeline). 見下圖.

image.png

timeline還有一些其他的操作, 比如不同的模式下的操作,比如選擇一個區(qū)間看這個區(qū)間的總的op耗時等等. 這些大家可以點擊timeline窗口右上角的問號查看幫助, 慢慢探索.

對于一個timeline:

  • 我們可以先看看整個timeline的時間消耗, 這個時間應(yīng)該和我們通過代碼測量出的時間差不多, 如果這個時間和代碼中測量的每個step的平均時間差得比較遠, 那說明這個step里面可能包括了一些普通的step里面沒有的東西. 比如我們每隔一些step會做summary和evaluation, 如果timeline的step正好是這種step, 測量出的時間就會偏差較大. 一般的,盡量讓timeline所在的step和普通step的是一樣的, 便于發(fā)現(xiàn)問題.
  • 查看看看cpu和gpu的使用情況. 在整個step中, 了解cpu和gpu的運算大概運行了多少時間. 因為gpu的高效性, 一般情況下, 盡可能的優(yōu)先利用gpu, 提高gpu的使用率. 下面是一個好的例子, gpu的利用率很高, cpu完成必要的操作, 數(shù)據(jù)io和計算并行良好.
image.png

在cpu或者gpu上, 因為會存在op并行的情況, 我們可以大概看一下哪些op是關(guān)鍵路徑, 哪些op的耗時比較多. 對于常規(guī)的任務(wù)而言, 大體可以分為io和計算兩部分. io包括數(shù)據(jù)io和網(wǎng)絡(luò)通信等, 而計算部分則是各種計算操作. 可以先了解這兩部分的耗時, 做到心里有數(shù).

單機優(yōu)化例子

這個部分, 記錄了我們借助timeline, 對于一個單機的模型進行優(yōu)化的情況.

優(yōu)化IO

下圖是模型的初始timeline.

image.png

從上圖可以看出, 一個step的耗時約為300ms.這個和我們代碼測量出的是一致的. 說明這個timeline可以反映普遍的情況.

image.png

觀察timeline可以看到:

image.png

QueueDequeueMany耗時占比超過90%(277/300, 近似計算). 而gpu和gpu上的計算在這個op完成之后才開始進行. QueueDequeueMany可以近似的認為是io讀取數(shù)據(jù), 所以在這個timeline中, 實際的cpu的計算和gpu的計算耗時比較少, io是一個瓶頸. 針對這個問題, 我們進行了數(shù)據(jù)讀取的優(yōu)化. 優(yōu)化后的timeline如下:

image.png

雖然QueueDequeueMany看起來在timeline中的占比還是很大, 但是整體的耗時變成了約100ms, QueueDequeueMany操作時間優(yōu)化到了77ms. timeline的結(jié)果和代碼測量是結(jié)果也是相符的.

image.png

可以看到, 通過減少關(guān)鍵路徑的耗時, 我們將性能提升了100%.

避免feeddict

一般的, 使用feeddict的方式, 會將所有的輸入數(shù)據(jù)先全部讀取到內(nèi)存中, 然后在feed進去, 執(zhí)行計算. 這樣, 數(shù)據(jù)io和計算被完全分開了, 所有的計算都必須在數(shù)據(jù)讀取完成之后才能進行. 但是如果全部交給tf調(diào)度, tf會嘗試將各種op做一定程度的并行, 這會帶來性能的提升, 也就是數(shù)據(jù)讀取的操作和其他的操作其實可以有一定程度上的并行(尤其是有多種輸入數(shù)據(jù)的時候). 從別人的經(jīng)驗來看, 不使用feeddict可以帶來一定幅度的性能提升, 這是一個優(yōu)化feedict帶來30%+性能提升的例子. 在我們的實踐中, 避免使用feeddict方式也會帶來一些性能的提升, 但是幅度沒有那么高.

注意summary的耗時

這也是新手常犯的一個錯誤, 因為新手對于tf不熟悉, 所以并不太清楚不同的函數(shù)會添加什么op. 而summary在對于一些變量做summary的時候, 會對相應(yīng)的變量進行計算. 因為最后運行的時候一般會把所有的sumamry打包運行, 所以通過run的參數(shù)看不出你做了哪些summary. 你需要確保這些操作是你預(yù)料之中的. 比如在我們的情況中, 我們遇到了一個非常耗時的inTopK操作:

image.png

而這個InTopK操作只有在計算recall的時候才會有, 而我們無意中把recall加了summary. 因為我們在計算loss的時候會做summary, 所以每次都帶上了計算recall. 在tf的程序中, 很多的地方都會加summary, 便于在tensorboard中做可視化, 但是稍微不注意, 就可能執(zhí)行很多你意料之外的op.

根據(jù)應(yīng)用量體裁衣優(yōu)化

對于使用雙gpu組成塔式結(jié)構(gòu), 一個常見的行為是將變量定義在cpu上, 然后在cpu上做梯度的Average. 很多的網(wǎng)上的代碼都是這個套路. 但是實際上, 根據(jù)模型的不同, 這個經(jīng)驗并不一定是最優(yōu)的. 在我們的模型中,我們發(fā)現(xiàn),當模型的參數(shù)變大之后, 如果適度把一些變量和計算挪動到gpu上, 那么可以省下非尘唬可觀的時間. 下面是不同方案每個batch的時間.

image.png

下面兩個分別是cpu方案和gpu方案的timeline.可以看到, 原來的方案中, 會在cpu上會做大量的運算.

image.png

優(yōu)化后, cpu上的運算被挪動到了gpu上, gpu的計算耗時遠遠小于gpu, 而整體的耗時也因此得到了優(yōu)化.

image.png

對特定的應(yīng)用和模型, 需要識別出相應(yīng)的熱點, 合理安排運算的次序和位置. 對于網(wǎng)上的代碼和經(jīng)驗,在使用的同時, 心里多帶個問號.

分布式的優(yōu)化例子

相對于單機的程序, 分布式的程序除了優(yōu)化單機效率之外, 另外一個重點是優(yōu)化分布式通信. tensorflow的grpc通信本身存在一定的性能上的問題,參看這個issue, 但是很多的應(yīng)用的慢并不一定是因為這個原因?qū)е碌男阅軉栴}. 所以不要覺得分布式的性能慢是理所當然的, 除非你能確定問題是因為底層的grpc引起的(即使這樣,也有優(yōu)化的途徑), 否則, 不要讓這種成見影響了你對整體的理解和性能的追求.

在我們的實踐中, 我們針對我們模型采用between-graph方式做了分布式, 這里, 我們把關(guān)注點放在了對于通信的優(yōu)化. 通過控制通信,我們達到了和單機一樣的性能和線性的加速比. 從下圖可以看到,采用10個ps, 10個worker的情況下, 相比起單機獲得了10倍的計算加速.

image.png

以下是實踐中的一些經(jīng)驗.

每個worker使用多個GPU

為了減少通信, 如果每個機器上有多個個gpu, 因為gpu與cpu的帶寬遠遠大于網(wǎng)絡(luò)帶寬, 所以讓每個worker使用多個gpu有助于降低通信開銷, 提升性能. 在我們的例子中, 每個機器有兩個gpu, 所以我們采取了每個worker使用兩個GPU的方式, 這樣10個worker可以利用20張卡, 而機器之間的通信開銷可以減少一半. 同時我們保持ps和worker的比例為1:1. 這樣, 對worker和ps, 分布式帶來的額外的通信的壓力基本上是一樣的, 避免出現(xiàn)瓶頸. 對于每個變量, 使用partitioner進行分片, 使得ps的負載更為均衡.

push和pull優(yōu)化

每個step, 因為變量定義在ps上, 所以理論情況下, 每個worker需要將特定的變量從ps, pull到本地進行計算; 計算好的梯度需要push到ps上. 對于這個push和pull操作, 需要大概清楚它的通信量的大小, 做到心中有數(shù). 你可以利用你的知識和你對模型的理解, 將這個通信量降低.

pull優(yōu)化.

在下面的timeline中, 可以看到,ps和worker的耗時都非常長.

image.png

進一步細看可以發(fā)現(xiàn), gpu的計算在數(shù)據(jù)io完成之后遲遲不能開始.

image.png

進一步結(jié)合ps上的數(shù)據(jù)發(fā)送和估計數(shù)據(jù)的大小, 可以發(fā)現(xiàn), 時間消耗在從ps接受數(shù)據(jù)上.

image.png

除此之外, 我們還發(fā)現(xiàn)一個現(xiàn)象, 就是我們觀察到分布式每個step的worer和ps的通信耗時隨著batchsize的變大而變大(這里需要一點計算和假設(shè)去分離各種時間), 而導(dǎo)致整個計算非常緩慢. 這個與直覺不相符, 因為每次通信應(yīng)該獲取的數(shù)據(jù)應(yīng)該是常量才對, 就算batchsize變大也不應(yīng)該劇烈變化.
我們查看了一下相關(guān)的實現(xiàn)源碼, 發(fā)現(xiàn)在tf的一些函數(shù)中,比如embeding_lookup中,會要求一些op和變量定義到同樣的device上, 這是tf的優(yōu)化操作, 這個優(yōu)化會無視外層的device placement. 這種優(yōu)化導(dǎo)致每次從PS獲取的數(shù)據(jù)量并不是固定的, 而是和batchsize相關(guān).

在embedding_lookup函數(shù), 它有一個params參數(shù), 也就是我們的embedding tensor, 我們會從這個大的tensor中, 根據(jù)ids, 選出一部分tensor進行計算.

embedding_lookup(
    params,    # 我們的embedding tensor.
    ids,
    partition_strategy="mod",
    name=None,
    validate_indices=True,  # pylint: disable=unused-argument
    max_norm=None)

根據(jù)這個函數(shù)的實現(xiàn)代碼, 它會要求params(embedding tensor)和相應(yīng)的gather op定義到同一個device上.
在分布式情況下,也就是PS上. 這樣做的目的是優(yōu)化通信, 因為一般情況下, embedding tensor非常大, 但是每次需要lookup取出的tensor在一般情況下比較小, 所以這個操作放到PS上, 使得最后傳輸?shù)絯orker上的數(shù)據(jù)量會變小很多.

但是我們發(fā)現(xiàn)在我們的模型中, 這個優(yōu)化變成了一個負優(yōu)化. 因為我們每次需要取出 $batchsize * 10$ 個向量, embedding tensor的大小和batchsize差不多(我們用了比較大的batchsize), 所以這個tf的優(yōu)化操作導(dǎo)致通信的代價增加了10倍,而且會隨著batchsize的增大而增大. 下面是相應(yīng)的timeline.

image.png

從這個圖上可以看出,recv耗時較長. 但是因為recv在timeline上的耗時并不一定是是真實的耗時,有可能是在等待. 所以這個耗時長只是一個疑點, 沿著這個線索, 通過分析ps的send發(fā)生的時間, 結(jié)合我們模型的數(shù)據(jù)大小, 計算通信的開銷之后我們發(fā)現(xiàn), 這個Recv確實很耗時, 通信開銷占了整個step的大頭.

了解了問題的原因之后,對于這種情況,我們采取了將embedding pull到本地的方法:

#下面的一行identity將變量pull到本地. 這里的variable就是上面提到的的embedding tensor
variable_copy = tf.identity(variable)
x = tf.embedding_lookup(variable_copy)

identify讀取variable, 產(chǎn)生一個等價tensor. 在分布式的情況下, 這個op定義在worker上, 相當于把variable從ps讀取到本地. 通過這樣的操作, 我們把通信量變成一個固定值, 優(yōu)化了網(wǎng)絡(luò)通信. 優(yōu)化后的timeline如下:

image.png

可以看到, gpu上的計算在數(shù)據(jù)IO完成之后立即開始(說明一些變量已經(jīng)在數(shù)據(jù)io的同時pull到本地了), 并且很快計算完成. 說明我們的pull優(yōu)化起效果了.

push優(yōu)化

通過觀察pull優(yōu)化之后的timeline, 我們發(fā)現(xiàn)worker計算完成之后, ps確遲遲不結(jié)束. 我們分析ps的timeline, 見下圖:

image.png

原因和pull的情況類似, 因為我們是從embedding_lookup中獲取tensor進行計算, 所以tf將最后的梯度用IndexedSlices表示, 這種表示導(dǎo)致梯度的大小不僅和變量相關(guān),而是和輸入相關(guān). 因為我們的batchsize比較大, 相比起所以帶了了一些不必要的通信量, 從timeline看, 耗時較多. 所以我們采取了如下操作:

grads_and_vars = [(tf.multiply(grad, 1), var) for grad, var in grads_and_vars]

這個做的是將應(yīng)用于同一個變量的梯度變成一個固定大小的tensor, 不使用稀疏表示 去控制通信.

最終優(yōu)化了push和pull之后的pipeline如下:

image.png

可以看到, gpu的利用率大大提高, cpu中耗時的操作為數(shù)據(jù)io, 而gpu上的op在數(shù)據(jù)io完成后即開始, ps的時間消耗僅僅是略多于worker(因為最后需要在ps上做apply grandent).

后記

上述就是我們優(yōu)化的過程.

單機的部分注意一下op和變量的placement, 手動控制的時候不要迷信經(jīng)驗, 要有充分的理由, 大部分情況下tensorflow的placement做的其實還不錯, 在顯存足夠并且cpu上的op不多的時候, 都放到gpu上即可. 分布式的情況, tf會做一些性能優(yōu)化, 但是tf的placement存在一定的問題, 算法其實可以比現(xiàn)在更加智能和數(shù)據(jù)驅(qū)動, 因為placement對通信影響很大, 如果發(fā)現(xiàn)tf犯了錯誤, 可以適度進行手工干預(yù). 不管是單機還是分布式, 更快的io永遠是值得追求的, 也有很多的優(yōu)化的方法.

總結(jié)一下, 性能調(diào)優(yōu)需要注意細節(jié), 對每個操作的情況做到心中有數(shù). 對于一些異骋鳎現(xiàn)象(比如我們上面碰到的因為colocation, 外層的device placement被無視),可以求諸源碼. 對于可能存在的性能問題, 需要大膽假設(shè), 小心求證. 在優(yōu)化的時候, 注意投入產(chǎn)出, 隨著優(yōu)化的進行, 系統(tǒng)的瓶頸也會變化. 比如把數(shù)據(jù)io時間從1000ms優(yōu)化到100ms可以帶來性能提升2倍, 花更大的精力繼續(xù)從100ms優(yōu)化到5ms可能并不會帶來整體性能的提升, 因為這個時候數(shù)據(jù)io可能已經(jīng)不是瓶頸了.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末棠涮,一起剝皮案震驚了整個濱河市厢拭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兼丰,死亡現(xiàn)場離奇詭異,居然都是意外死亡唆缴,警方通過查閱死者的電腦和手機鳍征,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來面徽,“玉大人蟆技,你說我怎么就攤上這事《芳桑” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵旺聚,是天一觀的道長织阳。 經(jīng)常有香客問我,道長砰粹,這世上最難降的妖魔是什么唧躲? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮碱璃,結(jié)果婚禮上弄痹,老公的妹妹穿的比我還像新娘。我一直安慰自己嵌器,他們只是感情好肛真,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著爽航,像睡著了一般蚓让。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上讥珍,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天历极,我揣著相機與錄音,去河邊找鬼衷佃。 笑死趟卸,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锄列,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼图云,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了右蕊?” 一聲冷哼從身側(cè)響起琼稻,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎饶囚,沒想到半個月后帕翻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡萝风,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年嘀掸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片规惰。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡睬塌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出歇万,到底是詐尸還是另有隱情揩晴,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布贪磺,位于F島的核電站硫兰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏寒锚。R本人自食惡果不足惜劫映,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望刹前。 院中可真熱鬧泳赋,春花似錦、人聲如沸喇喉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轧飞。三九已至衅鹿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間过咬,已是汗流浹背大渤。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掸绞,地道東北人泵三。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓耕捞,卻偏偏與公主長得像烫幕,于是被迫代替她去往敵國和親较曼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容