做 EDA flow 最容易翻車的地方,通常不是「 job 寫錯指令」,而是把不同時間點該做的事混在一起。
什麼時間點該做什麼事,自己要清楚。
這句話聽起來像人生雞湯,但在 flow runner 裡,它幾乎就是全部。Altair FlowTracer 把這件事做到極致——用 tracing 抓住「這個工具什麼時候該跑、依賴什麼檔、能不能並行」。WinFlow 走的是比較輕量、宣告式的路:把同一套時間哲學寫進 flow.json 與 Runner。今天就把這條時間軸攤開來看。
半導體實作流程很長。一個 place-and-route 風格的 demo,光是:
FloorPlan ───► Place ───► CTS ───► Route
│ │ │ │
───► QC ───► QC ───► QC ───► QC
就已經有兩種完全不同的「先後」:
Q_PLACE 檢查 PLACE 的產出,但不該擋住 CTS 開工。如果系統搞不清楚這兩件事發生在哪個時間點、由誰決定,後果很具體:
inputs / outputs 當成排程 → 檔案還沒寫完,下游就會以為自己 ready之前有提到坊間一個有名的 FlowTracer(Altair),它的核心是三件事:
WinFlow 沒有做 FlowTracer 那種「執行時去 trace 工具開了哪些檔」的法術。我把依賴寫在 flow.json 裡:parents / children 管排程,inputs / outputs 管檔案是否真的在磁碟上。想法相同,實作更直白—— 因為目標是讓寫 flow 的人、跑 flow 的人、畫 DAG 的 GUI,對同一套規則達成共識。

對應到 repo 裡的模組,大致是:
核心架構原則: 每一欄都有專屬負責人(Who)與核心任務(Do),並嚴格禁止越權行為(Don't)。
| 時間點 | 誰負責 | 做什麼 | 刻意不做什麼(禁止越權) |
|---|---|---|---|
| 設計時 | winflow/generator/ |
組出 job、command、I/O,必要時種子化 DAG | 不 bsub、不輪詢 |
| 編輯時 | Generator GUI + editor/deps.py |
改 parents / children |
不重新 annotate 覆蓋使用者的 unlink |
| 載入時 | FlowValidator |
檢查 JSON 結構、重複 stage、relation 一致性 | 不看磁碟上的檔案 |
| 排程時 | FlowRunner._run_dag |
父節點都完成才放行;ready 的 job 並行 | 不管 stage 順序、不管檔名 |
| 提交時 | run_job |
先確認 inputs 存在,再送 LSF |
還沒跑完就去驗 outputs |
| 輪詢時 | LSFJobManager.wait_job |
bjobs 看到 DONE 或 EXIT |
用 stage 名稱猜狀態 |
| 完成時 | wait_job + validator |
DONE 之後才驗 outputs |
把「檔案在不在」當成「能不能開始」 |
| 重跑時 | job_filter / GUI Rerun |
已 DONE 的跳過,從失敗點繼續 | 把成功的 job 再送一次 |
| 同步時 | Runner「Sync from Generator」 | 只在 runner 閒置時把編輯結果灌回去 | 有 job 在跑還去改 DAG |
參考 winflow/graph.py 開頭:
Jobs schedule solely from each job's
parents/children.
Stage/task nesting remains for identity, logging, and editor tags only.
Inputs/outputs do NOT schedule jobs.
這三句話可以當成 WinFlow 的憲法。拆開來看:
FLOOR_PLAN/PLACE/PLACE 這種 key 裡的 stage、task,是給人看的標籤:日誌、畫布分組、GUI 點選。Runner 不會因為 PLACE 寫在 FLOOR_PLAN 後面,就自動等它。
所以你可以把質檢 job 放在獨立的 stage,也可以跟主 job 放在同一個 stage——對排程沒差。差的是 parents 指到誰。
parents / children:什麼時候「可以開始」這才是時間軸上的開關。_run_dag 的邏輯,非常樸素:
remaining[key] = 還沒完成的 parent 數量
ready = remaining == 0 且尚未送出
某個 job 一 DONE,就去把它的 children 的 remaining 減一。減到零,立刻丟進 thread pool。兩個都沒有互相依賴的 root(例如 ithome 裡的 IT_1 與 IT_2_*)會一起跑——並行是 DAG 長出來的,不是額外寫的 parallel: true。
inputs / outputs:這個時間點「檔案在不在」檔案檢查發生在兩個非常特定的瞬間:
validate_paths(job_inputs, "input")
沒有 input,根本不該 bsub。這是保險,不是排程。
validate_paths(job_outputs, "output")
cluster 說做完了,但約定的 .done 沒出現,這份工作仍算失敗。
很多人會下意識覺得「output 就是下一棒的 input,所以檔案依賴 = 執行順序」。WinFlow 刻意拆開:檔案告訴你產物有沒有寫出來;DAG 告訴你什麼時候允許下一棒入場。兩者常常對齊(Generator 也會用 outputs → inputs 去種子化邊),但對齊是結果,不是定義。
質檢旁掛就是最好的例子:Q_PLACE 的 input 是 PLACE 的 output,所以它晚於 PLACE;但它不是 CTS 的 parent,所以 CTS 不必等質檢。時間關係被寫在邊上,而不是寫在檔名裡。
把鏡頭從整條 flow 拉近到單個 job,run_job 的順序幾乎是照抄整套做法的:
SKIP?(Rerun 時已完成)
→ 通知 GUI:pending
→ 驗 inputs
→ bsub
→ 通知 GUI:PEND
→ 輪詢 bjobs
→ DONE:驗 outputs → 通知 job_done
EXIT / 缺檔:通知 job_failed,丟例外
注意幾個「不要提早做」:
DONE,不會去驗 output(避免把「正在寫檔」當成失敗)job_failed
這樣 GUI 不會停在一個永遠的 RUN
GUI 也守同一個時鐘。Runner 還在跑時,Sync from Generator 是關掉的——你不會一邊執行舊 DAG、一邊把新 DAG 灌進去。Stop 走 bkill 與重試;Reset Flow 清的是本地狀態與 log,不會偷偷幫你殺 cluster 上的作業。每個按鈕都對應一個時間點,而不是「反正都能按」。
內建的 ithome flow 長這樣:
IT_2_0 .. IT_2_N ────┐
v
IT_1 ──> IT_3 ──> IT_4 ──> IT_5
│ ^
└────────────────┘
架構核心概念: 系統排程嚴格依賴 DAG 的
parents/children關係,檔案的存在是提交前(Inputs Verification)的保險,而非放行排程的依據。
| 問題 | 答案與執行邏輯 |
|---|---|
IT_1 跟 IT_2_* 誰先跑? |
兩者都是 root(parents 為空),同時可以 ready |
IT_4 什麼時候能送? |
等 IT_3 以及所有 IT_2_* DONE |
IT_5 能不能關掉? |
設計時: yaml 的 USE_IT_5執行時: DAG 裡根本沒這個 node |
IT_3 的 output 同時餵給 IT_4 與 IT_5,會不會變成排程邊? |
檔案是保險;真正放行的仍是 parents |
如果你發現自己在用「IT_4 寫在 JSON 比較後面」來解釋執行順序,就代表時間點又混了。JSON 的排列是給人讀的;Runner 只認邊。
config.json)→ 這次設計(setting.sh)→ 這次固化的流程(flow.json)parents / children
inputs 在提交前驗,outputs 在 DONE 後驗FlowTracer 用 tracing 在執行中抓住事實;WinFlow 用宣告在執行前把事實寫下來。
這是兩種實作,但是是同一種紀律 —— 流程工具如果自己都搞不清楚時鐘,使用者更不可能搞得清楚。