前記
眾所周知,夕小瑤是個做的小可愛。
雖然懂點DL框架層知識,懂點CUDA和底層,但是我是做演算法的哎,平時debug很少會遇到深度學習框架層的bug(上一次還是三年前被pytorch坑),更從沒遇到過CUDA層甚至硬體層的bug。直到有一天....
這個bug徹底顛覆了我的debug思路,從這個bug開始,我再也不認為做演算法的就做好演算法就好了。
事故現場還原
當時的背景是這樣的,我平時在一臺8卡的GPU伺服器上debug程式碼,這臺伺服器很少會把8卡全用滿,畢竟這臺機器只是用來debug程式碼而已,要跑那種好幾天的8卡訓練任務的話肯定就提交叢集了,否則你如果佔了全部8張卡,那自己和別人都沒法debug了。
當時恰逢deadline扎堆,0-5卡都被其他蹭機器的小夥伴佔了,只有6、7卡能用。於是我就用這兩張卡來debug程式碼,訓練和預測都是多卡並行,於是debug的時候這兩張是同時用的。
結果把新的idea實現後,跑了一下,發現一個step都沒有跑完就掛掉了,而且全部的報錯資訊就兩個單詞:
$ bash run.sh
segmentation fault
???我寫的都是python程式碼呀,我不是在做NLP麼,怎麼跑出來段錯誤segmentation fault了??眾所周知,段錯誤是寫C++程式碼時非常讓人頭疼的錯誤
隨手一搜
果然,自己攤上大麻煩了╮(╯▽╰)╭
雖然,連掛掉時的程式碼出錯位置都沒有列印;雖然,煉丹以來第一次遇到這個報錯。但!是!依然難不倒見過大風大浪的本寶寶的!
進一步加關鍵詞限制來強行Google了一下,發現可能的錯誤原因依然太多了,各種型別的程式碼裡都可能遇到。算了算了,這次就不Google oriented debug了,是時候展示真正的技術了!
首先,先看看是計算圖compiled之前還是過程中還是runtime掛的。
【此處省略一頓怒插數十斷點的操作】
於是一路debug發現計算圖可以順利建立,但是在深度學習框架完成計算圖編譯,開始runtime計算的時候,就掛了。emmmm,這說明。。。
果然是最糟糕的情況哇!!
基於靜態圖的DL框架就怕在runtime出問題,很多小夥伴可能不清楚runtime該怎麼debug,於是紛紛覺得pytorch真香。其實對於絕大部分錯誤,靜態圖還是蠻好debug的,秘訣也很簡單,那就是往圖裡插入debug op。
像tensorflow、paddlepaddle這種支援靜態圖的深度學習框架,都是有大量debug op的,最常用的就是print op(tensorflow中的tf.Print/tf.print,paddle中的layers.Print),它可以在計算圖的任意位置列印出執行到此位置時的任意Variable/Tensor的runtime值,當然也可以搭配shape op(TF中的tf.shape,paddle中的layers.shape)列印出Tensor的runtime形狀等資訊。除了print op外,還有assert op、is op等保證執行正確性、輔助debug的op,也就是說,python中常用來debug的一些關鍵字和函式呼叫,其實成熟的靜態圖框架裡基本都能找到對應的op。熟悉了對映關係後,靜態圖debug起來也不會太費力。
但!是!有兩種情況依然比較頭疼,一種是計算圖已經完全建立了,但是第一個op還沒有跑到的時候就掛了(這時候插入的debug op完全沒被run到);還有一種是前向正確的跑完了,但是計算梯度的時候掛掉了(這時候錯誤可能發生在整張圖的任意位置,甚至不在圖裡)。當它們再疊加上多機多卡問題時,就是最慘的情況。
不幸的是,我遇到了(。 ́︿ ̀。)
首先,如前所述,本寶寶很淡定的在計算圖裡插入了一堆Print結點,然後發現前向計算結果貌似非常正確,輕鬆的定位到錯誤發生在runtime計算反向梯度的時候,也就是
optimizer.optimize()
算了算了,也不是第一次掛在這兒,報錯資訊似乎也提供不了太多指導(報錯資訊裡有一些路徑類資訊,比較敏感就不貼出來了)。再多插入幾個debug op來驗證前向路徑的正確性
【此處省略另一頓插op的操作】
結果,萬萬沒想到,插入debug op之後,竟然第一個step跑完了,這次掛在了第二個step!
???
debug op還有強行續命的功效???
我!不!信!於是把debug op又都註釋掉了,重新run!
結果,
又掛在了第一個step!!!
我當時狠掐了自己一把,這特喵的一定是在做一個很荒唐的夢!
令我沒想到的是,現實果然比夢境更荒唐。我又重複的run了好多好多次,發現沒有插入debug op的時候確實永遠是第一個step就會掛掉。於是我把這個震驚的現象告訴了大家,然後果然,大家都覺得我瘋了。
小夕:”你們都過來!我來親自演示一遍!“
小夕:”你們看!沒有插入debug op的時候,第一個step就報了segment fault的錯誤叭~“
大家:”哦“
小夕:”然後看好哦,我把這裡的debug op都插上,然後,run!“
誒誒誒???怎麼還是第一個step就掛了,被瘋狂打臉(´Д` )
大家:小夕你要是累了就先去睡會兒,別太累了,大家散了散了╮(╯▽╰)╭
嚶嚶嚶怎麼會(。 ́︿ ̀。)
算了算了,不糾結這個了。回到debug本身,emmm,話說前向能正常跑完,永遠都是掛在反向階段,那麼說明要麼前向哪裡埋了一個坑暫時被強行計算了,但是這個節點處的梯度其實是無法計算的;要麼可能是最佳化器本身就有bug。
於是先掃了一遍程式碼,確實沒有使用也沒有cast很奇怪的資料型別,也沒有使用一些很奇怪的op,基本可以排除前一種情況。那應該就是後一種了!而最佳化器用的其實就是adam,沒什麼改動,那問題就肯定出在自己新加的多卡梯度合併這塊了!
於是果斷的在6卡上跑了一下單卡的程式碼,果然正常訓練!我真是福爾摩夕呀,馬上就要揪出bug真兇了好激動。
繼續!回去check了一下多卡邏輯,燃鵝似乎。。沒問題鴨?天吶,難不成這回要去框架原始碼裡插斷點了?
看來,我只能祭出我的殺手鐧了,那就是
滾!去!寫!demo!
由於程式碼邏輯有點複雜,並且會在多卡梯度合併的時候要進行一些額外的處理,當時覺得應該還是程式碼哪裡寫的有問題,於是決定先開ipython寫個多卡計算的小demo。
【此處省略一個demo】
嗯,前向計算順利透過,符合預期。好,加大難度,引入梯度合併。
結果,
又又又segmentation fault 了!!!我還沒引入我的idea呢,直接就掛了。
不對哇!!這不就是多卡訓練的標準玩法麼,難道最新版的框架有bug???天吶我發現了這麼大的一個bug!於是卸掉重灌了一箇舊版的非常穩定的包,重新跑這個demo程式碼
結果,又又又又 segmentation fault 了!!!
不會吧。。。又check了一下環境,確保CPU、記憶體、磁碟、GPU及其視訊記憶體都是夠用的,CUDA、CUDNN、NCCL也沒問題(自帶的tool都能check過)。難不成是python的鍋?不至於吧,我也太多疑了。。。
這時我出現了一個大膽的想法。
難不成這兩張顯示卡里,有一張是壞的??
前面測試CUDA時剛在GPU6上跑了一把,發現好好的。那麼。。。GPU7!狼人肯定是你了!!
於是又去GPU7上跑,滿心等著它掛掉,結果。。
訓練一切正常
強迫自己冷靜了一下。我是不是太多疑了,竟然都懷疑到硬體身上了。但是,基本的執行環境都check過了鴨,如果不是硬體,也不是python和框架的問題,那難不成。。。是中間的glibc的問題???聯想到segment fault這個報錯資訊,更加確信了!glibc就是狼人,這次跑不掉了!
然而眾所周知,glibc這種級別的庫,是非常底層的,一旦改壞了,會導致ls、cd這種級別的命令都掛掉。我看了一眼GPU0-5卡上跑的好好的別人的任務,再次陷入了迷茫。。。
【此處省略長達數十分鐘的卡頓】
嗯,先低調一些,先把demo放到一個嶄新的環境中跑下!於是把demo放到另一臺機器上跑了一下,果然,多卡也是能正常跑的。莫非真是glibc?但是為什麼別人的任務沒問題呢。似乎一切現象都在導向最後一種可能
GPU6到GPU7的底層通訊鏈路出故障了!
於是我耐心的等著其他小夥伴的任務跑完,重新測試!
GPU5+GPU6:訓練正常
GPU5+GPU7:訓練失敗
GPU0+GPU1+GPU2+GPU3+GPU4+GPU5+GPU6:訓練正常
GPU0+GPU1+GPU2+GPU3+GPU4+GPU5+GPU6+GPU7:訓練失敗
實錘!!!激動萬分的跑去找管機器的小哥哥了,語無倫次的給解釋了大半天。終於,小哥哥將信將疑的安排人去進一步測試了一下
焦急的等了若干天。。。
小哥哥:“是的,GPU7壞掉了,無法與其他卡通訊,更換GPU7後測試正常“
聽到這個訊息後,小夕非常激動的重新測了一遍各種跟GPU7的排列組合,成功!!
後記
這時候我突然想起來之前那個見鬼的插debug op會導致多跑一個step的現象,而且僅出現了一次,於是跑去問小哥哥,小哥哥悠悠的說
“那大概就叫做迴光返照吧,GPU7盡力了“