前面幾天都在追時間:GPU 有沒有閒著、kernel launch 卡多久、資料搬了多少。今天換一個角度,不只問 GPU 忙不忙,而是問這些時間最後換回多少「模型本來就需要做的運算」。
這是 MFU(Model FLOPs Utilization)要回答的問題。它把模型的理論運算量、實測的 step time,以及硬體峰值放進同一個式子,讓不同層次的觀察收斂成一個數字。不過 MFU 也很容易算錯:分子算一個 step、分母卻圈到五個 step,或把 kernel union 當成 wall time,最後都會得到一個看似精確、實際上無法解讀的百分比。
今天會用 theoretical_flops、region_mfu 與 arithmetic_intensity 三個 skill,依序處理這三件事:
MFU 的定義可寫成:
該 step 的模型 FLOPs
MFU = ─────────────────────────────────────────────────
step wall time × GPU 數 × 單卡 dense 峰值 FLOPS
三個輸入來自不同地方:
| 輸入 | 從哪裡來 | 最常出錯的地方 |
|---|---|---|
| 模型 FLOPs | 模型架構、global batch、序列長度與訓練階段 | 少算 backward、多算 checkpoint 重算,或用總參數量估 MoE |
| step wall time | 穩定區間內一個完整 training step 的實測時間 | 分子是一個 step,時間卻包含多個 step 或只有部分 region |
| 硬體峰值 | GPU 規格、精度、卡數 | 用到含 structured sparsity 的峰值,或卡數與分子範圍不一致 |
以 H200 SXM 為例,NVIDIA 規格表列出的 BF16 Tensor Core 峰值是 1,979 TFLOPS,但註腳寫的是 with sparsity。一般 dense 訓練應使用一半,也就是每張約 989 TFLOPS;HBM3e 頻寬則是 4.8 TB/s。規格表上的大數字不能直接抄,精度與 sparsity 模式都要先對齊。
MFU 與 HFU(Hardware FLOPs Utilization)也不要混在一起。MFU 的分子只算模型完成這一步本來需要的運算;HFU 則會把硬體實際執行的額外運算也算進去。最常見的差異是 activation checkpointing:backward 前重做的 forward 對硬體是真實工作,但不是模型數學上新增的工作,因此 HFU 會提高,MFU 不會。
另外,MFU 沒有一條適用所有模型的及格線。Llama 3 405B 論文在不同平行化與序列長度階段報告過 38–43% 的 BF16 MFU,這可以當大型 dense training 的實例,但不能直接拿來要求小模型、MoE、長序列或不同硬體也達到相同數字。先確認演算法與口徑相同,百分比才有比較價值。

region_mfu 不知道模型架構,所以每個 step 的理論 FLOPs 要由使用者提供。theoretical_flops 可以把常見 Transformer 區塊的公式集中算完,避免手動乘大數字時出錯。
下面先用一個標準 dense decoder 的尺寸示範:hidden dimension 4096、32 層、序列長度 8192、FFN dimension 16384。這個例子採一般 multi-head attention 與兩層 FFN,並不是在宣稱它等同某一個公開模型。
nsys-ai skill run theoretical_flops training.sqlite \
-p operation=full_model \
-p hidden_dim=4096 -p seq_len=8192 \
-p num_layers=32 -p ffn_dim=16384 \
-p batch_size=1 -p multiplier=3
輸出會把 attention、QKV projection、output projection 與 MLP 分開計算:
── Theoretical FLOPs ──
Operation: full_model
Total FLOPs: 422,212,465,065,984
Formula: (attention=1.0995e+12 + qkv_proj=8.2463e+11
+ output_proj=2.7488e+11 + mlp=2.1990e+12)
* L(32) * batch(1) * mul(3)
這裡的 422.21 TFLOPs 是一個序列完成 forward 與 backward 的估算。multiplier=1 代表只算 forward;multiplier=3 用 forward 的三倍近似 forward 加 backward,是訓練 MFU 常用的口徑。若整個模型都做 activation checkpointing,multiplier=4 可以近似硬體實際執行的 FLOPs,但它比較接近 HFU 分子,不應拿來把 MFU 墊高。實際只 checkpoint 部分層時,重算量也不會剛好是完整的一倍。
6 × 參數量 × token 數 仍然可以當快速檢查。假設模型有 8B 參數、一步處理 8192 tokens,訓練 FLOPs 約為 393.2 TFLOPs,跟逐層公式應落在相近量級。不過它只是一階估算:長序列的 attention 有平方項;GQA 的 K/V projection 比標準 MHA 少;SwiGLU 有三個線性投影;MoE 又只啟用部分 experts。要報正式 MFU,還是得依真實模型結構修正。
這個參數最容易讓數字差上好幾倍。原則只有一句:分子要涵蓋 NVTX region 內實際完成的全部模型工作。
如果 sample_0 包住一個 optimizer step,而這一步的 global batch 是 512 個序列,那麼分子就要包含這 512 個序列,以及 gradient accumulation 期間做過的所有 microbatches。若 region 只包一個 microbatch,就只能填那個 microbatch 的 FLOPs。多卡資料平行時,不要一邊填 global FLOPs,一邊又只除單卡峰值。
在執行 region_mfu 前,先確認四件事:
theoretical_flops 是否涵蓋同一段工作?以下是一個可直接替換數字的範本。假設選第 3 次出現的完整 step,分子是整個 8-GPU job 在這一步完成的 global FLOPs:
nsys-ai skill run region_mfu training.sqlite \
-p name=sample_0 \
-p occurrence_index=3 \
-p theoretical_flops=422212465065984 \
-p num_gpus=8 \
-p peak_tflops=989 \
--format json
occurrence_index 是從 1 開始,預設值也是 1,也就是只選第一個符合名稱的 NVTX range,不是自動把所有同名 region 加總。若想避開 warmup,可以先看 Day 7 找到的穩定區間,再指定其中一次 occurrence。若真的分析一個包住五個 step 的外層 region,分子也必須乘上五;不能只把時間加總。
草稿原先有這組數字:
theoretical FLOPs: 131.94 TFLOPs (一個 forward)
kernel union: 2.628 秒 (一個包住五步的 region)
131.94 ÷ 2.628 = 50.2 TFLOPS
50.2 ÷ 989 = 5.1%
除法本身沒錯,但範圍不一致:分子只有一次 forward,分母卻跨了五個 step,而且訓練還少了 backward。這個 5.1% 反映的是輸入錯配,不是主檔 A 的效率。最安全的做法是一次分析一個完整 step;若要分析多步平均,就讓 FLOPs、wall time 與 step 數一起放大。
主檔 A 的模型規格尚未公開,因此目前不能填出可信的正式 MFU。這不是少一個小數點,而是少了整個分子;直接借 Llama 8B 的 FLOPs 只適合示範欄位,不能拿來判斷 A 跑得好不好。
region_mfu 會整理三種時間:
wall_time_s:NVTX region 從開始到結束的完整經過時間。gpu_kernel_sum_s:每個 kernel duration 直接相加;多 stream 或多 GPU 同時執行時會重複計算。gpu_kernel_union_s:把重疊的 kernel interval 合併,只計算至少有一個被歸屬 kernel 在執行的時間。PaLM 定義與一般訓練報告所說的 MFU,分母是 wall time:
MFU_wall = model FLOPs ÷ (wall time × effective peak)
region_mfu 的人類可讀輸出目前主要顯示 MFU (union);用 --format json 才能同時檢查 mfu_pct_wall 與 mfu_pct_kernel_union。後者可寫成:
active-time ratio = model FLOPs ÷ (kernel union × effective peak)
在 FLOPs 與 GPU 範圍完全相同時,兩者可換算:
MFU_wall = active-time ratio × (kernel union ÷ wall time)
但 union 口徑不是正式 MFU,也不能直接叫作「計算 kernel 效率」。只要 NCCL 通訊是 GPU kernel,它就會包含在 union 裡;資料重排、記憶體型 kernel 也一樣。union 只表示這段時間有被歸屬的 GPU kernel 在跑,不保證它正在做模型 FLOPs。
反過來說,wall − union 代表沒有被這批 kernel 覆蓋的時間,可能包含 CPU 提交、同步等待、launch gap,或沒有被正確歸屬的工作;它也不能直接當成「通訊暴露時間」。通訊是否和計算重疊,要另外用 overlap 類分析按時間軸計算。
多 GPU 還有一個細節:未指定 device_id 時,union 會合併多張卡的時間區間,比較接近「至少一張 GPU 正在忙」。要診斷單卡 kernel-active ratio,最好固定 device_id;要報整個訓練系統的 MFU,則回到 global FLOPs、完整 step wall time 與全部參與 GPU 的峰值。

MFU 告訴我們端到端用了多少 dense peak,但不會直接回答為什麼。Roofline 把問題再拆成兩個硬體上限:
可達效能 ≤ min(峰值 FLOPS,運算密度 × 記憶體頻寬)
運算密度(Arithmetic Intensity)= FLOPs ÷ HBM 搬運 bytes
橫軸是 FLOP/byte,縱軸是 FLOP/s,通常都用對數刻度。低運算密度落在斜屋頂下方,主要受記憶體頻寬限制;跨過 ridge point 後,屋頂變平,才進入主要受計算峰值限制的區域。
H200 SXM 的 dense BF16 峰值約 989 TFLOPS,HBM3e 頻寬 4.8 TB/s,因此 ridge point 是:
989 TFLOPS ÷ 4.8 TB/s ≈ 206 FLOP/byte
先看一個理想化的 BF16 elementwise add。每個輸出要讀兩個 BF16 輸入、寫一個 BF16 輸出,共 6 bytes,只做一次加法:
AI ≈ 1 FLOP ÷ 6 bytes = 0.167 FLOP/byte
頻寬屋頂 ≈ 0.167 × 4.8 = 0.8 TFLOPS
它離 989 TFLOPS 很遠,不代表 kernel 寫壞,而是這個演算法每搬一批資料本來就只做很少運算。融合多個 elementwise op 的價值,正是少讀寫幾次 HBM,讓每個 byte 承擔更多運算。
方陣 GEMM 則在另一端。若 A、B 各讀一次、C 寫一次,BF16 的 N × N GEMM 理想運算密度約為 N ÷ 3;N=4096 時約 1365 FLOP/byte,已經越過 206 的 ridge point,有機會接近計算屋頂。這是理想下限流量,真實結果仍會受 tile、cache reuse、矩陣形狀與 Tensor Core 使用方式影響。
attention 位在兩者之間,而且實作方式會改變位置。標準 attention 若把大型中間矩陣反覆寫回 HBM,記憶體流量會升高;FlashAttention 透過 tiling,減少 HBM 與 on-chip SRAM 之間的讀寫,在不改變 exact attention 結果的前提下提高運算密度。圖上的 attention 點因此只畫方向,不是假裝成實測座標。

Nsight Systems 的 .sqlite 有 kernel 時間軸,但沒有每個 kernel 真正執行的 FLOPs 與 HBM bytes。arithmetic_intensity 因此需要使用者補上分子;若要做真正的 Roofline 分類,還要補 bytes_moved。
nsys-ai skill run arithmetic_intensity training.sqlite \
--trim 39 42 \
-p device=4 \
-p theoretical_flops=<device_4_在這三秒內完成的總_FLOPs> \
-p bytes_moved=<同一範圍的_HBM_bytes> \
-p peak_tflops=989 \
-p hbm_bw_gbps=4800
這裡再次強調「同一範圍」。如果 --trim 39 42 圈到三個 step,就要提供三個 step 在 device 4 上對應的 FLOPs;拿單一步 FLOPs 去除三秒 kernel union,一樣會得到假的低效率。
bytes_moved 沒填時,工具仍可依理論 FLOPs 與 kernel union 算 achieved TFLOPS,但沒有辦法得到 FLOPs / byte,也就不能真的判斷點在 ridge point 左邊還是右邊。此時輸出的 likely memory-bound 只是依 MFU 高低做的 heuristic,不能當成 Roofline 證據。要取得逐 kernel 的實測 FLOPs 與記憶體流量,仍要用 Nsight Compute 的硬體 counter,Day 21 會接著做。
主檔 A 曾自動顯示 3350 GB/s,原因是 profile 內的 chip name 先匹配到 GH100/H100 規格;H200 應使用 4800 GB/s。這類自動偵測錯誤不只影響標籤,還會把 ridge point 從 206 推到約 295 FLOP/byte。執行時明確覆寫 peak_tflops 與 hbm_bw_gbps,也把硬體口徑寫進報告,比相信自動值安全。
Roofline 還有一個邊界:它分析的是工作執行時的算術與資料流量,無法單獨解釋 CPU launch、跨 rank 同步、pipeline bubble 等系統問題。單一 GEMM 可以很貼近屋頂,整個 step 的 wall MFU 仍然可能很低;兩者並不矛盾。
目前可以確定的是:A 在穩定區間內的 device-level idle 很低,Day 16 也看到 NCCL kernel 佔了顯著時間。但這兩個觀察還不能直接相減,推成「計算最多只剩七成」。通訊 kernel 可能和計算重疊,kernel sum 也可能重複計時;必須先用 union 或 overlap 的共同時間軸對齊,才有辦法算互斥比例。
因此,A 的 MFU 這一格先誠實留白。模型資訊齊全後,至少要補上:
假設未來確認某一步的模型 FLOPs 是 3.9 PFLOPs、wall time 是 1.232 秒,並由 8 張 H200 以 BF16 dense 執行,才可以這樣算:
可供應 FLOPs = 1.232 × 8 × 989 TFLOPS ≈ 9.75 PFLOPs
MFU = 3.9 ÷ 9.75 ≈ 40%
這裡的 3.9 PFLOPs 只是示範值,不是對主檔 A 模型的猜測。40% 的意思也不是「另外 60% 的 GPU 成本全部浪費」;記憶體存取、通訊與同步是完成分散式訓練所需的成本。比較精確的說法是:在這套 FLOPs 定義下,模型必要運算的完成速率相當於 dense 理論峰值的 40%。
對 dense Transformer,還可以從近似式換算吞吐量:
tokens/s ≈ MFU × GPU 數 × 單卡峰值 FLOPS ÷ (6 × 參數量)
它適合做快速 sanity check,不適合直接套在長序列、MoE、額外 loss 或非標準 block 上。日常監控仍可看 tokens/s,正式比較再把模型 FLOPs 定義與 MFU 一起保存。
第一,分子與時間範圍不同。一個 step 的 FLOPs 除以五個 step 的時間,數字再精確也沒有意義。先對齊 NVTX occurrence,再做除法。
第二,把 union 口徑當成正式 MFU。union 可以協助判斷 GPU-active 期間的上限,但標準 MFU 使用完整 step wall time;對外報告一定要寫清楚分母。
第三,MFU 低就判定 memory-bound。沒有 HBM bytes 就沒有 arithmetic intensity;低 MFU 也可能來自通訊、launch、同步、矩陣太小,或 FLOPs 分子本身算錯。
第四,峰值拿到 sparse 規格。H200 BF16 的 1,979 TFLOPS 含 structured sparsity;一般 dense training 應用 989 TFLOPS。精度、sparsity 與卡型少一個沒對齊,MFU 就會成倍偏差。
今天最重要的不是記住某個百分比,而是記住範圍:模型 FLOPs、step time、GPU 數與硬體峰值必須描述同一段工作。正式 MFU 用 wall time;kernel union 是診斷用的 active-time 口徑,而且包含 NCCL 等非模型運算。Roofline 則要同時有 FLOPs 與 HBM bytes,才能把 memory-bound 和 compute-bound 放上座標。
到這裡,Part C 從 kernel 時間、idle、launch、資料搬運一路接到 MFU,量測口徑算是收完整了。明天進 Part D,開始拆主檔 A 最明顯的 NCCL 通訊:為什麼時間主要落在 AllGather 與 ReduceScatter,以及它們和模型平行化有什麼關係。
theoretical_flops、region_mfu 與 arithmetic_intensity 的實作:nsys-ai v0.3.0 source