昨天載範本時,Reference 那一欄我們直接略過。今天把它補齊

看起來只是多填 queue、cpu。現場不是這樣。設定檔真正好用的地方,不是把 LSF 數字從 GUI 打進去,而是讓同一份範本,依設定長出不一樣的圖。
範本負責「這家公司大概怎麼蹲」;設定檔負責「這一顆 design 今天要蹲幾下、要不要最後那一棒」。
例如驗證要不要開、開幾條:
PV --> 驗 A
--> 驗 B
--> 驗 C
或 hierarchy design,頂層要看到幾個下層 APR block:
TOP --> subblock A
--> subblock B
--> subblock C
開關寫在設定裡,builder 讀完再決定生幾個 job。Flow 變靈活,不必為每一顆 chip 複製一份 Python。
以昨天的 ithome 為例,收工時它要能:
winflow/generator/parsers 加一個 parser 讀 YAMLIT_5、IT_2 扇出成幾個目標圖長這樣:
IT_2_0 ( IT_2_LIST = IT_2_0 ... IT_2_N )
.
.
IT_2_(N-1)
IT_2_N ____________
|
v
IT_1 —> IT_3 —> IT4 -> IT_5 (僅當 USE_IT_5 = true)
| ^
|________________|
跟昨天比,差在 IT_2 從「一個人」變成「一排人」。人數不是寫死在 builder,是 YAML 裡的 IT_2_LIST 說了算。
參考設定:
FLOW_NAME:"ithome"
CPU:
- IT_1:4
- IT_2:4
- IT_3:4
- IT_4:8
- IT_5:16
IT_2_LIST:[subblock_A,subblock_B,subblock_C]
USE_IT_5:false
這部分可以裝套件讓 parse 變簡單。本專案的自我限制是盡量不依賴第三方套件,所以選擇自己吃這份方式。程式碼不在這裡展開。
現在這類問題交給 AI 寫也非常方便 🤣
重點只有兩個:
winflow/generator/parsers/,讓 builder 用 from winflow.generator.parsers import parse_yml
CPU 最好整理成 dict、IT_2_LIST 整理成 list、USE_IT_5 整理成 boolParser 是邊界。Builder 不會一邊組 DAG、一邊跟設定搏鬥。
## 上面記得多加 from winflow.generator.parsers import parse_yml
def build_ithome_stages(
queue: Optional[str] = None,
cpu: Optional[str] = None,
machine: str = "",
yml: Optional[Dict[str, Any]] = None,
yml_path: Optional[Union[str, Path]] = None,
) -> List[Stage]:
"""把 yml 設定讀進來"""
cfg = _cfg()
queue = queue if queue is not None else cfg.default_queue
default_cpu = str(cpu if cpu is not None else cfg.default_cpu)
machine = machine or ""
data = _load_yml(yml=yml, yml_path=yml_path)
cpu_map = data.get("CPU") if isinstance(data.get("CPU"), dict) else {}
it2_list = data.get("IT_2_LIST") or ["default"]
if not isinstance(it2_list, list) or not it2_list:
it2_list = ["default"]
use_it_5 = bool(data.get("USE_IT_5", True))
# map cpu 用量
def cpu_for(job_name: str) -> str:
key = "IT_2" if job_name.startswith("IT_2") else job_name
print(key)
print(str(cpu_map.get(key, default_cpu)))
return str(cpu_map.get(key, default_cpu))
out_it_1 = job_output_path("IT_1")
out_it_3 = job_output_path("IT_3")
out_it_4 = job_output_path("IT_4")
out_it_5 = job_output_path("IT_5")
# 一定會有第一個 job
jobs: List[Job] = [
make_job(
"IT_1",
job_command("IT_1"),
[],
[out_it_1],
queue,
cpu_for("IT_1"),
machine,
),
]
#看有多少個 it2 job,空的話就走 default
it2_outputs: List[str] = []
for index, block in enumerate(it2_list):
name = f"IT_2_{index}"
out_path = job_output_path(name)
it2_outputs.append(out_path)
jobs.append(
make_job(
name,
f"{job_command('IT_2')} {block}",
[],
[out_path],
queue,
cpu_for(name),
machine,
)
)
jobs.append(
make_job(
"IT_3",
job_command("IT_3"),
[out_it_1],
[out_it_3],
queue,
cpu_for("IT_3"),
machine,
)
)
jobs.append(
make_job(
"IT_4",
job_command("IT_4"),
[out_it_3,*it2_outputs],
[out_it_4],
queue,
cpu_for("IT_4"),
machine,
)
)
# 如果有 5 的話
if use_it_5:
jobs.append(
make_job(
"IT_5",
job_command("IT_5"),
[out_it_3, out_it_4, *it2_outputs],
[out_it_5],
queue,
cpu_for("IT_5"),
machine,
)
)
return _wrap_jobs_as_stages(jobs)
這段值得對著圖看三件事:
IT_2_* 仍是 Root。 它們的 inputs 是空的,可以跟 IT_1 一起跑。IT_2_LIST 有三個 block,就生三個 job,command 把 block 名稱當參數傳進去。IT_4 的 inputs 把所有 IT_2_*.done 都吃進去。 扇出多少,匯流就等多少。不必為 N=3 寫一套、N=10 再寫一套。USE_IT_5: false 時,圖上根本沒有 IT_5。 不是生出來再 skip,是 builder 在設計時就把人請出場。Runner 因此不必認識這個開關。cpu_for() 也有一個現場細節:YAML 只寫了 IT_2: 4,實際 job 卻叫 IT_2_0、IT_2_1。對應時用前綴,才不必為每個 subblock 重複填 CPU。
_load_yml 的預設路徑可以指到 example_flow/ithome.yml。GUI 沒選檔時,builder 仍能長出一張「預設口令」的圖——這就是 Reference 標成 optional 的原因。
GUI 不是本篇重點,改動集中在兩個地方。
generator/editor/document.py 讓範本選項多帶一條 yml 路徑:
@dataclass
class TemplateOptions:
"""User-provided options when loading a flow template."""
queue: str = DEFAULT_QUEUE
machine: str = ""
cpu: int = DEFAULT_CPU
setting_path: Optional[Path] = None
# for ithome.yml
yml_path: Optional[Path] = None
ui/generator.py 只在 template 叫做 ithome 時,多畫一個檔案列:
class TemplateLoadDialog(tk.Toplevel):
"""Collect template options (queue, machine, cpu, optional setting.sh)."""
def __init__(self, parent, template_name: str):
# ADD
self.yml_path = tk.StringVar()
.
.
.
# ADD
if self.template_name == "ithome":
opts_frame = ttk.LabelFrame(
outer,
text=" Reference files (optional) ",
style="Card.TLabelframe",
padding=10,
)
opts_frame.pack(fill=tk.X, pady=(0, 4))
ttk.Label(
opts_frame,
text="Read setting from yml.",
style="Muted.TLabel",
wraplength=400,
).pack(anchor="w", pady=(0, 8))
self._file_row(
opts_frame,
"ithome.yml",
self.yml_path,
[("YAML", "*.yml"), ("All", "*.*")],
)
.
.
.
載入範本時會多一個視窗。兩個欄位都可以留空:系統會去預設位置找設定。也可以不關 WinFlow,改 YAML 後再 Load 一次,圖就按新口令重長——這比改 Python、重開程式更接近 APR 日常。

把這三天串起來,職責會比較清楚:
| 檔案 | 變的頻率 | 管什麼 |
|---|---|---|
flows/ithome/builder.py |
很少 | 這種 flow 的骨架:誰是 Root、誰匯流 |
ithome.yml / setting.sh |
每顆 design、每次實驗 | 扇出幾個、要不要末站、CPU / queue |
flow.json |
每次 Load / Export | Runner 真正執行的那份凍結口令 |
Builder 不該把 USE_IT_5 寫死;YAML 不該去呼叫 bsub;flow.json 不該在跑到一半還被設定檔改寫。各管各的時間點,後面講 Runner 時還會再碰到同一句話。