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 的單卡臨檢表。
面對一份陌生的 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 架構值得採用、也值得一起開發的原因。
公開的 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 都會採用。

把三題會用到的 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 使用 deviceId 或 analysed_device,renderer 卻只辨識 gpu_id、device_id、device 等欄位,因此實跑輸出仍可能印成 scope=profile。結尾產生的 Verify 指令也只有 --trim 39.964 46.123,沒有把內部傳入的 device=4 帶回 CLI。這表示它能重跑同一個時間窗,卻不能重現同一張卡。
這裡真正缺的是跨層契約:runner 應明確區分共用 scope 與 skill-specific 參數;skill 應宣告自己採用哪些 scope;renderer 則應輸出實際採用值,而不是只回顯呼叫端要求的值。
固定 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。
這篇是中文文章,使用者自然會用中文提問。但 deterministic selector 的關鍵字表主要是英文;如果問題沒有命中,就退回固定的 top_kernels 與 gpu_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,但那是另一條尚未測試的路徑。
三題呼叫都帶入相同的穩定窗與 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。

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

這次實跑找到的不是一句籠統的「agent 還不夠準」,而是四個可以各自驗收的開發入口:
agent ask 增加 window 與 device 介面,讓使用者不必呼叫內部 runner。top_kernels、memory_transfers 支援 device,或在不支援時回報 effective scope,避免靜默忽略。deviceId、device_id、analysed_device 等欄位,並讓 Evidence 與 Verify 指令保留同一組 scope。每一項都可以先從一個 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_kernels 與 memory_transfers 會靜默忽略。驗收不能只看函式簽名,要看每個結果的 effective scope。
第二,中文與英文問的是同一件事,就假設路由結果也相同。 在 deterministic 關鍵字路徑中,記憶體搬運 不會命中 memory 或 transfer。文章、測試與產品介面都應保留原始問句,不能只保留翻譯後的顯示文字。
第三,把 0.75 當成答案有七成五正確。 這裡的 0.75 只表示 root matcher 的第一筆 actionable finding 是 warning,而且存在 usable evidence;它沒有衡量問題相關性、scope 一致性或 metric 是否選對。
今天把一場「公平重賽」拆成四個可以驗收的動作:傳遞、採用、揭露、判讀。實跑證明 trim_kwargs 不只可以傳窗口,也能傳 device=4;但 memory_transfers 與 top_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。
agent/runner.py(commit 1cc03b5)
gpu_idle_gaps.py
kernel_overlap_matrix.py
nsys-ai v0.3.0-21-g1cc03b5,無 LLM key。runner.answer_question 使用 trim_kwargs={"trim_start_ns": 39964000000, "trim_end_ns": 46123000000, "device": 4};device 4 的 total_idle_ms=14090.36、device_idle_ms=91.38、kernel span 6155.75 ms。三個混合中英文問句與兩個純中文反例均以同一份 megatron.sqlite 重跑。