iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
AI Engineering

GPU很忙?他真的有在做事嗎?系列 第 22

Day 22|鵝鵝鵝!kernel 裡面在跑什麼指令?CUTracer 的三步流程

  • 分享至 

  • xImage
  •  

Day 21 的 ncu 像把儀表接到機器上:它能告訴我們 DRAM throughput、SM throughput 與 warp stall,卻不會逐條列出 kernel 到底執行了哪些 GPU 指令。今天把機器外殼再拆一層,直接看 SASS(GPU 組合語言)。

先講最重要的界線:**CUTracer 數的是動態執行的指令,不是每類指令花掉的時間。**它很適合回答「有沒有 tensor core 指令」「位址計算與格式轉換多不多」「同步指令是否異常密集」,但不能只看一張指令直方圖,就斷言 kernel 的時間耗在運算、記憶體或同步。

今天回答四件事:

  1. CUTracer 跟 nsys、ncu 各自看哪一層?
  2. plan、trace、analyze 三步要在哪裡跑?
  3. H100 的示範資料,為什麼會讓自動分類下錯結論?
  4. 換到 B200 重跑後,Blackwell SASS 又暴露了什麼解析邊界?

資料範圍提醒:主檔 A 只執行離線的 plan,還沒有回原訓練環境重跑;H100 trace 來自 Day 21 的最小示範程式。本文另外在 8×B200 主機的 device 1,以 CUDA 12.9、CUTracer v0.2.1 與 ncu 2025.2.1 重跑 vector add 與 BF16 GEMM,確認 trace 內容與工具邊界。B200 device 0–7 都存在,選 device 1 只是因為當時它是空的。

顯微鏡的第四層

把這個系列用過的工具排成一座塔,每往下一層,視野越窄、重跑成本越高:

工具 回答的問題 成本
體檢表 nvidia-smi GPU 忙不忙 幾乎為零
錄影 nsys 誰慢、慢在哪、跟誰有關 錄一次,離線反覆讀
臨檢 ncu 這顆 kernel 的吞吐、stall 與資源限制 視 metrics 需要一次或多次 replay;Day 21 的 full set 是 39 passes
顯微 CUTracer 這顆 kernel 動態執行了哪些 SASS 指令 NVBit 插樁重跑,逐 warp 計數

CUTracer 建立在 NVIDIA 的 NVBit 上。它把 instrumentation tool 注入目標程式,在 GPU 二進位層替指令加上計數邏輯,最後得到 per-warp instruction histogram。這和 ncu 的硬體 counter 不是同一種證據:前者描述指令組成,後者描述硬體單元的吞吐與等待狀態

實務上我會把 nsys、ncu 與 CUTracer 分開跑。這不是說任何版本都絕對不能共存,而是 NVBit 插樁本來就會改變程式的執行行為,工具疊加的相容性也跟 driver、CUDA 與容器權限有關。CUTracer 那次執行的時間不適合當 baseline;時間回 nsys 看,吞吐與 stall 回 ncu 看,CUTracer 專心回答指令組成。

顯微鏡的四層:nvidia-smi 看忙不忙、nsys 錄影回答誰慢在哪、ncu 對嫌疑 kernel 收吞吐與 stall counter、CUTracer 以 NVBit 注入逐 warp 計數;越往下視野越窄、重跑成本越高

三步流程:GPU 只負責中間一步

nsys-ai cutracer 把工作切成三步,但輸出格式有一個容易漏掉的差別:

1. plan     (不需 GPU) :從 nsys profile 排出候選 kernel,生成執行腳本
2. trace    (需要 GPU) :NVBit 注入重跑,先產生 raw NDJSON 與 cubin
3. analyze  (不需 GPU*):解析指令、對回 profile,再產生分類與提示

* 已解析的 CSV 可在 Mac 離線讀;raw trace 要有 nvdisasm,通常留在 CUDA Linux/Modal 解析。

先對主檔 A 跑第一步:

nsys-ai cutracer plan megatron.sqlite --top-n 5
CUTracer Plan — 5 kernel(s) selected from: megatron.sqlite

  Selected for instrumentation:
          ms       %    calls  value   category        Kernel
  ─────────────────────────────────────────────────────────────
    13919.94   10.0%     1785  HIGH    custom_gemm     nvjet_tst_128x320_64x3_2x1_v_bz_coopB_NNN
     6988.03    5.0%     1260  HIGH    custom_gemm     nvjet_tst_128x320_64x3_2x1_v_bz_coopB_TNN
     6132.46    4.4%      105  HIGH    custom_gemm     nvjet_tst_192x192_64x3_2x1_v_bz_coopB_NTN
     5568.71    4.0%      525  HIGH    custom_gemm     nvjet_tst_192x192_64x4_2x1_v_bz_coopB_TNN
     5510.66    4.0%      420  HIGH    custom_gemm     nvjet_tst_320x128_64x3_1x2_h_bz_coopB_NTT

  Skipped (LOW instrumentation value,以下僅節錄):
          ms       %  category        Reason
  ─────────────────────────────────────────────────────
    39809.77   28.7%  flash_attn      top-5 slots already filled by higher-priority kernels
    24494.24   17.7%  nccl_comms      NCCL collective — always bandwidth-bound
     1745.32    1.3%  elementwise     elementwise op — trivially memory-bound

這張表適合拿來排檢查順序,不適合直接當診斷書。NCCL collective — always bandwidth-boundelementwise — trivially memory-bound 是 planner 內建的分類規則,不是它對這份 profile 新量到的結論。Day 21 的 H100 elementwise 確實由 ncu 證實接近 DRAM 屋頂,但那只能支持那顆 kernel,不能外推成所有 elementwise 都必然受記憶體頻寬限制。

表格的 % 也有口徑:這一版是相對於進入 planner 候選池的 GPU duration,不是整份 profile 的 GPU 總時間。它適合在候選之間比較,不能直接拿來回答「這顆占整次訓練幾成」。

加上 --script 會生成 kernel filter、輸出目錄與 CUTracer 路徑都填好的執行腳本。把 LAUNCH_CMD 換成原本的訓練命令後,再帶回 A 的原機器執行。

沒有本機 GPU?trace 交給 Modal

nsys-ai cutracer run --backend modal 產生的是一份可編輯的 Modal 應用程式。它在 CUDA Linux 映像裡編譯 CUTracer、掛載輸出 Volume、執行目標程式,並把 raw trace 解析成方便帶回 Mac 的 histogram CSV。也就是說,「trace 直接得到 CSV」是 Modal workflow 幫忙做完後處理的結果;直接在 GPU 主機跑 upstream CUTracer,先拿到的是 NDJSON 與 cubin。

這次 H100 示範改了三處:

  • CUDA 基底映像改成 12.8,並讓 NVBit、CUTracer 與目標框架落在相容組合;
  • 加入 torch 與 Day 21 的 mini.py,因為生成的映像不會自動包含你的訓練程式;
  • kernel filter 改用 nvjet,vectorized_elementwise 家族前綴,避免把某台機器的完整變體名稱寫死。

filter 還有一個實測陷阱:CUDA kernel 名稱比對會區分大小寫,但本文使用的這版 nsys-ai runner 會把 filter 正規化成小寫。我們在 B200 用 vecAdd 測試時,命令以 exit code 0 結束,卻因實際比對成 vecadd 而沒有產生 trace。所以成功的判準不是程式有結束,而是輸出目錄真的有非空的 NDJSON、cubin 或 CSV。

版本也要一起記錄。這次 nsys-ai 的 Python dependency 允許 cutracer>=0.2.0,環境可能抓到較新的 Python package;安裝器實際編譯的上游原始碼則是 v0.2.1。重現結果時,至少要留下 nsys-ai commit、CUTracer Python package、CUTracer source、NVBit 與 CUDA 五個版本,不能只寫「有裝 CUTracer」。

同一台 H100 上,nsys 量到 nvjet GEMM 平均 175.1806 µs、elementwise 122.3166 µs;Day 21 的 ncu 分別是 174.75 與 121.73 µs,差約 0.25% 與 0.48%,也就是都在 0.5% 內。這次受限的 Modal 容器在開啟 CPU sampling 時會 segfault,加上 --sample=none --cpuctxsw=none -b none 才能錄 CUDA trace;這是該環境的實測限制,不是「Modal 一定不能做 CPU sampling」。

CUTracer 三步流程:plan 用規則排序候選、trace 在 GPU 產生 raw 指令紀錄、analyze 在確認名稱解析與架構支援後產生提示;planner 分類與自動 bottleneck 都是線索,必須再和 ncu 證據交叉檢查

H100:13 個 mnemonic,不是單一 warp

Modal H100 的 trace 抓到兩顆 kernel,但兩份輸出的解析程度不同:

kernel 動態指令計數總和 名稱解析狀態
elementwise add 72,500,000 13 個已解析 mnemonic
nvjet GEMM 80,999,864 826 個未解析 opcode ID/靜態指令列

所以不能寫成「GEMM 有 826 種指令、elementwise 只有 13 種」。前者仍是 OPCODE_138 這類識別碼,後者已經合併成 IMADLDG 等 mnemonic,兩邊不是同一個分類層級。mini 程式實際執行五次;CSV 裡的 count 也彙總了多個 launch 與 warp。

同理,CSV 裡只看到 warp_id=0,也不代表 CUTracer 只取樣一個 warp。Modal 後處理器把 histogram 聚合成一列時,會用 [0, 0, mnemonic, count, ...] 寫出合計值。若要回答到底涵蓋幾個 warp,要回 raw metadata 或未聚合的 trace 查,不能從這個合成欄位猜。

回到 Mac 執行最後一步:

nsys-ai cutracer analyze mini.sqlite ./cutracer_out
Kernel: _ZN2at…vectorized_elementwise_kernel…
Instruction Mix:
  ███████████████░░░░░  compute   74.1%
  ██░░░░░░░░░░░░░░░░░░  memory    12.1%
  ░░░░░░░░░░░░░░░░░░░░  control    1.7%
Bottleneck: COMPUTE

Day 21 的 ncu 明明顯示這顆 elementwise add 接近 DRAM 屋頂,為什麼這裡判成 COMPUTE?先把 13 個 mnemonic 的動態計數攤開:

指令 佔比 在做什麼
IMAD 31.0% 位址計算
PRMT/SHF 22.4% byte 重排與位移
FFMA 13.8% BF16 轉成 FP32 後執行加法
LDC/ULDC 12.1% 讀取常數與參數
F2FP 6.9% BF16/FP32 格式轉換
LDG+STG 5.2% global-memory load/store 指令
其他 8.6% S2R、ISETP、EXIT 等

這張表能告訴我們「位址計算與資料轉換的指令不少」,卻不能把 LDG+STG 的 5.2% 翻譯成「只有 5.2% 的成本在搬資料」。這裡的向量化 LDG 每個 participating lane 一次可載入 16 bytes;一筆 warp-level 動態指令最多同時涵蓋 32 個 lane。而且 LDGSTG 代表 global-memory 指令,請求可能由 cache 服務,不能一律寫成真的進出 DRAM。

ncu 的 DRAM Throughput 90.16% 也不是「90.16% 的時間在等 DRAM」,而是 DRAM 實際吞吐相對該硬體 peak rate 的比例。這顆 kernel 的完整證據是:DRAM 實測 3.02 TB/s、理論 DRAM roofline 達成率約 94.2%,而且 warp 每發一條指令,平均有 107.5 cycles 卡在 L1TEX scoreboard dependency,約占發射間隔的 92.5%。吞吐、roofline 與 stall 三項放在一起,才支持「主要在等記憶體資料就緒」。

同一顆 elementwise kernel 的兩種量測:CUTracer 顯示各 mnemonic 的動態執行次數,ncu 顯示相對峰值吞吐並以 scoreboard stall 補足等待原因;LDG 加 STG 僅占指令數 5.2%,不等於資料存取只占 5.2% 成本

B200:trace 看得到 UTCHMMA,analyze 卻說沒有

為了確認上述限制不是 H100 或 Modal 的偶發狀況,我在實體 B200 再做兩組測試。這台主機第一次用一般帳號執行 ncu 時回傳 ERR_NVGPUCTRPERM,改以有權限的帳號收硬體 counter 才成功;遇到這個錯誤時要先查 profiling permission,不是把「沒有 counter」誤判成 kernel 沒有活動。

第一組是 upstream CUTracer 的 vector add;每次 launch 的 raw metadata 有 608 筆動態 warp-instruction、32 個不同 warp,兩次 launch 合計 1,216 筆。常見指令如下:

mnemonic 每次 launch 的動態計數
LDC 160
IMAD 128
LDCU/LDG 各 64
S2R/S2UR/ISETP/DADD/STG/EXIT 各 32

nsys-ai 按 count 分類為 memory 42.1%、compute 36.8%,最後標成 memory bottleneck;但 ncu 實測這個小 kernel 的 DRAM throughput 是 54.77%、SM throughput 是 27.90%,兩者都不到 60%,並提醒要繼續查 latency 與 scheduler 狀態。這正好示範:count-based 分類可以幫忙提問題,不能代替硬體量測下結論。

第二組是 1024×1024 的 cuBLAS BF16 GEMM。CUTracer 的 log 與 SASS 明確抓到 Blackwell 指令:

UTMALDG / UTMASTG
UTCHMMA.2CTA

raw trace 共有 641,834 筆動態 warp-instruction、1,024 個 warp,UTCHMMA 執行 4,096 次。它只占動態指令數約 0.64%,但一次 tensor core 指令完成的工作量遠大於一般 scalar 指令;占比低不表示 tensor 運算不重要。

接著把同一份 trace 交給目前的 nsys-ai analyze,卻得到:

warning: no SASS function matching ...
instruction_mix_pct: {"other": 100.0}
tensor_core_active: false
bottleneck: balanced

這次不能怪 CUDA 太舊:同一台機器的 CUDA 12.9 nvdisasm,透過 cutracer sass 可以正常反組譯 cubin,也看得到 UTCHMMA.2CTA。問題在後處理器:現有 function mapping 沒吃到這份 Blackwell cubin 的標籤格式,Tensor Core opcode 分類也還沒涵蓋 UTCHMMA。所以 tensor_core_active: false 是假陰性,balanced 則是解析失敗後的誤導輸出,都不是這顆 GEMM 的量測結論。

這顆小 GEMM 的 ncu 結果同樣不能草率貼標籤:Compute throughput 16.83%、DRAM throughput 6.79%,而且 grid 只有一個完整 wave。這比較像工作量太小、無法把 B200 餵飽,不是「運算與記憶體剛好平衡」。

讀自動結論前,先過四關

第一,確認真的有 trace 到資料。exit code 0 不夠,還要看 NDJSON、cubin 或 CSV 是否存在且非空,kernel filter 的大小寫是否吻合。

第二,確認 mnemonic 已解析。如果輸出大多是 OPCODE_138 或直接落到 other 100%,後面的 instruction mix、tensor core 與 bottleneck 都先不要信。

第三,確認分類器支援這代架構。看到新的 Blackwell UTCHMMA,分類規則仍以 HMMAIMMABMMADMMA 等舊命名為主時,tensor_core_active: false 反而是在提醒我們檢查 parser。

第四,回 ncu 與 nsys 對口徑。CUTracer 的插樁時間不作數,planner 的 skip reason 只是 heuristic,count-based bottleneck 也是提示;真正要談時間、吞吐與等待,仍要回到 timeline、counter、stall 與 roofline。

小結與明天

顯微鏡塔到今天蓋完四層:nvidia-smi 看忙不忙、nsys 看誰慢在哪、ncu 看吞吐與 stall、CUTracer 看指令長什麼樣。CUTracer 的價值不是取代前面三層,而是補上它們沒有的 SASS 組成:位址計算有多少、格式轉換有多少、tensor 與同步指令到底有沒有出現。

但越靠近底層,越不能把工具印出的標籤直接當答案。H100 的 5.2% LDG+STG 不等於只有 5.2% 成本在搬資料;B200 明明執行了 UTCHMMA,舊 parser 仍可能回報 tensor_core_active: false先確認資料有抓到、名稱有解出來、架構規則跟得上,再談 bottleneck。

Part D 到此收官。明天開始的 Part E 要問一個新問題:這套「先驗證證據、再下結論」的流程,能不能交給 agent 代跑?第一課先從最現實的限制開始:一份 trace 塞不進 context window。

參考資料


上一篇
Day 21|kernel 為什麼慢?nsys 點名之後,換 Nsight Compute 臨檢
系列文
GPU很忙?他真的有在做事嗎?22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言