Day 18 的結尾留下一個問題:主檔 A 已錄到的 device 1–7,NCCL 與其他 GPU kernel 的 overlap 落在 33.8–39.3%。也就是說,大部分通訊還是單獨出現在 timeline 上。
不過,只看一個平均百分比還不夠。今天要繼續追三件事:
本文沿用同一份主檔 A、同一段 sample_0(repeat=5),分析範圍是 39.964–46.123 秒。單卡細節以 device 4 為主,再用 device 1 與 6 檢查型態是否只出現在單一卡上。工具是實際執行的 nsys-ai v0.3.0-21-g1cc03b5,不是示意資料。
先提醒一個容易混淆的範圍:這是八卡作業,CPU 端的 NCCL NVTX payload 留有 8 個 ranks;但這份 profile 的 GPU kernel timeline 只錄到 device 1–7。因此 payload 次數要用 8 ranks 換算,單卡 kernel 分析則只能談已錄到的 7 張卡,兩邊不能混著除。
先把 timeline 簡化成兩條 lane:一條放 NCCL kernel,另一條放其餘 GPU kernel。把每條 lane 內互相重疊的區間先取聯集,再比較兩條 lane,就能把整段時間拆成四個互斥部分:
Holistic Trace Analysis 採用的 communication-computation overlap 定義是:
overlap ratio = NCCL 與其他 GPU kernel 重疊的時間 ÷ NCCL 總時間
它回答的是「通訊有多少比例與其他 GPU 工作同時發生」。另一個也常用、但分母不同的數字是:
NCCL-only fraction = 只有 NCCL kernel 的時間 ÷ 整段 wall time
後者回答的是「整段時間裡,有多少比例只有 NCCL kernel 在跑」。它不是可直接回收的時間,也不等於通訊造成的 critical path;資料相依、CPU 工作與 copy engine 都可能影響實際能省下多少時間。
先看 device 4 的 overlap_breakdown 實際輸出:
Total span: 6155.8ms
Compute only: 4260.4ms
NCCL only: 1094.7ms
Overlap: 709.2ms (39.3% of NCCL overlapped)
Idle: 91.4ms
四個數字相加正好是 6,155.8 ms,這是檢查區間拆分是否重複計算的第一關。這裡的 Idle 也要照工具口徑解讀:它只表示「NCCL 與其他 GPU kernel 兩條 lane 都沒有事件」,不保證整張 GPU 完全沒事做,因為同一時間仍可能有 memcpy 或其他引擎在工作。
如果只做算術,把 1,094.7 ms 的 NCCL-only 全部從 6,155.8 ms 扣掉,五步會剩 5,061.1 ms,平均每步約 1.012 秒。這可以當成一個很寬鬆的理論上限,用來判斷值不值得繼續查;不能把它當成打開某個開關後就能得到的預測。通訊有相依性,重疊也可能和計算爭用 GPU 資源,實際收益通常會小於這個算術上限。

Day 18 還無法從 kernel 名稱讀出每筆傳輸的 bytes。這次改用 nccl_payload_breakdown,解開 NCCL 寫在 NVTX 上的 typed payload:
nsys-ai skill run nccl_payload_breakdown megatron.sqlite \
--trim 39.964 46.123
── NCCL Payload Breakdown ──
Total payload events: 1,920
Message-carrying: 1,920 (458.96 GiB total)
Distinct schemas: 3
[collective] schema=16777220 calls=1,120 communicators=9
msg_size: p50=256.00 MiB p99=256.00 MiB total=278.96 GiB
[collective] schema=16777221 calls= 80 communicators=1
msg_size: p50=0.00 MiB p99=0.00 MiB total=0.00 GiB
[collective] schema=16777223 calls= 720 communicators=1
msg_size: p50=256.00 MiB p99=256.00 MiB total=180.00 GiB
終端輸出四捨五入到 MiB,看不出第二個 schema 的大小,所以我又讀了 JSON 的原始 bytes,並核對每種大小的次數:
| 推定 collective | 事件數 | message size 分布 | 每 rank、每步 |
|---|---|---|---|
| AllGather | 1,120 | 1,080 筆 × 256 MiB;40 筆 × 229.3 MiB | 27 × 256 MiB + 1 × 229.3 MiB |
| AllReduce | 80 | 全部都是 4 bytes | 2 × 4 bytes |
| ReduceScatter | 720 | 全部都是 256 MiB | 18 × 256 MiB |
事件數也能反向核對五個 step:
AllGather: 1,120 ÷ 8 ranks ÷ 5 steps = 28 次/rank/step
AllReduce: 80 ÷ 8 ranks ÷ 5 steps = 2 次/rank/step
ReduceScatter: 720 ÷ 8 ranks ÷ 5 steps = 18 次/rank/step
這裡有三個值得記下來的觀察。
第一,主力 collective 的 message size 很規律,但不是每一筆都一樣。ReduceScatter 全是 256 MiB;AllGather 每個 rank、每步有 27 筆 256 MiB,另有 1 筆約 229.3 MiB。256 MiB 若用 BF16 表示,相當於 2²⁷ 個元素,符合大型張量分片的量級;要進一步還原成 sequence length、hidden size 或特定 layer,仍需要模型設定,不能只憑 bytes 猜形狀。
第二,AllReduce 不是「接近零」,而是每筆精確的 4 bytes。這已經可以排除 MiB 等級的參數梯度 bucket,更像某個 FP32 純量的同步。不過純量究竟是 loss、token 數、旗標,還是其他統計值,仍要回到 call site 才能命名。
第三,每 rank 每步的邏輯 payload 約為:
27 × 256 MiB + 1 × 229.3 MiB + 18 × 256 MiB ≈ 11.47 GiB
乘上 8 ranks、5 steps,會得到表頭的 458.96 GiB。這是 NCCL 呼叫側記錄的邏輯 payload,不是 NVLink 上實際搬動的 bytes;collective 演算法會讓實體鏈路流量再乘上對應係數,Day 18 講的 algbw 與 busbw 差異就是從這裡來的。
有了 message size,我原本也嘗試用 nccl_communicator_analysis 直接算頻寬。不過同一個範圍內,它列出的 AllGather/ReduceScatter/AllReduce 次數是 1,129/744/76,和 payload 已核對的 1,120/720/80 對不起來。因此本篇不採用它輸出的 33–37 GB/s,也不拿這個數字除以 H200 的 900 GB/s 宣稱「NVLink 只用了 4%」。
這是一個很實用的驗證習慣:工具同時輸出 count、bytes 和 duration 時,先確認事件數能不能和其他視角閉合,再談速率。即使事件配對正確,message size ÷ collective duration 也只是應用層的有效速率;duration 可能包含等待其他 rank,而 collective 的邏輯 payload 又不等於單條鏈路上的 bytes,所以不能直接視為實體鏈路利用率。若要判斷通訊本身是否跑得慢,還是 rank 到得不整齊,仍需要同拓撲、同大小的 nccl-tests baseline 和 straggler 分析。
先用 kernel_overlap_matrix 把 device 4 的 collective 拆開。以下只節錄和本文有關的欄位:
nsys-ai skill run kernel_overlap_matrix megatron.sqlite \
--trim 39.964 46.123 -p device=4
compute allgather reducescatter
compute 4969.6ms 386.0ms 323.2ms
allgather 386.0ms 1167.9ms 0.0ms
reducescatter 323.2ms 0.0ms 628.5ms
NCCL overlap efficiency: 39.3% of NCCL time hidden behind compute
AllGather 的 1,167.9 ms 裡有 386.0 ms 與其他 GPU kernel 重疊,約 33.0%;ReduceScatter 的 628.5 ms 裡重疊 323.2 ms,約 51.4%。另外還有 7.5 ms 的 AllReduce 沒有和其他 GPU kernel 重疊。光是拆到 collective 類型,就已經看到接近 20 個百分點的差距。
接著我用 SQL 讀出 kernel interval,再以 flash attention kernel 當錨點,把五個 step 粗分成 forward 與 backward:相鄰 flash_fwd 間隔超過 300 ms 時視為新 step;每步從第一個 flash_fwd 到第一個 flash_bwd 算 forward,之後算 backward。這不是 nsys-ai 內建輸出,也不是精確的框架 phase annotation,所以只能拿來看結構,不能把邊界附近的單筆 kernel 過度解讀。
device 4 的結果如下:
| 操作 × 相位 | NCCL 時間 | 與其他 GPU kernel 重疊 | overlap |
|---|---|---|---|
| AllGather,forward | 608.4 ms | 296.6 ms | 48.8% |
| AllGather,backward | 554.2 ms | 89.4 ms | 16.1% |
| ReduceScatter,forward | 396.7 ms | 149.5 ms | 37.7% |
| ReduceScatter,backward | 225.9 ms | 173.7 ms | 76.9% |
四格的重疊時間合計 709.2 ms,和 overlap_breakdown 完全一致。平均值底下其實藏著 16.1% 到 76.9% 的巨大落差。
同樣的方法在 device 1 與 device 6 重跑後,型態沒有改變:
所以「單卡不能代表全體」仍然成立,但這次要追的主結構很穩定:backward AllGather 是最沒有藏好的那一格。

相位表只能告訴我們「發生在哪裡」,還不能直接回答「這筆資料是做什麼用」。幸好這份 profile 保留了 Transformer Engine 的 NVTX range,可以再往 call site 靠近一步。
我把 device 4 的 NCCL kernel 經由 CUDA runtime event 接回外層 NVTX,看到部分 backward AllGather 落在這兩種標籤下:
| Transformer Engine NVTX 標籤 | 五步內看到的 AllGather | 可以確定的事 |
|---|---|---|
LayerNormLinear.backward.column_parallel_comm_input |
40 筆 | column-parallel backward 正在收集 input |
Linear.backward.grad_output_preprocess |
40 筆 | backward 正在預處理 grad output;確切用途仍取決於平行化設定 |
再對照 Transformer Engine 現行原始碼,column_parallel_comm_input 這段會在需要時非同步呼叫 gather_along_first_dim:input 是稍後計算 weight gradient(wgrad)需要的資料,而中間的 data gradient(dgrad)就是可用來遮住這次通訊的計算窗口;進入 wgrad 前則必須等待 gather 完成。
這讓相位表有了更具體的解釋:backward AllGather 並不是等 dgrad 做完才開始,而是可以先非同步發起、嘗試和 dgrad 重疊;但在主檔 A 裡,device 4 只有 16.1% 藏住,表示可用的 dgrad 窗口或實際排程還不足以蓋住大部分通訊。這和 sequence/tensor parallel 的 backward 資料流相容,但不能把所有 backward AllGather 都歸成同一個原因,因為仍有其他 NVTX 標籤與未能精確配對的事件。
這個差別會直接影響調校方向:
--tp-comm-overlap 與 Transformer Engine userbuffers 路徑。--overlap-grad-reduce、--overlap-param-gather。換句話說,優化前要先把 collective 對回 call site,不能只看到「AllGather 很慢」就隨便開一個名稱相近的選項。
最後把 device 4 五步窗內「NCCL kernel 在跑、其他 GPU kernel 沒在跑」的區間逐段列出來。為了和 overlap_breakdown 採用相同邊界,這裡只納入完整落在 39.964–46.123 秒內的 kernel:
分布並不是某一個超大的停頓。93 段小於 2 ms,合計 35 ms,只占曝露時間約 3.2%;真正的主力是 140 段 4–6 ms 的區間,合計約 758 ms,占 69.3%。這比較像「每層都有一筆 collective 藏住一小段、剩下 4–6 ms 露在外面」,反覆累積後才變成 1.09 秒。
因此,最長的 12.9 ms 不一定是第一個該處理的目標;它就算完全消失,對五步也只省十幾毫秒。更值得先做的是把 4–6 ms 那批事件對回 layer、NVTX call site 和前後依賴,確認它們為什麼規律地出現,再決定適合提早發起、做融合,還是調整平行化配置。小於 2 ms 的 93 段也不是「不用管」,只是目前合計占比小,優先級可以排後面。

第一,把 overlap ratio 當成跨 job 的成績單。它的上限會受模型結構、平行化方式與資料相依影響;比較同一個 job 改動前後,通常比拿兩個不同工作負載直接比高低更有意義。
第二,只看平均 overlap。主檔 A 的 39.3% 底下,其實是 16.1% 到 76.9% 的四格。若不先拆 collective 和 phase,很可能把時間花在本來就藏得不錯的 backward ReduceScatter,卻漏掉真正曝露的 backward AllGather。
第三,把 message size ÷ duration 當成鏈路利用率。這個商最多只能描述應用層的有效速率;要談 NVLink 吃了幾成,至少還要確認事件配對、collective 流量係數、拓撲,以及 rank 間的等待。
今天先把 overlap 的口徑定清楚:device 4 有 709.2 ms 的 NCCL 與其他 GPU kernel 同時執行,占 NCCL 時間 39.3%;另有 1,094.7 ms 是 NCCL-only,占 wall time 17.8%,但不能直接當成可省下的時間。
message size 也補齊了。每 rank、每步有 28 次 AllGather、18 次 ReduceScatter 和 2 次 4-byte AllReduce,邏輯 payload 約 11.47 GiB。最重要的是,平均數拆開後,backward AllGather 的 overlap 只有 16.1–22.0%;NVTX call site 進一步顯示,其中一部分是 wgrad 前需要的 input gather。
最後,這 1.09 秒不是一個大洞,而是 273 段小等待累積,其中 4–6 ms 的區間占了約七成。下一步要看的就不是「NCCL 總共跑多久」,而是哪些 rank 或哪些 layer 反覆晚到,讓本來可以非同步發起的 collective 還是露在外面。
overlap_grad_reduce 與 overlap_param_gather 的語意:Megatron Core — DistributedDataParallelConfig
linear.py
nsys-ai v0.3.0-21-g1cc03b5 實跑 megatron.sqlite 的 39.964–46.123 秒;相位表與曝露段分布是針對同一範圍的 interval 聯集、交集與差集分析。