昨天那張榜查下來算是虛驚一場,flash attention 吃三成吃得有道理,整體表現也沒有異常。今天這位就沒那麼好打發了,病歷首頁真正圈紅字的第一位,GPU idle 兩成(Day 10 那張示意卡)。Day 11 你用眼睛垂直切過那些洞,今天摘第二瓣 gpu_idle_gaps,把每一個洞點名造冊,一個一個問清楚來歷。
先把話講重一點。發呆這病跟昨天的肥不一樣,肥還有得吵該不該,發呆連吵的餘地都沒有,GPU 空轉的每一毫秒都是純浪費,跳表照跳、電照燒、租金照付,沒有任何一種解釋能把它講成有用功。訓練是這樣,推論(inference)伺服器的情況可能更明顯,batch 之間的空檔、冷啟動(cold start)的等待,全是同一種病的變形,Day 28 換場景再算那筆帳。
先算錢。Day 1 的報價,8 卡 H100 一天一千三百美元上下,idle 兩成就是一天兩百六十美元、一個月快八千美元,什麼都沒算出來的錢。對比一下昨天那刀,換一版 attention 實作,整體省 10%、月省四千,你就知道為什麼分診表把發呆掛在第一位,可改善的空間甚至比昨天的 kernel 那刀還大。
再套 Amdahl,發呆有一點是其他病都比不上的。昨天的公式裡,加速倍率 s 頂多是個有限的數字,kernel 再怎麼改也要花時間算。但洞不用算得更快,它可以直接消失,等於 s 是無限大,兩成的發呆全部刪掉,整體就是 1 ÷ (1 − 0.2) = 1.25 倍,而且過程中你一顆 kernel 都不用寫。天花板不是三種病裡最高的,但改善成本通常最低,這就是最好賺的意思。
順便給 Day 3 那支錶補最後一刀。idle 兩成的這張卡,nvidia-smi 可能整場給你九成以上的 util,因為它的定義是取樣窗內有沒有任何一顆 kernel 在跑,而名冊上的洞多數是幾 ms 到幾十 ms,剛好全躲在取樣的縫裡。每月八千美元的空轉,錶上一格都看不到,這就是整個系列喊到現在的那句話,忙不等於有用,今天它有了名冊跟金額。

指令照舊,窗照舊:
nsys-ai skill run gpu_idle_gaps \
baseline.t128k.host-fs-mbz-gpu-899.nsys-rep --trim 39 42
吐出來的單子有三層(結構是真的,數字示意):
── GPU Idle Gaps (Bubbles) ──
Total: 47 gaps, 731.5ms idle (24.4% of profile)
Device-level idle: 601.2ms
Distribution: 12 × 1-5ms, 30 × 5-50ms, 5 × >50ms
Stream Gap(ms) Before Kernel Attribution
──────────────────────────────────────────────────────────────────────
14 88.412 ncclDevKernel_AllReduce... synchronization
└─ cudaStreamSynchronize: 61.20ms
7 46.108 vectorized_elementwise_kernel synchronization
└─ cudaMemcpyAsync: 29.83ms
└─ cudaHostAlloc: 11.31ms
7 18.755 flash::flash_fwd_kernel...
第一層是總帳,幾個洞、加起來多久、佔窗多少。第二層是分布(distribution),把洞按大小分桶,幾顆 1 到 5 ms 的、幾顆 5 到 50 的、幾顆超過 50 的。三個桶各有嫌疑犯的味道,超過 50 ms 的多半是同步(synchronization)罰站或週期工,5 到 50 的常是等資料、等別張卡,1 到 5 的一般是圈界的縫;而且帳往往頭重腳輕,示意單上最右桶只有 5 顆,加總卻可能吃掉半數 idle,名冊排在最前面的那幾條大魚先辦,跟昨天的 80/20 是同一個道理。第三層是名冊,一洞一列,哪條 stream、多寬、洞前最後一顆 kernel 是誰,最值錢的是後面那欄歸因(attribution),這個洞期間 CPU 側哪些 CUDA API 在忙,直接印給你。
先兌現一句 Day 12 的預告。Day 5 說真 idle 要全 stream 聯集(union)、要自己寫 SQL,Day 10 你只能粗估,Day 11 你拿眼睛垂直切,今天這個 Device-level idle 就是那個聯集的精確值,三天的欠帳到今天全部結清。順手對個帳,Day 10 卡上粗估兩成、Day 11 拖選加總也在兩成上下、今天精確值 20% 出頭,三次收斂,也能確認前面的目測大致準確。
單子底下沒有魔法,講一遍它怎麼算的,你就能覆算。洞,是同一條 stream 上相鄰兩顆 kernel 的時間差,後一顆的 start 減前一顆的 end,大於門檻(threshold)的留下;Device-level,就是 Day 5 那句聯集 SQL 的程式版。歸因更是土法煉鋼,拿洞的起訖區間去跟 CUDA API 那張表(Day 5 掀過的 RUNTIME)做時間交集,洞期間誰在 CPU 側活動、各活動多久,排出來就是那幾行縮排。JSON 版每個洞還帶著 before_kernel、after_kernel 跟 start_ns,洞的前後鄰居跟座標都給你,待會全用得上。
這張單子有三個地方藏著判準,比數字本身還值錢。
第一個機關,兩個總數會岔開,而且岔的成分要看懂。Total 那行是照 stream 記的口徑,每條佇列自己的斷點自己算,而且只收過了門檻的洞,示意單加總 731 ms;Device-level 那行是全 stream 聯集後的口徑,不吃門檻,601 ms。兩股力同時在拉,多 stream 的假空白把 Total 往上撐(某條斷了、別條還在跑,Day 11 那課的數字版),門檻把 Total 往下削(1 ms 以下的縫沒入帳)。訓練檔通常假空白佔上風,岔開的 130 ms 大宗就是它;反過來,單 stream 的檔你會看到 Total 反而比 Device-level 小一點點,那是門檻削掉的零頭,我在一份單卡推論檔上實測過,差了 5 ms 出頭。用法不變,看總帳引用 Device-level(真發呆,垂直切口徑),修洞看名冊(斷的是哪條隊伍);真要精算假空白,把 min_gap_ns 降到很小再相減才乾淨。極端情況 Day 12 的 known limits 也預告過,stream 很多的推論伺服器,per-stream 口徑可以加出超過窗長的 idle,別嚇到,這是統計口徑造成的,不是資料異常。另外提防一個小陷阱,名冊只列前幾大(limit 預設 20),它是頭部不是全量,別拿名冊自己加總去當總帳,總帳在最上面那兩行。

第二個機關,min_gap_ns 預設一百萬奈秒,也就是 1 ms。抓個比例感,一圈 800 ms 的訓練,1 ms 的洞是一圈的千分之一點二五,這個門檻篩掉的是真正的零頭。小於 1 ms 的縫預設不列,所以這一瓣天生是看大洞的,kernel 之間 µs 級的天然縫隙不會灌爆你的名冊。反過來說,要是你的發呆是幾萬條碎縫堆出來的,這張單子會顯得乾淨得可疑,總帳高、名冊卻沒幾條大魚,這時候看分布那層,最小的桶爆量,就是碎縫病,那是明天那瓣的地盤,今天別降 min_gap_ns 去撈,撈出來你也讀不完。你發現了嗎,這跟昨天 Count 配 Avg 的判讀方式其實相同,洞也有體格,幾顆大洞是一種病,萬條碎縫是另一種。
第三個機關,device 參數預設 0 號卡,一次只驗一張。八卡的檔要摘八次,一個迴圈的事:
for d in 0 1 2 3 4 5 6 7; do
nsys-ai skill run gpu_idle_gaps \
baseline.t128k.host-fs-mbz-gpu-899.nsys-rep \
--trim 39 42 -p device=$d --format json
done
JSON 的最後一列是 _summary,gap 數、總 idle、Device-level、佔比、三桶分布全打包在那,八輪各抄那一列,湊成病歷上的一行。這個麻煩其實是提醒,多卡的發呆不能平均,七張卡 15%、一張卡 45%,平均起來還是兩成上下,但病根完全不同。判準給一條,八個佔比的最大減最小超過十個百分點,先查最高那張,它就是 straggler 的頭號嫌疑犯,Day 20 專門辦它。平均值會替犯人打掩護,分布才會指認。
名冊拿到了,確診開始。發呆不是一種病,是一類病的總稱,每個洞都要問出來歷才能掛號,而歸因欄跟洞的長相,就是指紋。
歸因欄滿滿的 cudaMemcpyAsync、cudaHostAlloc,洞的期間 CPU 在搬資料、配置記憶體,這是資料餵不上的發呆,轉診 Day 16。歸因指向 cudaStreamSynchronize、cudaDeviceSynchronize,同步把管線截斷,Day 11 講過的罰站。不過這裡多想一層,sync 是現場逮到的人,不一定是主謀,真正要問的是為什麼有人叫了同步,常見的答案是每一步都在 .item() 讀 loss、進度條每圈跟 GPU 對一次數字、或框架在某個角落偷偷幫你同步。判準是看規律,同一個位置每圈準時出現的 sync 洞,是程式碼寫出來的,去 code 裡抓;偶發不定點的,先想系統跟資源。洞的前一顆 kernel 是 NCCL,這洞多半是等別張卡到齊,單卡治不了,Day 18 到 20 的戲。分布最左桶爆量、名冊卻抓不到大魚,碎縫病,CPU 下單跟不上,明天確診。還有一種稀有大洞,每隔固定圈數出現一次、又特別巨大,那是週期工,checkpoint、evaluation、Python 的 GC,Day 11 量圈的時候你就見過它們的影子。
拿示意名冊那三條現場走一遍,你就知道指紋怎麼對。第一條,88 ms 的洞,洞前是 AllReduce、歸因掛著 cudaStreamSynchronize 61 ms,等卡跟同步攪在一起,掛通訊科,病歷備註一句 Day 19 要查 overlap。第二條,46 ms,歸因是 cudaMemcpyAsync 加 cudaHostAlloc,可以優先往資料管線調查,而且 hostAlloc 出現在這裡本身就是一條線索,pinned memory 應該開場配置好重複用,有人在迴圈裡一次一次配置,這種發現具體到可以直接開 issue。第三條,18.7 ms,歸因欄空白,CPU 側安靜得很,洞前又剛好是 forward 的第一顆 kernel,八成是圈界的縫,拿 start_ns 回 NVTX 樹對一下就能定案。三個洞,三張處方,這就是名冊的用法。
還有一手,拿名冊裡的 start 時間回 timeline 對位。JSON 版每個洞都帶 start_ns,切回 Day 11 的片子,看這個洞落在 NVTX 樹的哪一段,data 段的洞跟 backward 段的洞是兩種病,同一個寬度、兩張處方。牌是 Day 8 掛的,這時候全回本。before_kernel 那欄也一樣好用,洞前老是同一顆 kernel,代表管線有個固定斷點,某個階段做完固定沒人接棒,去看那個階段的下游;洞前五花八門,比較像隨機的等待,往資源跟系統那邊想。
掛完科,補一道門檻再進病歷。同一科的洞全部加總,佔窗不到 2% 的直接歸檔,不開處方,Amdahl 昨天才教過,改善空間太小的不值得動工。剩下的照修理成本排,同步病通常最便宜,常常刪一行程式就好;資料病最貴,動的是管線結構。省的錢除以工程成本,昨天桌上那三個數字,今天原班人馬再用一次。
病歷的發呆頁照這個格式記,之後交接跟 diff 都省事。第一行總帳,Device-level 佔比加八卡分布(最大最小標出來);第二行科別統計,同步幾成、資料幾成、通訊幾成、圈界幾成;然後前五大洞各記一行,座標、寬度、科別。三行加五條,一頁講完一個紅字,Day 27 優化完,同一個格式再填一次,兩頁一疊就是成績單。

一個重要的提醒,今天只開名冊、不動手修。每個洞掛完科,寫進病歷,就收工。看到 memcpy 歸因就衝去改 dataloader,跟 Day 10 罵過的看到可疑就撲上去是同一種病,會診的紀律是先把全部病灶掛完號,再照 Amdahl 排優先序。
順手把 Day 10 的三分鐘體檢升級一版。SOP 第 1 步四張表跑完之後,補一行今天這瓣,病歷首頁的 idle 那格從此不再是粗估,是精確值加一份名冊。SOP 是活的,學到新招就縫進去,這也是當初把它寫成固定流程的原因,流程才有地方讓你升級。
第一種,把搬運中的時段全當純發呆。idle 的定義是 kernel 聯集的空隙,一段時間沒有 kernel、但 memcpy 條滿滿的,帳面上是洞,實際上是搬運在獨佔(Day 11 沒重疊的那種長相)。這種洞的歸因欄會自己招,memcpy 一排,所以它的科別是資料,不是發呆本身,掛錯科藥就開錯。
第二種,窗污染。窗裡混進 warmup、混進存 checkpoint 的那幾圈,idle 佔比直接爆表,你以為病入膏肓,其實是圈錯窗。Day 11 教的量圈先做,挑三五個長度穩定的圈再摘,Day 12 的 --iteration 選窗在這裡最好用。
第三種,拿 0 號卡代表全機。device 預設 0,摘一次就當全體檢完,偏偏 0 號卡常常是最忙的那張(rank 0 通常多扛雜事)或最閒的那張,單卡代表不了八卡。老話,八張各摘,排排站再說話。
第二瓣摘完了,收三件事。發呆是最貴也最好賺的病,兩成的洞就是每月八千美元,而且刪掉它不用寫 kernel。單子有三層三機關,Device-level 是真發呆、名冊照 stream、兩者的岔大宗是假空白;1 ms 以下預設不列,碎縫看分布;一次一張卡,八卡排排站。最後,洞要問來歷,歸因欄加洞的長相就是指紋,指紋對科,名冊進病歷。
主檔 A 的這格也結案了,兩成的發呆裡,名冊點出了同步的罰站、資料的縫、等卡的洞(示意),每一個都掛了科。但有一種病今天的名冊抓不到大魚,分布最左邊那桶卻擠得不像話,幾萬條 1 ms 不到的碎縫,單條不起眼,加總很嚇人。
明天 Day 15,摘第三瓣,kernel_launch_overhead。碎縫病的兇手通常不在 GPU 身上,是前面那個下單的跟不上,我們去看看 CPU 是怎麼把一張好卡拖垮的。
gpu_idle_gaps 的參數(min_gap_ns 預設 1,000,000 ns、limit、device)與輸出結構(總帳、分布、per-stream 名冊、CPU 歸因):nsys-ai docs — user/skills(以 0.3.0 實測為準)。GindaChen/nsys-hero 的 8 卡 Megatron .nsys-rep。