iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
佛心分享-SideProject30

打造 APR Engineer 的生產力平台,從 Flow Tracer 到 SignOff DashBoard 的落地實戰系列 第 8

【Day 08 】 Flow Generator 靈魂擴充:透過範本與設定檔實現自動化生成

  • 分享至 

  • xImage
  •  

前言

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

https://ithelp.ithome.com.tw/upload/images/20260812/20127932hIZ8n95l7v.png

看起來只是多填 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 為例,收工時它要能:

  1. winflow/generator/parsers 加一個 parser 讀 YAML
  2. 用這份 YAML 動態調整 ithome:要不要 IT_5IT_2 扇出成幾個
  3. GUI 多一個欄位選設定檔;不選就走預設路徑

目標圖長這樣:

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

Step 1. myParser.py

這部分可以裝套件讓 parse 變簡單。本專案的自我限制是盡量不依賴第三方套件,所以選擇自己吃這份方式。程式碼不在這裡展開。

現在這類問題交給 AI 寫也非常方便 🤣

重點只有兩個:

  • 放進 winflow/generator/parsers/,讓 builder 用 from winflow.generator.parsers import parse_yml
  • 讀出來的結構要穩定:CPU 最好整理成 dict、IT_2_LIST 整理成 list、USE_IT_5 整理成 bool

Parser 是邊界。Builder 不會一邊組 DAG、一邊跟設定搏鬥。

Step 2. 讓 ithome builder 讀設定再長圖

## 上面記得多加 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)

這段值得對著圖看三件事:

  1. IT_2_* 仍是 Root。 它們的 inputs 是空的,可以跟 IT_1 一起跑。IT_2_LIST 有三個 block,就生三個 job,command 把 block 名稱當參數傳進去。
  2. IT_4 的 inputs 把所有 IT_2_*.done 都吃進去。 扇出多少,匯流就等多少。不必為 N=3 寫一套、N=10 再寫一套。
  3. USE_IT_5: false 時,圖上根本沒有 IT_5 不是生出來再 skip,是 builder 在設計時就把人請出場。Runner 因此不必認識這個開關。

cpu_for() 也有一個現場細節:YAML 只寫了 IT_2: 4,實際 job 卻叫 IT_2_0IT_2_1。對應時用前綴,才不必為每個 subblock 重複填 CPU。

_load_yml 的預設路徑可以指到 example_flow/ithome.yml。GUI 沒選檔時,builder 仍能長出一張「預設口令」的圖——這就是 Reference 標成 optional 的原因。

Step 3. 讓 GUI 出現可以勾選的選項

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 日常。

https://ithelp.ithome.com.tw/upload/images/20260812/20127932qF2rYGgnlh.png

設定檔、範本、JSON,各自管一段

把這三天串起來,職責會比較清楚:

檔案 變的頻率 管什麼
flows/ithome/builder.py 很少 這種 flow 的骨架:誰是 Root、誰匯流
ithome.yml / setting.sh 每顆 design、每次實驗 扇出幾個、要不要末站、CPU / queue
flow.json 每次 Load / Export Runner 真正執行的那份凍結口令

Builder 不該把 USE_IT_5 寫死;YAML 不該去呼叫 bsubflow.json 不該在跑到一半還被設定檔改寫。各管各的時間點,後面講 Runner 時還會再碰到同一句話。

小結

  • Reference 不只是填 LSF 數字,還能決定要不要某站、扇出幾條
  • Parser 負責消化各家公司的設定;builder 只消化結構化的資料
  • GUI 不選檔就走預設;改檔再 Load,不必重開程式

上一篇
【Day 07 】 Flow Generator 架構設計:如何定義一個標準化的流程?
下一篇
【Day 09 】 Flow Tracer 架構哲學:什麼時間點該做什麼事,自己要清楚
系列文
打造 APR Engineer 的生產力平台,從 Flow Tracer 到 SignOff DashBoard 的落地實戰13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言