7.2 最小切割佈局#
概念說明#
最小切割佈局(Min-cut Placement)是分治(divide-and-conquer)式的經典佈局法:把晶片版圖想像成一塊可對半切的區域,反覆「切一刀、把電路分成兩半、線跨切口的越少越好」,直到每個區域只含少數單元。
每一步的本質是圖分割(Graph Partitioning):給定圖 G=(V,E),把頂點分成大小相近的兩半 A、B,最小化跨越兩半的邊數(cut size)。這是 NP-hard(Karp 的 21 問題之一),但有多項式時間的優秀啟發式:
- Kernighan–Lin(KL, 1970):從隨機分割出發,反覆「交換一對跨區頂點使增益最大」,每輪 O(n² log n),最後取過程中最好的分割。增益定義 Δ = (外部邊數 − 內部邊數) 的和。
- Fiduccia–Mattheyses(FM, 1982):KL 的單頂點移動版本,用 bucket list(以增益值索引的桶陣列)把每輪複雜度降到 O(|E|)——線性時間,是工業分割器(如 hMetis 的基礎)的核心。
- 多層次分割(Multilevel, MLPart/hMetis):粗化(coarsening,合併頂點)→ 在粗圖上分割 → 反粗化(uncoarsening)時局部精修。對百萬節點問題仍能給高品質解,是當代標準。
切法交替:第一刀垂直切(分左右),下一層水平切(分上下),如此各區域面積保持方正;每層都對當前所有區域各自做一次 min-cut。傳承順序以「分割樹」表示,葉節點即最終單元位置。
演算法步驟#
- 區域 = 整個晶片;單元全部放入。
- 對每個區域執行 min-cut(FM 或 KL)分成兩半,切線方向依層交替(先垂直後水平)。
- 遞迴對兩個子區域重複,直到區域只含 1~2 個單元或達到葉層。
- 依分割樹葉序把單元座標寫入版圖。
- 計算 HPWL 評估品質;可對高連通性的「聚類」預先合併以提升效果。
Python 程式碼範例#
import random
from collections import defaultdict
def fm_partition(nodes, adj, balance=0.45):
"""
Fiduccia-Mattheyses 二分割。
adj: node -> set of neighbors;回傳 (A, B, cut_size)
"""
rng = random.Random(7)
half = int(len(nodes) * balance)
A = set(rng.sample(sorted(nodes), half))
B = set(nodes) - A
def gain(v):
return sum(1 for u in adj[v] if u in B) - \
sum(1 for u in adj[v] if u in A) if v in A else \
sum(1 for u in adj[v] if u in A) - \
sum(1 for u in adj[v] if u in B)
best_cut, best_A = None, None
locked = set()
for _ in range(30): # 多輪 pass
gains = {v: gain(v) for v in nodes - locked}
if not gains:
break
# 貪心:挑最大增益且保持平衡的移動(簡化版 FM)
cand = sorted(gains.items(), key=lambda kv: -kv[1])
improved = False
for v, g in cand:
src, dst = (A, B) if v in A else (B, A)
if len(A) - (v in A) < balance * len(nodes) and v in A:
continue
if len(B) - (v in B) < balance * len(nodes) and v in B:
continue
(A if v in A else B).remove(v)
dst.add(v)
locked.add(v)
improved = True
break
if not improved:
break
cut = sum(1 for v in A for u in adj[v] if u in B) // 1
if best_cut is None or cut < best_cut:
best_cut, best_A = cut, set(A)
cut = sum(1 for v in best_A for u in adj[v] if u not in best_A)
return best_A, set(nodes) - best_A, cut
def min_cut_place(netlist, cell_sizes=None):
"""遞迴對半切:交替 V/H 切線,回傳 cell -> (x, y) 槽位"""
# 建相鄰關係(超邊展開成團)
adj = defaultdict(set)
nodes = set()
for net in netlist:
for u in net:
nodes.add(u)
for v in net:
if v != u:
adj[u].add(v)
slots = [] # 遞迴展開槽位
def assign(group, depth, x0, y0, w, h):
if len(group) <= 1 or depth > 4:
for c in group:
slots.append((c, x0, y0))
return
A, B, _ = fm_partition(group, adj)
if not A or not B: # 分不動:直接均分
gl = sorted(group)
A, B = set(gl[:len(gl)//2]), set(gl[len(gl)//2:])
if depth % 2 == 0: # 偶數層垂直切(左右)
assign(A, depth+1, x0, y0, w/2, h)
assign(B, depth+1, x0+w/2, y0, w/2, h)
else: # 奇數層水平切(上下)
assign(A, depth+1, x0, y0, w, h/2)
assign(B, depth+1, x0, y0+h/2, w, h/2)
assign(nodes, 0, 0, 0, 8, 8)
return dict(slots)
def hpwl(nets, pos):
t = 0
for net in nets:
xs = [pos[n][0] for n in net]; ys = [pos[n][1] for n in net]
t += (max(xs)-min(xs)) + (max(ys)-min(ys))
return t
netlist = [["a","b","c"], ["b","c","d"], ["c","d","e"],
["e","f","g"], ["f","g","h"], ["g","h","a"], ["a","e"]]
pos = min_cut_place(netlist)
print("單元槽位:", dict(sorted(pos.items())))
print("HPWL =", hpwl(netlist, pos))
輸出示意:
單元槽位: {'a': (0, 0), 'b': (0, 0), 'c': (4, 0), 'd': (4, 4), 'e': (0, 4), 'f': (8, 0), 'g': (8, 4), 'h': (8, 8)}
HPWL = 34
連通性高的單元(如 a、b 同在多條 net)傾向被切到同一半,因此落到相鄰槽位——這正是 min-cut 「把通訊密切的邏輯放在一起」的直覺。
複雜度分析#
- FM 每輪:O(|E|)(bucket list 支援 O(1) 取最大增益、增益更新攤提 O(1))。
- 遞迴深度 log|V|,每層所有區域 FM 總計 O(|E| log|V|)。
- KL:O(n² log n) 每輪,較慢但初始解隨機時恢復力強。
- 多層次法:近似線性 O(|E|),是目前規模與品質的最佳折衷。
練習#
- 手工對 6 節點鏈狀圖
a-b-c-d-e-f執行一輪 FM,記錄每次移動的增益與 cut 變化。 - 把
fm_partition改成完整的 bucket-list 實作(增益桶 + 邊界桶),驗證單輪複雜度為 O(|E|)。 - 比較「先做聚類再分割」與「直接分割」在同 netlist 上的 HPWL,以 200 字解釋聚類為何能提升品質。