Day 17 對齊完 MFU 的口徑後,主檔 A 還有一筆大帳沒有解釋:kernel 時間裡有將近三成掛在 ncclDevKernel 開頭的名字底下。Part D 從今天開始處理多卡,第一步先不急著調 NCCL,而是回答三個比較基本的問題:
第三題很重要。通訊 kernel 跑了 300 ms,不代表 step 一定因此增加 300 ms;如果它和其他工作同時執行,其中一部分成本已經被藏起來。今天先把帳分清楚,明天再談如何增加 overlap。
先用最常見的資料平行(data parallelism)建立量感。每張 GPU 拿不同資料,對同一份模型做 forward 與 backward;backward 結束後,每張卡只有自己那批資料算出的梯度。若下一步要維持相同模型狀態,所有 data-parallel ranks 就必須同步梯度或更新後的參數。
經典 DDP 使用 AllReduce:先把所有 rank 的梯度加總,再讓每個 rank 都拿到相同結果。若以環狀(ring)演算法實作,ReduceScatter 與 AllGather 各走 N−1 個階段,因此每個 rank 的送出量約為:
每卡送出量 ≈ 2 × (N−1) / N × payload
接收量也相同。假設模型有 8B 參數,且要同步的是 BF16 梯度,payload 約 16 GB;8 張卡做 ring AllReduce 時,每張卡每步約送出:
2 × 7 / 8 × 16 GB ≈ 28 GB
這只是量級示範,不是主檔 A 的實測。真實 payload 取決於梯度通訊精度、bucket、參數分片與框架設定;如果梯度以 FP32 歸約,資料量還會再翻倍。不過這個例子足以說明:通訊不是程式不小心多做的雜事,而是分散式訓練為了維持一致狀態必須支付的成本。
NCCL 文件還給了一條很實用的語意等式:
AllReduce = ReduceScatter + AllGather
ReduceScatter 負責歸約後只留下每個 rank 的一份 shard,AllGather 再把所有 shards 集回完整結果。這表示兩個 collective 接在一起可以得到 AllReduce 的結果,但不代表一次 ncclAllReduce 在 timeline 上一定會拆成兩個獨立 kernel。
原本草稿把所有 ZeRO 都寫成通訊量與 AllReduce 相同,這樣太簡化。比較準確的說法是:
Tensor parallelism 傳的又是另一種資料。它通常在每一層交換 activation 或 activation gradient,資料量跟 microbatch、序列長度、hidden dimension 與層數有關;MoE expert parallelism 則常用 All-to-All,把 token 送到 expert 所在的 rank。先確認「在傳參數、梯度還是 activation」,通訊量公式才有意義。
| 平行化資料流 | 常見傳輸內容 | timeline 常見 collective |
|---|---|---|
| DDP | 梯度 | AllReduce,或分桶後的多次 AllReduce |
| Distributed optimizer/ZeRO-1/2 | 梯度 shard、更新後參數 | ReduceScatter+AllGather |
| FSDP/ZeRO-3 | layer/unit 參數、梯度 shard | 多次 AllGather+ReduceScatter |
| TP/sequence parallel | activation、activation gradient | AllGather、ReduceScatter、AllReduce |
| MoE expert parallel | 送往不同 experts 的 tokens | All-to-All 或等價交換 |

這次不只看先前整理的表。我使用本機同步到 upstream 的 nsys-ai(v0.3.0-21-g1cc03b5),對同一份 megatron.sqlite 與 39.964–46.123 秒的五步穩定區間重新執行。
這是 --nproc_per_node=8、--num-gpus-per-node=8、--tp-size=8 的作業,TARGET_INFO_GPU 也列出 device 0–7,因此預期確實是八張 GPU。不過直接查 kernel table,結果只有 device 1–7:
SELECT deviceId, COUNT(*) AS kernels
FROM CUPTI_ACTIVITY_KIND_KERNEL
GROUP BY deviceId
ORDER BY deviceId;
deviceId kernels
-------- -------
1 6460
2 6460
3 6460
4 6460
5 6460
6 6460
7 6460
device 0 不只沒有 NCCL,整份檔案連一般 kernel、memcpy 與 memset 都沒有;但它的 CUDA context 和 NVTX events 仍然存在。用 nsys-ai 指定 device 0 也會分別得到 No NCCL collectives found 和 Overlap analysis: no kernels found。
所以正確解讀不是「這個 job 只有七張卡」,而是「八卡 job 的這份 profile 只錄到 device 1–7 的 GPU activity」。以下跨卡加總與範圍都只涵蓋這七張,不能當成完整八卡 job 的總量;device 0 也不能用其他卡的平均值補上。這個 capture 缺口本身就是分析前應先做的完整性檢查。
確認限制後,先從有資料的 device 4 開始:
nsys-ai skill run nccl_breakdown megatron.sqlite \
--trim 39.964 46.123 -p device=4
實際輸出:
NCCL Collective Breakdown
[Stream 52]
allgather 1.2% 22.4ms ×4 avg=5.6ms [5.3–6.0ms]
[Stream 56]
allgather 63.5% 1145.5ms ×135 avg=8.5ms [5.3–57.4ms]
reducescatter 34.8% 628.5ms ×90 avg=7.0ms [5.3–55.8ms]
[Stream 60]
allreduce 0.4% 7.5ms ×10 avg=0.7ms [0.1–1.4ms]
這裡的百分比是「device 4 的 NCCL 時間組成」,不是占 step wall time 的比例。主通訊都在 stream 56:五步共有 135 次 AllGather 與 90 次 ReduceScatter,換成每步就是 27 次與 18 次;另外兩條 stream 每步再加約 4.5 ms 與 1.5 ms。三條合計每步約 361 ms,而不是把不同裝置的時間全部相加。
nccl_breakdown 會按 collective 類型與 stream 分組;若要保留完整 kernel symbol,再用底層 SQL 交叉檢查:
SELECT s.value AS kernel_name,
COUNT(*) AS calls,
SUM(k.end - k.start) / 1e6 AS total_ms
FROM CUPTI_ACTIVITY_KIND_KERNEL k
JOIN StringIds s ON k.demangledName = s.id
WHERE s.value LIKE 'ncclDevKernel%'
AND k.start >= 39964000000
AND k.end <= 46123000000
GROUP BY s.value
ORDER BY total_ms DESC;
把有 kernel 記錄的 device 1–7 加總後,結果和原本表格一致:
| A 檔實測(五步 × 已錄到的 7 卡) | 次數 | 總時間 | 平均 |
|---|---|---|---|
AllGather_RING_LL |
973 | 7,727.83 ms | 7.942 ms |
ReduceScatter_Sum_bf16_RING_LL |
630 | 4,654.17 ms | 7.388 ms |
AllReduce_Sum_f32_TREE_LL |
70 | 46.95 ms | 0.671 ms |
有記錄的七張卡,collective 次數都是 239 次,表示呼叫數量相當整齊;但總 NCCL 時間仍從 1,710.19 到 1,842.57 ms 不等。次數一致不代表抵達時間與完成時間也一致,後面還要看每張卡的 overlap。
主檔 A 的 symbol 大致長這樣:
ncclDevKernel_<操作>[_<歸約>_<資料型別>]_<演算法提示>_<協定提示>
以 ncclDevKernel_ReduceScatter_Sum_bf16_RING_LL 為例:
ReduceScatter 是 collective 類型。Sum_bf16 表示 reduction 與資料型別;AllGather 不做 reduction,因此名稱裡沒有這一段,也不能只靠 symbol 判斷它搬的是 BF16 還是其他 dtype。RING_LL 看起來像演算法與 protocol,但要把它當成 kernel specialization 的提示,不能直接當作 runtime tuning 的真值。最後一點很容易踩雷。NCCL 的 device code 會讓多種 collective 實作共用代表性的 specialized kernel symbol;目前 NVIDIA/NCCL 的產生程式甚至會把多個實際 protocol 映到名稱帶 _LL 的 kernel entry。換句話說,timeline 看到 _LL,不代表這次傳輸一定真的選了 LL。不同 NCCL 版本的 symbol 也可能改變,它不是穩定的公開診斷介面。
因此,原稿根據三個 _LL 名稱推論「所有 collective 都走 LL,所以有效頻寬最多一半」並不成立。若要確認實際演算法與 protocol,應在可重跑的環境開 NCCL tuning log:
NCCL_DEBUG=INFO \
NCCL_DEBUG_SUBSYS=INIT,GRAPH,TUNING \
python train.py ...
必要時再用 NCCL_ALGO 或 NCCL_PROTO 做 A/B test,但不要因為 kernel 名稱長得像 _LL 就直接強制切換。先用 nccl-tests 建立同拓撲、同訊息大小的 baseline,再比較 training run,會比猜 symbol 可靠。
另一個限制是訊息大小。NCCL 2.16.2 的 release notes 記載 NVTX instrumentation 開始加入 call arguments;但 payload 最後能不能出現在 .sqlite,仍取決於 NCCL、Nsight Systems 與匯出版本。主檔 A 的相關欄位沒有可用 payload,所以目前只能確認次數與時間,無法從這份檔案直接算每一筆 GB/s。看不到 bytes 不代表傳輸量是零,而是證據不完整。

拿到 message size 之後,下一個常見錯誤是直接用 payload ÷ kernel time 跟 NVLink 規格相比。NCCL tests 會同時報兩個不同數字:
algorithm bandwidth(algbw)= payload size ÷ elapsed time
這個數字回答「應用程式完成這個 collective 的速度」。但 collective 會在多個 ranks 間搬動資料,實際鏈路流量不一定只等於一份 payload,因此 NCCL tests 再用 operation-specific factor 換算 bus bandwidth:
Ring AllReduce: busbw = algbw × 2(N−1)/N
AllGather / RS: busbw = algbw × (N−1)/N
前者適合看端到端完成速度,後者則把 collective 的演算法流量納入換算,讓不同 rank 數的結果比較接近同一個硬體頻寬尺度。若使用 NVLS、CollNet 或分層演算法,busbw 又不一定等於某一條實體 link 的流量。實務上最穩妥的比較方式,是用相同 GPU 拓撲與相近 payload 跑 all_reduce_perf、all_gather_perf 或 reduce_scatter_perf,再把訓練中的時間跟 baseline 對照。
主檔 A 缺 message size,因此本文不硬算頻寬。現階段能可靠回答的是 collective 類型、呼叫次數、duration 分布與 overlap;頻寬效率要等 bytes 或 NCCL log 補齊。
collective 的組合可以縮小範圍,但很少能單獨定案:
| 主要組合 | 常見來源 | 可以下的結論 |
|---|---|---|
| 大宗 AllReduce | DDP 梯度同步 | 可能是 replicated data-parallel path |
| 大宗 AllGather+ReduceScatter | distributed optimizer、FSDP/ZeRO-3、TP sequence parallelism、部分 context parallelism | 存在分片式資料流,但不知道切的是參數、梯度或 activation |
| 大宗 All-to-All | MoE expert parallelism、Ulysses 類 sequence parallelism | 很可能在做重新分配,但仍要看 communicator 與 call site |
| Send/Recv | pipeline parallelism、ring attention/context parallelism | 有點對點交換,不能只靠名字判定是哪一種 |
套回 A 檔,大宗確實是 AllGather 與 BF16 ReduceScatter,所以可以合理判斷它有固定、頻繁的分片式資料流。Megatron distributed optimizer 會做 gradient ReduceScatter,再 AllGather 更新後的參數;sequence parallel 的 autograd mapping 也會讓 forward 的 AllGather 在 backward 對應 ReduceScatter,反之亦然。兩種路徑都能產生目前看到的組合。
A 在五步穩定區間、已錄七張卡的比例是 973:630 ≈ 1.54,接近 1.5:1,而且每張有記錄的卡都是 239 次。這表示呼叫形狀很規律,可能跟 layer 或固定 bucket 排程有關;但它不能證明一定是 sequence parallelism。Distributed optimizer 的參數 AllGather、gradient accumulation、recomputation 與 bucket 邊界都會改變比值。要定案,仍需 NVTX call site、communicator ranks 或 training config。
已錄到的每個 device 都有 10 次 FP32 AllReduce,因此:
10 ÷ 5 steps = 每卡每步 2 次
它們平均只跑 0.671 ms,顯然和兩個主要 collective 不是同一種時間尺度。gradient norm、overflow flag、token/loss 統計,或未分片參數的梯度聚合,都可能產生這類固定頻率的 FP32 AllReduce;但沒有 message size 與 call site 前,文章不替它指定唯一用途。這裡能確定的是「每步固定兩次、FP32、duration 短」,其餘保留成待驗證假說。
device 4 的通訊分布在 stream 52、56、60。原稿說「每個 communicator 各用自己的 stream,所以至少有三個通訊群組」,這個推論不可靠。NCCL API 會把操作 enqueue 到呼叫者傳入的 CUDA stream;同一 communicator 可以在框架控制下使用不同 stream,多個 communicator 也可能共用排程方式。stream ID 告訴我們工作被放在哪條 CUDA 執行佇列,不能直接換算成 TP/DP/PP group 數量。
不過時間分布仍然有用:
三條合計每步約 47.8 次 collective,平均每 26 ms 左右就出現一次。這種固定而密集的節奏,比「每步只在尾端做一次大同步」更像 layer-wise 或 bucketed communication;但 DDP 也會在 backward 中按 bucket 提前啟動 AllReduce,所以仍只能當線索,不能只憑分布替框架下診斷。
把視野拉出穩定區間,還能看到訓練開始前的 Broadcast,以及結束時的 communicator teardown。一次性的 initialization/shutdown 要和每步重複的 steady state 分開,只有後者會持續影響 tokens/s。
另外要記得,NCCL kernel 是真的 GPU kernel。它會佔用 SM 與執行資源,也會被 Day 14 的 busy time、Day 17 的 kernel union 算進去;GPU 顯示很忙時,其中一部分可能是在通訊。另一方面,collective 必須等參與 ranks 的資料都到位才能完成,因此某張卡上的 NCCL kernel duration 也可能包含等待 peer 的時間。單筆變長不一定代表 link 變慢,也可能是上游計算或 launch 較晚。
接著對同一個 device 4 與五步範圍執行:
nsys-ai skill run overlap_breakdown megatron.sqlite \
--trim 39.964 46.123 -p device=4
實際結果:
── Compute/Communication Overlap ──
Total span: 6155.8ms
Compute only: 4260.4ms
NCCL only: 1094.7ms
Overlap: 709.2ms (39.3% of NCCL overlapped)
Idle: 91.4ms
Kernels: 1875 compute + 239 NCCL
Devices: 4 (analyzed); profile also has [1, 2, 3, 5, 6, 7] — re-run with -p device=N for each
這裡的 compute 是工具的分類名稱:實作上,它把名稱不含 NCCL 的 GPU kernels 都歸在這一側,所以更精確的理解是「其他 GPU kernels」,不保證每一個都是 Tensor Core 計算。四個區間是用聯集與交集算的,不是把 kernel duration 直接相加。
device 4 的 NCCL 總時間為 1094.74 + 709.19 = 1803.93 ms,占 6.156 秒範圍的 29.3%;其中 39.3% 與其他 GPU kernels 重疊,未重疊部分是 1094.74 ms,平均每步約 219 ms。
但只看 device 4 還不夠。我把 profile 裡有 kernel 資料的 device 1–7 全部重跑一次:
| device | NCCL overlap | NCCL only(五步) | 平均每步 NCCL only |
|---|---|---|---|
| 1 | 34.3% | 1,129.27 ms | 225.85 ms |
| 2 | 35.3% | 1,106.14 ms | 221.23 ms |
| 3 | 36.2% | 1,137.00 ms | 227.40 ms |
| 4 | 39.3% | 1,094.74 ms | 218.95 ms |
| 5 | 35.9% | 1,180.34 ms | 236.07 ms |
| 6 | 33.8% | 1,177.84 ms | 235.57 ms |
| 7 | 38.7% | 1,099.37 ms | 219.87 ms |
device 4 剛好是已錄七張卡中 overlap 最高、NCCL only 最少的一張。如果只挑它代表所有已錄裝置,結論會稍微樂觀。device 1–7 的 overlap 落在 33.8–39.3%,未與其他 kernels 重疊的 NCCL 則是每步約 219–236 ms;這個範圍比單一數字更適合當 Day 19 的起點。由於 device 0 缺 trace,本文不宣稱這就是完整八卡 job 的全域範圍。
最後再收緊一次用詞:NCCL only 是「沒有和其他 GPU kernels 重疊的通訊時間」,可以當作改善 overlap 的機會上限,但不等於全部都能從 wall time 一比一拿回來。它可能仍和 CPU 工作、memory copy 或其他裝置活動重疊,也可能受無法移動的資料相依性限制。要估真正 critical-path saving,還需要比較修改前後的 step wall time。

第一,看到 _RING_LL 就當成實際 tuning 結果。kernel symbol 可能只是共用的 specialized entry;實際 algorithm/protocol 要看 NCCL tuning log 或受控的 A/B test。
第二,把 stream 數量當成 communicator 數量。stream 是 CUDA 排程佇列,不直接等於 TP、DP 或 PP group;要看 rank membership 與框架設定。
第三,把多卡 kernel duration 直接相加後除以 wall time。已錄七張卡的 12.43 秒是跨卡總和,不是 6.16 秒視窗內的 wall 占比。先固定 device,再做 interval union。
第四,把 NCCL only 全部當成可回收時間。它是未和其他 GPU kernels 重疊的區間,也是值得優先調查的上限;能否真的縮短 step,仍取決於相依性、straggler 與修改後的 critical path。
今天先把 NCCL 的帳讀到證據允許的程度。Ring AllReduce 的每卡流量約為 2(N−1)/N × payload,但 ZeRO/FSDP 不同階段的通訊時機與總量不能混成一句話。主檔 A 預期有八張 GPU,實際 SQLite 卻只有 device 1–7 的 GPU activity;已錄七張都有規律的 AllGather、BF16 ReduceScatter,以及每步兩次的 FP32 AllReduce,足以判斷存在分片式資料流,還不足以只靠 kernel 名稱定案是哪種平行化。
實際用 nccl_breakdown 與 overlap_breakdown 重跑後,device 1–7 每步都有 239 ÷ 5 ≈ 47.8 次 collective;NCCL overlap 落在 33.8–39.3%,NCCL only 約 219–236 ms/step。這段未藏住的通訊是明天要追的重點,但先把它當機會上限,不急著宣稱全部可回收;缺少的 device 0 則需要重新 capture 才能補齊。
明天進 Day 19:正式拆 overlap ratio、啟動時機與資料相依性,看看主檔 A 的通訊為什麼只有約三分之一藏進其他 GPU 工作裡。
ReduceScatter + AllGather = AllReduce:NCCL User Guide — Collective Operations
src/device/generate.py
algbw、busbw 與各 collective 的換算:NVIDIA/nccl-tests — Performance
nsys-ai 實跑 megatron.sqlite 的 39.964–46.123 秒範圍;預期的八張 GPU 中,只有 device 1–7 有 kernel activity,這七張皆分別執行 nccl_breakdown 與 overlap_breakdown。device 0 缺少 CUDA trace,因此所有跨卡表格都不是完整八卡總量。