5.3 模擬退火佈局#
原始碼:
veryl-eda/src/asic/place.rs(342 行)。輸入:映射後網路 + 目標 利用率;輸出:RowPlacement(每 cell 的 (row, site)、IO pin 位置、退火 history)。
5.3.1 問題與座標系#
標準單元佈局(standard-cell placement)要求:所有 cell 落在等高的列 (row)上,彼此不重疊,並最小化某種估計成本。vlib 的幾何(lib.rs):
pub const ROW_H: f64 = 4.0; // 列高 µm
pub const SITE_W: f64 = 0.5; // site 寬 µm
每列是一排 site;cell 寬度以 site 計(NAND2 3 site、DFF 14 site,見 lib::LIB)。巨集(SRAM,多列高)位置固定(fixed),先以 shelf packing 從左下角排好,die 不夠大就長大:
// veryl-eda/src/asic/place.rs(節錄)
// macros: shelf packing from the bottom left (grow the die until they fit)
loop {
mpos.clear();
let (mut x, mut y, mut shelf) = (0u32, 0u32, 0u32);
for c in (0..n).filter(|c| fixed[*c]) {
if x + widths[c] > sites { x = 0; y += shelf; shelf = 0; }
if y + heights[c] > rows || x + widths[c] > sites { ok = false; break; }
mpos.push((c, y, x)); x += widths[c]; shelf = shelf.max(heights[c]);
}
if ok { break; }
rows += 1; sites += sites / 20 + 1;
}
die 大小由利用率反推:
IO pin 均勻分布在 die 邊(輸入左/上、輸出右/下,around()),並吸附到 繞線網格(site 欄 × 軌道列)上,被佔的節點沿邊讓位。
5.3.2 模擬退火理論#
佈局是組合最佳化問題(把 \(n\) 顆 cell 放進 \(\text{rows} \times \text{sites}\) 的格子、不重疊),搜尋空間天文數字且處處局部極小。模擬退火(simulated annealing, Kirkpatrick et al. 1983)用統計力學的退火類比跳脫局部極小:
- 每步在鄰域中隨機產生新解,成本變化 \(\Delta\) ;
- Metropolis 準則: \(\Delta \le 0\) 必收; \(\Delta > 0\) 以機率
接受( \(T\) 是「溫度」); - \(T\) 逐輪下降,高溫時幾乎什麼都收(自由探索),低溫時只接受變好 (收斂成局部下降)。
本專案的實作,接受判準一行:
// veryl-eda/src/asic/place.rs
if delta <= 0.0 || rng.unit() < (-delta / temp).exp() { ... accept ... }
溫度排程#
初始溫度不是拍腦袋,而是從 200 顆隨機 cell 的 net 成本統計出標準差:
let mut temp = {
let mut d = vec![];
for _ in 0..200 {
let c = rng.below(n); if fixed[c] { continue; }
d.push(cell_nets[c].iter().map(|k| ncost[*k]).sum::<f64>());
}
let m = d.iter().sum::<f64>() / d.len() as f64;
(d.iter().map(|x| (x - m) * (x - m)).sum::<f64>() / d.len() as f64).sqrt().max(1.0)
};
這是 Kirkpatrick 原文的自適應初始溫度想法: \(T_0\) 應與「單步成本擾動的 尺度」同量級,使初期接受率落在舒適區。之後每輪依實測接受率調溫:
temp *= if rate > 0.96 { 0.5 } else if rate > 0.8 { 0.9 }
else if rate > 0.15 { 0.95 } else { 0.8 };
解讀:接受率太高(>96%)→ 降溫太快一半(還在亂晃,省時間);80–96% → 穩定 下降;15–80% → 溫和降溫(黃金工作區);<15% → 凍住了,反而回升 20% 給它 一次機會。這比幾何排程( \(T \leftarrow \alpha T\) )更能對不同大小的設計自洽。
終止條件也是尺度化的:
if temp < 0.005 * cost.max(1.0) / pnets.len() as f64 { break; }
即:單步溫度小於「每 net 平均成本的 0.5%」時,擾動已無意義,停。
鄰域:move 與 swap,且永遠合法#
let a = rng.below(n);
let rr = rlim.max(1.0) as i64;
let r2 = (ra as i64 + rng.below((2*rr+1) as usize) as i64 - rr).clamp(0, rows as i64 - 1) as u32;
let xr = (rr * 8).max(4);
let x2 = (xa as i64 + rng.below((2*xr+1) as usize) as i64 - xr).clamp(0, (sites - wa) as i64) as u32;
// target: free space, or a single cell we can swap with
let b = occ[r2 as usize][x2 as usize];
if free_run(&occ, r2, x2, wa, &[a as i32]) { plan.push((a, r2, x2)); }
else if b >= 0 && ... && free_run(&occ, rb, xb, wa, &ig) && free_run(&occ, ra, xa, wb, &ig) && !clash {
plan.push((a, rb, xb)); plan.push((b, ra, xa)); // swap
}
只有兩種鄰域動作:move(cell 挪進一段 free_run 空位)與 swap (兩顆 cell 互換,且互換後雙方都得放得下)。每次移動一步就合法 (不重疊、不越界)——這是讓退火可以直接做的關鍵設計;若允許非法中間態, 就要每步做「合法化修復」,成本高且成本函式不連續。
rlim(range limit)控制 move 的距離:高溫時大範圍搬動,低溫時只做 小抖動,且同樣依接受率自適應:
rlim = (rlim * (1.0 - 0.44 + rate)).clamp(1.0, rows.max(sites / 8) as f64);
若從既有佈局精修(start: Some(...),timing-driven pass 用),則 temp *= 0.02; rlim = 2.0——低溫短步,只做局部整理。
flowchart TD
A[生成 move/swap] --> B{目標有空位或可 swap?}
B -- 否 --> A
B -- 是 --> C[套用,增量算 Δ 線長+密度]
C --> D{Δ ≤ 0 或 rand < e^{-Δ/T}?}
D -- 接受 --> E[更新 ncost/cost]
D -- 拒絕 --> F[還原 occ/pos/bins]
E --> A
F --> A
A --> G[每輪結束:依接受率調 temp 與 rlim]
G --> H{T 極低?}
H -- 是 --> I[結束]
H -- 否 --> A5.3.3 成本:HPWL + 密度懲罰#
線長用半周長線長(Half-Perimeter Wire Length)估計:
這是 Steiner 樹長度的下界,計算只要掃一遍端點,且對移動單一端點的 增量更新極廉價——退火每步要做幾萬次,這是唯一現實的選擇:
let hpwl = |k: usize, pos: &Vec<(u32, u32)>| -> f64 {
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for &c in &pnets[k].cells { ... x0 = x0.min(x); ... }
for &p in &pnets[k].pins { ... }
((x1 - x0) + (y1 - y0)) * weights.get(&pnets[k].bit).copied().unwrap_or(1.0)
};
weights 來自 mod.rs 的 timing_weights()(時序驅動 pass):
crit.iter().map(|(b, c)| (*b, 1.0 + 6.0 * (*c as f64).powi(8)
/ (fanout.get(b).copied().unwrap_or(1).max(1) as f64).sqrt())).collect()
只加重近臨界(criticality 接近 1)的 net,並以 \(\sqrt{\text{fanout}}\) 折減—— 為什麼折減? 一條高扇出又高權重的 net 會把散布全 die 的端點全拉近, 擠爆它周圍的佈局;criticality⁸ 的陡峭次方則保證只有真正臨界的 net 才被拉。
純線長最佳化有個著名病理:cell 全擠到 pin 附近,其餘 die 空著、局部繞線 密度爆炸。所以成本加上密度 bin 懲罰:
let (bw, bh) = (32u32, 4u32); // 16 × 16 µm 的 bin
let target = (util + 0.15).min(0.9);
let pen = |used: f64, cap: f64| lambda * (used - target * cap).max(0.0);
懲罰是單側線性(超過 target × 容量 才罰),每 bin 的 bin_used 在 move/swap 時增量維護——拒絕時逆序回退:
for &(b, dw) in &bins { delta += pen(bin_used[b] + dw, bin_cap[b]) - pen(bin_used[b], bin_cap[b]); bin_used[b] += dw; }
...
} else {
for &(b, dw) in bins.iter().rev() { bin_used[b] -= dw; } // undo
...還原 occ/pos...
}
5.3.4 增量評估:退火能跑起來的唯一理由#
naïve 做法每步重算全部 net 的 HPWL: \(O(N)\) 。本實作只重算被動到的 net:
touched.clear();
for (c, _, _) in &plan { for &k in &cell_nets[*c] { if stamp[k] != gen { stamp[k] = gen; touched.push(k); } } }
let newc: Vec<f64> = touched.iter().map(|&k| { let v = hpwl(k, &pos); delta += v - ncost[k]; v }).collect();
stamp/gen 是去重蓋章法(同一 net 一個 plan 只算一次)。cell 平均接 2–4 條 net,所以每步成本是常數級,總複雜度 \(O(\text{rounds} \times \text{moves}) = O(n)\) 量級——瀏覽器裡跑幾千顆 cell 可行的原因。
時鐘/非同步 reset 網路不參與線長退火:
pub fn special_nets(nl: &Netlist) -> HashSet<Bit> { ... "CK" | "RN" | "SN" ... CLKBUF ... }
let pnets: Vec<PNet> = nets(nl, &pins).into_iter().filter(|p| !special.contains(&p.bit)).collect();
為什麼? 時鐘樹的形狀由 CTS 之後才決定(5.6),而且時鐘網路是星狀 (一源多載),HPWL 會誤導退火把所有 FF 拉成一團——那既不可能(FF 屬於 各自邏輯簇)也不需要(CTS 會建平衡樹)。
5.3.5 時序驅動 pass 與 keep-the-better#
mod.rs 的 physical() 在第一次(純線長)佈局 + STA 之後,用臨界度加權 再退火(可能從原佈局精修,或小設計整個重來),並只在「可繞且更快」時 採用:
let p2 = place::place(&mapped, seed, util, &weights, start.as_deref());
let r2 = route::route(&mapped, &p2);
let t2 = sta::analyze(&mapped, &p2, &r2, extra_hold);
if r2.overflow == 0 && t2.fmax > timing.fmax { placement = p2; routing = r2; timing = t2; }
RowPlacement.history 記錄每輪的 (temp, cost, accept, pos)(上限 40 個 frame),供網頁端畫退火過程曲線——退火過程本身就是教學素材。
5.3.6 與 SKY130 重試的銜接#
佈局層不管詳細繞線,但 die 夠不夠鬆由它定:flow_with 在詳細繞線有 conflict/opens 時以 util = r.9 * 0.85 重跑 physical(),等於回到本章 重新退火一個更大的 die(見 5.1.2)。另一個方向:physical() 開頭的 route probe(route_with(..., 8))若 overflow 大或 demand 高,也會直接 util *= 0.8 重退火。佈局品質不只有線長一個面向,繞得通是前提。
5.3.7 虛擬碼總覽#
place(nl, seed, util, weights, start):
# 1. 幾何準備
die 大小 ← cell 總 site / util(巨集 +10% 餘量)
巨集 shelf packing(放不下就長大 die)
IO pin 均勻分布到四邊 → 吸附繞線網格(佔用讓位)
# 2. 初始解
標準 cell 按拓撲序蛇形鋪排(每列配額 ∝ 空位數,奇數列反向)
放不下的 spill cell 找第一段空位
if start: 清空標準 cell,按 start 座標重放(精修模式)
# 3. 成本狀態
ncost[k] ← 每條 net 的 weighted HPWL
bin_used/bin_cap ← 32×4 site 的密度 bin
cost ← Σ ncost + Σ pen(bin)
T ← 200 顆 cell net 成本的標準差;rlim ← 大範圍
# 4. 退火
for round in 0..400:
for move in 0..max(12n, 2000):
隨機 cell a,範圍 rlim 內選 (r2, x2)
plan ← move(有空位)或 swap(雙方可放、同列不重疊)
套用 plan;增量算 Δ(touched nets + bins)
if Δ≤0 or rand < e^(−Δ/T): 接受(更新 ncost/cost)
else: 還原 occ/pos/bins
rate ← accepted/moves
T ← 依 rate 四段調整;rlim ← (1 − 0.44 + rate)·rlim
if T < 0.005·cost/nets: break
return RowPlacement(pos、pins、history、max_density…)
對照 Rust 原始碼讀,每一段都有對應的行:初始鋪排在 place.rs:170-220、 初始溫度在 :258-264、主迴圈在 :274-334。虛擬碼省略的只有 start.is_some() 時的 temp *= 0.02; rlim = 2.0 精修模式,以及 history 壓縮到 40 個 frame 的收尾。
5.3.8 已知的簡化與它們的後果#
誠實列出本實作相對工業工具的簡化,以及為何在教育語境下可接受:
- HPWL 而非 Steiner/RSMT:退火階段的線長估計系統性偏低約 10–30%(Steiner 樹有分支節點);但這是單調偏差,退火只在乎相對 好壞,真正的線長由全域繞線與詳細繞線量測。
- 無巨集方向旋轉:SRAM 只能 N/FS(列翻轉),不能 90° 轉——教學 設計的巨集少且大,shelf packing 的位置已由面積主導。
spillcell 的落點不管線長(:204的let _ = spill):初始解 允許幾顆 cell 隨便落位,退火會立刻把它們搬走;省掉的是「初始解 就完美」的不必要工程。- 密度 bin 是懲罰而非硬約束:
max_density可能超過target(RowPlacement.max_density記錄實測值);若超出太多,繞線 probe 會發現並放大 die 重來——硬約束的成本被轉嫁給 5.1 的重試迴圈。
這些簡化共享一個哲學:佈局的最佳化目標不必精確,只要單調; 精確性由下游(繞線、STA)量測,錯了由重試迴圈糾正。
本章重點#
- 佈局 = 列/site 上的合法化組合問題;move/swap 兩種鄰域動作每步保持合法, 讓退火不需要修復步驟。
- Metropolis 準則 \(P = e^{-\Delta/T}\) ; \(T_0\) 由 net 成本統計自適應,排程依 實測接受率四段調整(含 <15% 時回升),
rlim同步縮放搜尋範圍。 - 成本 = Σ weighted HPWL + 密度 bin 線性懲罰;HPWL 是 Steiner 長度下界且 可增量更新(
stamp去重),是退火速度的根基。 - 時鐘/set-reset 網路排除在退火外,交給 CTS;時序驅動 pass 用 criticality⁸/√fanout 權重,且只在「可繞且更快」時採納。
- 利用率由
mod.rs的重試迴圈(probe 失敗 ×0.8、詳細繞線失敗 ×0.85)反覆 餵回退火——佈局與繞線是同一個資源分配問題的兩面。