昨天體檢收工,病歷首頁圈了兩個紅字,發呆兩成上下、通訊一成五(Day 10 那張示意卡)。今天開始,我們進會診室。
會診跟體檢是兩回事。體檢給你數字,會診要找病因,而找病因的第一步從來不是翻報告,是把片子掛上燈箱,所有人親眼看一遍。我們的片子,就是 Day 4 那份 8 卡 Megatron 的錄影。那時候你剛學會認列,看什麼都新鮮;這次回來,你口袋裡多了 stats 的帳、analyze 的線索、還有一張病歷首頁。同一份檔,這次你會看到完全不同的東西。
今天只練一件功夫,讀片。讀片只讀三件事,kernel bar、空白、stream。前兩件 Day 4 打過照面,今天要在真檔上把它們讀成病徵;第三件 stream 是新功課,多卡的戲,全在它身上。
片子還是同一份,沒抓過的抓一下,抓過的直接開:
hf download GindaChen/nsys-hero \
distca-0/baseline.t128k.host-fs-mbz-gpu-899.nsys-rep \
--repo-type dataset --local-dir .
nsys-ui distca-0/baseline.t128k.host-fs-mbz-gpu-899.nsys-rep
一個小提醒,看片的 GUI 版本至少別舊過錄這份檔的 nsys,舊 GUI 開新檔常常直接打不開,新 GUI 開舊檔一般沒事。
開場還是 Day 4 那三個動作,放大、找一圈、收列。這次再多教兩招,讓你翻片子翻得像個老手。
第一招,拖選加 Shift+Z。在時間軸上拖出一段範圍,按 Shift+Z,畫面直接鑽進那一段。配著最上面那棵 NVTX 樹用最順,樹上一格 Iteration 就是一圈,對著它拖選再 Shift+Z,一秒鐘就站在你要看的那圈裡,比滾輪滾半天體面多了。
第二招,Events View。對著任何一條列按右鍵,選 Show in Events View,這條列上所有事件會攤成一張可排序的表,按耗時排序,點兩下第一名,時間軸自動跳到那顆 kernel 面前。看片看到一半想找最肥的那條 bar 本人,這招比用眼睛掃快十倍。順手一提,列名旁邊有個圖釘,常看的列釘起來,捲到哪它都跟著,八張卡跳來跳去看的時候特別省事。
另外有一頁別跳過。左上角的視圖下拉選單裡有個 Diagnostics Summary,錄影當下的狀況全記在這,事件有沒有掉、buffer 有沒有滿、哪個功能沒開成,它都會老實招。Day 10 第 0 步驗檢體驗的是有沒有貨,這一頁是檢體的品管報告,先掃一眼再開始讀片,免得對著一份殘缺的錄影認真半天。
挑一張卡,把它的 CUDA HW 放大到一圈的範圍,先看 kernel bar。認 bar 不用背幾千個名字,三類就夠用。名字帶 gemm 或 flash 的是矩陣乘法跟 attention,重計算的最大宗;名字帶 elementwise 的是逐元素小工,加加減減、activation 那掛;ncclDevKernel 開頭的是通訊,八張卡在對答案。認的時候有個免費幫手,同名的 kernel 塗同一個顏色,所以一圈的花紋才會重複,Day 4 說老手掃一眼認的是形狀,認的其實是色塊的節奏。另一個免費幫手是列名上掛的百分比,那是這條列的活動在上一層帳裡佔的時間比重,還沒放大就先有一張排行榜,最肥的 stream、最肥的 kernel 家族一眼先揪出來。滑鼠停在一條 bar 上別急著走,浮出來的小卡片會給你全名、起訖時間、耗時,還有 grid、block 這些發射參數。
讀 bar 讀的是體格。同一條列上,幾百 µs 一條的寬 bar,跟不到 10 µs 的小不點,是兩種完全不同的病理。寬 bar 佔滿版面,代表 GPU 大口大口在算,病歷上 top kernel 那格的 flash attention 三成,在片子上就是 forward、backward 段裡一排排最寬的條子,Day 13 拿它開刀。反過來,有一段遠看密密麻麻,放大再放大才散開成上百顆小點,每顆平均不到 10 µs,那是 launch 海的長相,Day 15 的病。體格用看的就分得出來,這是表格給不了的。

bar 認完,別忘了頭頂那棵 NVTX 樹,它讓你分段讀。對齊著看,forward 段跟 backward 段各是一群 bar,兩群長度一比,就是病歷卡上 fwd : bwd 那格的來源,backward 在 forward 的兩倍上下算正常,拉到三倍先想 activation checkpointing。小字註記,這棵樹掛在 CPU 執行緒上,格線是下單時間,跟 GPU 的 bar 會有一點錯位,粗讀沒差,要對到微秒等級,得用 Day 8 的投影版再核一次。這種分段的讀法很值錢,同樣是肥 bar,長在 forward 還是 backward,之後動刀的位置完全不同;optimizer 段要是攤開全是小點,那又是另一科的病。
洞就是發呆。但這裡先立一條今天最重要的規矩,單獨一條 stream 的空白,不算數。
為什麼,因為一張卡上同時有好幾條佇列在跑(下一節馬上講)。計算那條空了,可能通訊那條正忙;搬運那條停了,可能計算正大口在算。要判斷這張卡此刻是不是真的閒著,得拿一把尺垂直切下去,同一時刻,這張卡所有 stream 上一顆 kernel 都沒有,才是真發呆。Day 5 掀蓋子的時候講過,算真 idle 要把所有 stream 的區間做聯集、再拿 wall time 去減,眼睛版的聯集,就是這一刀垂直切。

認得真洞之後,再看洞長在哪。圈與圈之間規律出現的縫,一圈一次、寬度差不多,通常是 optimizer 收尾、loader 補資料或 logging 這類每圈固定要做的事。圈內不定時冒出來的大洞,比較像同步等待或有誰卡住了。位置不同,嫌疑人完全不同。
洞有多大也不用瞎猜。在時間軸上把一個洞拖選起來,選取範圍的時間長度就顯示在上面的尺標,一個洞幾 ms 直接讀出來,拿一圈的長度一除就是佔比。這是今天最土但最實用的量法,體檢卡上那格 idle 粗估,來源之一就是這樣一個洞一個洞圈出來的。
同一招還能拿來量圈。挑三五圈各拖選一次,長度應該幾乎一樣,訓練的 steady state 就長這樣。圈長要是忽長忽短,先回 Day 7 檢查窗是不是圈到暖機;不然就是每隔幾圈混進了存 checkpoint、跑 evaluation 這種週期工,把那幾圈標出來,別讓它們污染你的 baseline。
再補一手 Day 4 教過的問診。看到洞,點洞前後的 kernel,順著那條連回 CPU 的線往上看。CPU 那排忙得團團轉,八成是 CPU 拖後腿,指令發不贏;CPU 也一起閒著,那就是大家都在等某個更慢的人,等資料,或等隔壁那張卡。洞是病徵,這兩條線索才開始指向病因,Day 14 正式確診。
CPU 那排忙,也要看它在忙什麼。CUDA API 列上要是躺著一條拉得老長的 cudaDeviceSynchronize 或 cudaStreamSynchronize,那不是忙,是罰站。同步這一刀會把管線截斷,CPU 停在原地等 GPU 把佇列清空,佇列一抽乾,洞就跟著出現,所以洞的正上方常常就壓著一條 sync bar,這正是 Day 5 analyze 那條 cuda_api_sync 規則在時間軸上的長相。程式裡一個 .item()、一個手癢加的 synchronize,都做得出這種洞。
再進階一點,量下單跟出貨的距離。Day 4 說過 CUDA API 是下單、CUDA HW 是出貨,健康的非同步訓練,CPU 會遠遠跑在前面,一顆 kernel 從下單到真正開跑隔著一長段,代表佇列裡排滿了貨,GPU 做完一顆接得上下一顆。反過來,出貨緊貼著下單,佇列見底,GPU 做完手上這顆就只能乾等下一張單,洞就是這樣一格一格長出來的。這個距離 Day 15 會拿數字量,今天先會用眼睛抓。
今天的新功課來了。Day 4 介紹 CUDA HW 的時候只分了兩條,kernel 在算、memory 在搬,那是總帳的看法。把這條列再往下點開,你會發現底下還摺著好幾條,每條前面標著 Stream 幾號。其中多半有一條叫 Default stream 的,那是 CUDA 的預設佇列;NCCL 的通訊 kernel 幾乎不走它,自己另開 stream 跑。
stream 是 GPU 上的工作佇列,規矩兩句話講完。同一條 stream 裡的工作乖乖排隊,一個做完才輪下一個;不同 stream 之間沒有先後,可以同時跑。框架就是靠這個玩重疊的,計算走一條,NCCL 通訊自己一條,資料搬運再一條,目的只有一個,讓通訊跟搬運躲在計算後面做,不要佔用整張卡的時間。搬運能躲得特別徹底,因為 GPU 上有獨立的 copy engine 負責 DMA,搬資料根本不佔 SM;通訊就沒這麼便宜,NCCL 的 kernel 也是 kernel,重疊時會分走一小部分 SM,所以 overlap 不是零成本,只是比整張卡空等便宜太多。
讀 stream 的方法,就是把計算那條跟 NCCL 那條上下並排,對著讀。
好的長相,NCCL 的 bar 在跑,同一時段計算那條照樣排滿。通訊確實花了時間,但它躲在計算後面,帳面上近乎免費。壞的長相,NCCL 的 bar 拉得老長,底下計算那條整段空白,這段時間整張卡就只在對答案,八張卡手牽手一起等。病歷上通訊一成五那個紅字,值不值得緊張,就看這一成五裡有幾段是這種獨佔的長相。有重疊的通訊是成本,沒重疊的通訊是浪費,這個比例 Day 19 會拿工具正式量,今天先學會用眼睛分。

這裡先打一支預防針,NCCL 的 bar 不是純通訊。集合通訊要八張卡到齊才動得了,最慢的那張還沒進場,先到的七張就在 kernel 裡空轉等人,這些等待通通算進 NCCL bar 的長度。所以通訊 bar 特別長,不一定是網路慢,常常只是有人遲到,這也是 util 把等待全記成忙的老把戲。帳怎麼拆是 Day 18 的事,遲到的人怎麼抓是 Day 20 的事,今天先記住,NCCL bar 的長度是通訊加等待的總和,別急著怪網路。
同一套讀法搬到搬運身上也成立。Day 4 的第三件事是看 memory 條,今天拿 stream 的眼光重讀它。memcpy 落在哪條 stream,本身就是線索,跟計算擠同一條,它天生就得排隊,想重疊也重疊不了;被框架挪到自己的搬運流上,才有資格躲到計算後面。並排照樣問那個問題,它有沒有躲起來,還是一條長長的 memcpy 擋在每圈開頭、第一顆 kernel 只能在它後面排隊。後者就是 loader 的稅,每圈的資料都卡在門口,Day 16 專門收拾它。順手滑到一條 memcpy 上,小卡片會給你這筆搬運的 bytes 數,新一點的版本連換算好的頻寬都直接寫在上面,Day 10 手算的有效頻寬,這裡有單條版可以抄。
stream 讀完,最後把鏡頭拉遠,讓八組 CUDA HW 一起入鏡。先講清楚畫面結構,Megatron 這類訓練一張卡配一個 process,所以左邊是八組,各組有自己的 CPU 執行緒跟自己的 CUDA HW;八個 process 全錄進同一份檔,Day 9 那道 fork 門保的就是這件事。更要緊的是八組共用同一把時間尺,跨卡的先後可以直接比。八張卡跑同一個訓練,每圈的邊界理論上要對齊,像八條泳道同時轉身。要是有一張卡老是慢半拍,其他七張的 NCCL bar 就會拉長等它,一張卡的病拖垮整台機器,這叫 straggler,Day 20 的戲,今天先認個臉。
會診第一天的產出不是新數字,是把昨天病歷上的紅字,跟片子上的病徵對起來。
發呆兩成,在片子上是哪幾種洞,圈間的縫多、還是圈內的大洞多,垂直切下去是不是真的全空。通訊一成五,有幾段是獨佔的長相、幾段躲在計算後面。對得上,紅字就從一個數字變成一個看得見的現場;對不上,先懷疑自己站錯位置,回 NVTX 樹重新找一圈,再切一次。
給個對帳的手感(數字示意)。一圈 800 ms,圈內幾個真洞拖選起來加一加 150 ms 上下,將近兩成,跟病歷那格對得上,這格就能結案。要是你圈了半天只湊得出 5%,別急著懷疑眼睛,先檢查是不是有些洞其實不是洞,垂直切下去別條 stream 還在跑,那種假空白最會騙人。
也老實說,眼睛的極限就到這裡。洞的總量、重疊的比例,用看的只能給粗估,要精確就得回去寫 SQL 做聯集,Day 5 你見識過那有多繞。看得到、算不動,這正好是明天的開場白。
今天你把 Day 4 那份錄影重新讀了一遍,這次讀出來的不是列名,是病徵。三件事收好。bar 看體格,寬 bar 是 Day 13 的刀口、小不點海是 Day 15 的病。洞要垂直切,所有 stream 全空才算真發呆。stream 對著讀,通訊有沒有躲在計算後面,一眼就分。
明天 Day 12,換工具進場。這十一天你人肉練出來的讀法,聯集、佔比、重疊,有一整套疊在 nsys 上的工具幫你一鍵跑完。人肉讀片的功夫不會白練,因為工具吐出來的每一個數字,你從此都看得懂它在講片子上的哪一塊。
片子看完了,病徵也指認過了。明天,讓專科儀器進場。
GindaChen/nsys-hero 的 distca-0/baseline.t128k.host-fs-mbz-gpu-899.nsys-rep。