iT邦幫忙

2026 iThome 鐵人賽

DAY 25
1
AI Engineering

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

DaDay 25|把 GPU 判準交給 AI agent:nsys-ai 已經能做什麼,下一步一起補什麼?

  • 分享至 

  • xImage
  •  

Day 24 把 nsys-ai agent ask 拆開後,看到它會先執行 skills、保留來源,再用固定格式組裝答案;同時也抓到 full profile、device scope 與 idle 口徑混用的問題。先把今天的結論講在前面:nsys-ai 已經值得拿來縮短 GPU profile 的分診時間,但現階段更適合當可驗證的分析助手,不是按一下就自動定案的黑盒子。

它真正有用的地方,是把「我該先查哪張表、跑哪支 SQL」縮成一個問題,再把答案接回可重跑的 skill。也因為 skill、runner 與 renderer 都是可以讀、可以測的程式,我們不只能發現答案不對,還能把問題定位到哪一層,進一步改成測試與 pull request。

今天不評模型文筆,而是做一個更容易驗證的實驗:把時間窗口與 GPU 編號明確傳進去,輸出是否就能和人工分析公平比較?

這個問題看似只是多帶兩個參數,實際上要連過四關:runner 有沒有收到、每個 skill 有沒有採用、答案有沒有揭露實際 scope,以及最後的診斷有沒有使用正確口徑。少任何一關,輸出仍可能有來源、有數字、可以重跑,卻不是同條件比較。

資料範圍提醒:本文使用 nsys-ai commit 1cc03b5,不設定 LLM API key,走 deterministic 路徑。主檔 A 的 kernel activity 只有 device 1–7,各 6,460 筆;本次固定分析 Day 7 圈出的 39.964–46.123 秒,並以 device 4 對齊 Day 21 的單卡臨檢表。

先回答:為什麼現在就值得試 nsys-ai

面對一份陌生的 Nsight Systems profile,最花時間的往往不是執行查詢,而是決定先查 idle、launch、memory 還是 NCCL。agent ask 會先跑固定 triage,再依問題挑選最多四個 registered skills,最後把來源、量測窗、信心與 Verify 指令組成同一份回答。就算沒有 API key,仍有 deterministic 輸出;需要追細節時,也能跳過 agent,直接執行同一支 skill。

pip install 'nsys-ai[agent]'

# 先用問題縮小搜尋範圍
nsys-ai agent ask profile.sqlite "GPU idle 高嗎"

# 再用同一支 evidence skill 固定窗口與 GPU 覆核
nsys-ai skill run gpu_idle_gaps profile.sqlite \
  --trim 39.964 46.123 -p device=4 --format json

對已經有 .sqlite profile 的人,正常的分析與測試不需要 CUDA、NVIDIA GPU 或本機安裝 Nsight Systems。這讓 nsys-ai 不只適合在 GPU 主機上跑,也適合把錄好的 profile 帶回筆電做查詢、寫測試或討論診斷。

今天會指出它的缺口,並不是因為工具沒有價值,剛好相反:如果答案只是一段無法追查的生成文字,我們根本無從知道錯在哪。現在能把錯誤定位到參數傳遞、skill 查詢、renderer 或判讀規則,正是 evidence-first 架構值得採用、也值得一起開發的原因。

實驗設計:把窗口與 device 放進同一個參數袋

公開的 agent ask CLI 目前沒有 --trim--device,所以這次直接呼叫 runner:

scope = {
    "trim_start_ns": 39_964_000_000,
    "trim_end_ns": 46_123_000_000,
    "device": 4,
}

answer, evidence, selected = answer_question(
    conn,
    question,
    profile_path="megatron.sqlite",
    trim_kwargs=scope,
)

trim_kwargs 這個名稱容易讓人以為它只能放時間。從原始碼看,runner 其實只是複製這個 mapping,再以 skill.execute(conn, **kwargs) 傳給 triage 與每個 selected skill。因此 device=4 也傳得進去。

最直接的驗證是 kernel_overlap_matrix。只傳時間窗口時,它沿用預設 device 0,而 A 檔的 device 0 沒有 kernel,所以回傳:

source_skill=kernel_overlap_matrix; unavailable: no kernels found

在同一個 trim_kwargs 加入 device=4 後,這個 skill 會正常產生 compute、memcpy 與 NCCL 的 overlap matrix。也就是說,真正的結論不是「device 傳不進 runner」,而是接下來這句:參數傳進 runner,不代表每個 skill 都會採用。

參數從 runner 到答案要經過四關:同一個 mapping 會被傳給所有 skills,但各 skill 是否採用 device 並不一致;最後還要正確揭露 scope,並以相同口徑判讀。參數送達不等於分析條件一致

第一個結果:device 有傳遞,但沒有形成一致的 scope contract

把三題會用到的 skills 逐一對照,可以看到參數「送達」與「生效」是兩件事:

skill 採用時間窗口 採用 device=4 實際範圍
root_cause_matcher device 4、穩定窗
gpu_idle_gaps device 4、穩定窗
memory_bandwidth device 4、穩定窗
kernel_launch_overhead device 4、穩定窗
kernel_overlap_matrix device 4、穩定窗
memory_transfers device 1–7 合計、穩定窗
top_kernels device 1–7 合計、穩定窗

原因很具體:前五個 skills 的參數或執行函式會讀取 device;後兩個只處理時間窗口,即使收到額外的 device,查詢也沒有用它過濾。Python 不會因此報錯,結果看起來仍然正常,卻已經混入不同分析範圍。

答案的 scope 標記也沒有揭露這項差異。device-aware skills 的部分 rows 使用 deviceIdanalysed_device,renderer 卻只辨識 gpu_iddevice_iddevice 等欄位,因此實跑輸出仍可能印成 scope=profile。結尾產生的 Verify 指令也只有 --trim 39.964 46.123,沒有把內部傳入的 device=4 帶回 CLI。這表示它能重跑同一個時間窗,卻不能重現同一張卡。

這裡真正缺的是跨層契約:runner 應明確區分共用 scope 與 skill-specific 參數;skill 應宣告自己採用哪些 scope;renderer 則應輸出實際採用值,而不是只回顯呼叫端要求的值。

第二個結果:窗口修正了,idle 口徑仍然錯

固定 device 4 與穩定窗後,root_cause_matcher 仍把 GPU Bubbles 排在第一筆:

GPU Bubbles (Pipeline Stalls)
  20 gaps > 1.0ms, totaling 14090.4ms (57.2% of profile)

但同一張卡、同一個窗口,gpu_idle_gaps 的 summary 同時包含另一個數字:

total_idle_ms   = 14090.36   # 各 active stream 的 gaps 相加
device_idle_ms  =    91.38   # 所有 kernel 區間取聯集後,整張卡全空的時間
kernel span     =  6155.75

91.38 ÷ 6155.75 ≈ 1.48%,這才是 device-level idle。兩個數字相差約 154 倍,原因不是窗口,也不是不同 GPU,而是統計定義不同。stream 52、60 每一步只在特定階段執行 collective,其餘時間沒有工作是正常分工;把各 stream 的空白相加,不能解讀成整張 GPU 都沒事做。

runner 已經把窗口與 device 正確傳給 root_cause_matcher,但規則仍選用 total_idle_ms 判斷 bubbles。這個實驗把責任邊界切得更清楚:**scope 傳遞成功,並不會自動修正 metric semantics。**規則引擎仍需要明確指定「GPU 發呆」應採 device union,而不是 per-stream sum。

第三個結果:沒有 LLM 時,問句語言會改變 skill routing

這篇是中文文章,使用者自然會用中文提問。但 deterministic selector 的關鍵字表主要是英文;如果問題沒有命中,就退回固定的 top_kernelsgpu_idle_gaps

實際把中文原句送進去,結果如下:

原始問句 命中的關鍵字 selected skills(不含固定 triage)
GPU 有沒有在發呆 top_kernels, gpu_idle_gaps(fallback)
記憶體搬運會不會太多 top_kernels, gpu_idle_gaps(fallback)
哪個 kernel 最花時間 kernel kernel_launch_overhead, top_kernels

因此,若文章只把實際問句翻成中文,卻保留英文問句選出的 skills,讀者會無法重現。接下來的重賽明確使用三個實際字串:GPU idle 高嗎memory transfer 會不會太多哪個 kernel 最花時間。前兩題刻意保留 selector 認得的英文關鍵字。

這不是語言模型的中英文能力問題,因為本次沒有啟用 LLM;它是 deterministic keyword router 的 coverage 問題。有 LLM 時可能選出不同 skills,但那是另一條尚未測試的路徑。

三個問題重跑:選到相關 skill,不代表主診斷回答了問題

三題呼叫都帶入相同的穩定窗與 device=4,結果整理如下;至於各 skill 最後採用的 effective scope,正是表中要檢查的項目:

實際問句 selected skills agent 的 Primary Diagnosis 相關 evidence 與人工判讀
GPU idle 高嗎 gpu_idle_gaps GPU Bubbles,57.2% device 4 的 union idle 是 91.38 ms/1.48%,應排除大面積發呆
memory transfer 會不會太多 memory_bandwidth, memory_transfers GPU Bubbles memory_bandwidth:device 4 H2D 20.97 MB/1.69 ms/12.43 GB/s;另查 memcpy 的 srcKind 確認來源是 pageable。成本僅約佔窗口 0.03%,不是瓶頸;memory_transfers 則是七張卡合計
哪個 kernel 最花時間 kernel_launch_overhead, top_kernels GPU Bubbles top_kernels 彙總七張卡:flash backward 140 次、總計 11,710.59 ms、平均 83.65 ms;Day 21 的 device 4 單卡結果是 20 次、平均 84.05 ms

這三題的問句確實改變了 selected skills,Primary Diagnosis 卻都維持 GPU Bubbles,信心也都是 0.75。原因不是它挑了「最高 severity」,而是 _first_actionable_row()root_cause_matcher 的回傳順序,直接取第一筆可用 finding;它不會依問題相關性重新排序,也不會搜尋 severity 最高者。這次第一筆剛好是 warning,所以固定信心規則給出 0.75。

三題呈現三種不同失敗:idle 題的主題對了,但 metric 用錯;memory 題選到相關 skills,但主診斷答非所問,而且兩個佐證 skill 的 device scope 不同;kernel 題的 top_kernels 找到正確 hotspot,Primary Diagnosis 仍停在 triage 的 bubble。skill routing、evidence correctness 與 answer relevance 是三個獨立檢查,不能用其中一項替另外兩項背書。

另外修正一個容易混淆的數字:55.4 GB/s 的 pinned H2D 是 Day 16 的 B 檔量測,不是主檔 A。A 檔在本次 device 4 穩定窗的 H2D 來源是 pageable,有效頻寬 12.43 GB/s;它能被排除,是因為只有 20.97 MB、累積 1.69 ms,不是因為跑到 pinned baseline。

三個問題的同條件重跑:idle 題選到正確 skill 但使用錯誤口徑;memory 題混用單卡與全卡 scope;kernel 題找到 flash backward hotspot,Primary Diagnosis 仍固定取 triage 第一筆 GPU Bubbles

一個可測試的 agent,需要四層契約

把今天的結果抽象化,一個 profile agent 至少需要四層契約:

  1. 傳遞(pass):runner 把 window、device 與其他 scope 送給每個分析工具。
  2. 採用(consume):每個 skill 明確宣告並實際使用自己支援的 scope;不支援時要說明,而不是靜默忽略。
  3. 揭露(expose):evidence 與 Verify 指令顯示實際使用的 window、device、聚合範圍與統計口徑。
  4. 判讀(judge):Primary Diagnosis 要和問題相關,規則使用的 metric 也要符合問題語意。

目前 runner 已經能完成第一層,第二層只完成一部分,第三、四層則還有今天看到的缺口。這個分法比「內圈交給 agent、外圈交給人」更具體,因為每一層都能寫測試:傳入 device 4 後,每個 selected skill 的 effective scope 是否都是 device 4;輸出的 Verify 是否保留 device;問 memory 時,Primary Diagnosis 是否來自 memory evidence;判斷 GPU idle 時,是否使用 device-union 欄位。

從 nsys-ai 使用者到貢獻者的四層契約:runner 傳遞 scope、skill 採用 scope、答案揭露實際 scope、診斷依正確語意判讀;每一層都能整理成最小 fixture、contract test 與範圍明確的 PR,而且一般測試不需要 GPU 或 Nsight Systems

從使用者到貢獻者:這四層都能一起補

這次實跑找到的不是一句籠統的「agent 還不夠準」,而是四個可以各自驗收的開發入口:

  • 傳遞層:替公開的 agent ask 增加 window 與 device 介面,讓使用者不必呼叫內部 runner。
  • 採用層:讓 top_kernelsmemory_transfers 支援 device,或在不支援時回報 effective scope,避免靜默忽略。
  • 揭露層:統一 deviceIddevice_idanalysed_device 等欄位,並讓 Evidence 與 Verify 指令保留同一組 scope。
  • 判讀層:補上中文 routing 測試、question relevance 排序,以及用 device-union idle 判斷 GPU bubbles 的規則。

每一項都可以先從一個 fixture、一個 failing test 和一個小改動開始,不需要先重寫整個 agent。nsys-ai 的正常單元與整合測試使用 repository 內的 SQLite fixtures;依專案的貢獻指南,這些工作不要求 CUDA、NVIDIA GPU 或 Nsight Systems。也就是說,即使手邊沒有 GPU,仍然可以改 skill contract、renderer、CLI、中文關鍵字或測試。

git clone https://github.com/GindaChen/nsys-ai.git
cd nsys-ai
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[dev]'

# 從涵蓋這次問題的 contract tests 開始
python -m pytest tests/test_agent_runner.py tests/test_skills.py -q

如果你只是想用工具,從 agent ask 找方向,再用 skill run 固定 scope 覆核就夠了;如果你也想讓它更準,則可以把這篇的任一重現案例整理成 issue,先寫清楚輸入、實際輸出與期待行為,再送一個範圍明確的 PR。這是開放式分析工具很有意思的地方:使用者在真 profile 遇到的問題,可以去除敏感資料、縮成最小 fixture,再長成下一個測試案例。

三種常見的誤判

第一,函式接受參數,就當成所有分析都使用了參數。device=4 的確到達每個 skill,但 top_kernelsmemory_transfers 會靜默忽略。驗收不能只看函式簽名,要看每個結果的 effective scope。

第二,中文與英文問的是同一件事,就假設路由結果也相同。 在 deterministic 關鍵字路徑中,記憶體搬運 不會命中 memorytransfer。文章、測試與產品介面都應保留原始問句,不能只保留翻譯後的顯示文字。

第三,把 0.75 當成答案有七成五正確。 這裡的 0.75 只表示 root matcher 的第一筆 actionable finding 是 warning,而且存在 usable evidence;它沒有衡量問題相關性、scope 一致性或 metric 是否選對。

小結與明天

今天把一場「公平重賽」拆成四個可以驗收的動作:傳遞、採用、揭露、判讀。實跑證明 trim_kwargs 不只可以傳窗口,也能傳 device=4;但 memory_transferstop_kernels 不採用 device,renderer 沒有揭露實際差異,Verify 指令也遺失 device。即使 scope 正確傳到 root_cause_matcher,它仍以 per-stream gaps 得出 57.2% bubbles,而同窗同卡的 device-union idle 只有 1.48%。

再加上中文問句的 keyword fallback,以及 Primary Diagnosis 固定取 triage 第一筆 finding,今天的結論很明確:**參數送達,不等於量測條件一致;有相關 evidence,也不等於主診斷正在回答問題。**但 nsys-ai 已經把原本散落在 GUI、SQL 與人工筆記裡的分析步驟,收斂成可重跑、可測試的介面。現在值得用它縮小搜尋範圍,也值得把 effective scope、metric semantics 與 question relevance 一起補成更完整的契約。

明天 Day 26 收 Part E:故意製造一個模型很容易說得順、但證據不支持的答案,再用 skill 輸出與 diff 驗收每項主張。重點不只是「抓到幻覺」,而是把今天四層契約中的最後一層——判讀——變成可以測試的 rubric。

參考資料


上一篇
Day 24|有證據,不代表診斷正確:nsys-ai evidence-first 的實作與一次誤診
下一篇
Day 26|看不到 Profile 時,AI 會怎麼答?一次 profile-blind 診斷與證據判讀
系列文
GPU很忙?他真的有在做事嗎?26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言