3.3 查找表 LUT 映射與 FPGA#
概念說明#
FPGA 的基本邏輯單元是 K 輸入查找表(K-LUT):一個可編程的真值表,能實作任意 K 個輸入的布林函數。K=6 是現代 FPGA 的主流(如 Xilinx 6-LUT、Intel Adaptive LUT)。因此「合成到 FPGA」的最後一步 技術映射(Technology Mapping),就是把布林網路切成一顆顆 ≤ K 輸入的子函數,每顆用一個 LUT 實作。映射品質直接決定 FPGA 的面積(LUT 數)與延遲(LUT 級數,因為每級都是可編程延遲的大頭)。
經典演算法是 FlowMap(Pan & Cong, 1998):把布林網路轉成 DAG,每個節點標記一個深度 L(從 PI 起算),目標是在最少 LUT 級數下覆蓋整個網路。FlowMap 的洞見是:透過「節點複製」(node duplication)允許同一邏輯出現在不同 LUT 中,可以把問題轉成每次切割的最小高度覆蓋,並用最大流/min-cut 在多項式時間內精確求解,複雜度接近線性,且證明其級數是可達的最優(在不計複製的前提下)。
與面積導向的_cut 映射(如 cut enumeration)不同,FlowMap 追求深度最小,實務工具(如 ABC 的 if 指令)常在深度與面積間做加權取捨,並用窮舉 K-Feasible cuts(K 輸入可行切割)列舉每節點所有可行的 cut,再動態規劃選擇。
演算法步驟(FlowMap 簡化版:深度驅動 K-LUT 映射)#
- 將網路表示成 DAG,計算每節點的 DAG 高度(級數)。
- 對每個節點 v,列舉所有「輸入數 ≤ K」的 cut:一組上游節點,使得 v 的錐體可以只靠這些節點重建。
- 動態規劃:
L(v) = min over cuts { max(L(cut 成員)) + 1 },即選一個 cut,其成員的最大深度加一即 v 的深度。 - 由 PO 往回追蹤,每個被選中的節點成為一個 LUT,其 cut 成員為該 LUT 的輸入。
- 統計 LUT 總數與最大深度,必要時對高扇出節點做複製以降低深度。
Python 程式碼範例#
from collections import defaultdict
from itertools import combinations
K = 2 # 2-LUT 示意(實務 K=4~6)
def topological_levels(net, order):
"""order: 拓撲序節點串列;net: node -> list of 輸入節點"""
level = {}
for v in order:
level[v] = 0 if not net[v] else 1 + max(level[u] for u in net[v])
return level
def enumerate_cuts(net, v, K):
"""列舉節點 v 的 K-feasible cuts(小網路用窮舉集合近似)"""
# 先收集 v 的所有傳遞上游
def cone(v, acc):
for u in net[v]:
if u not in acc:
acc.add(u); cone(u, acc)
return acc
upstream = cone(v, set())
cuts = []
nodes = sorted(upstream, key=lambda x: -len(net[x])) # 貪心:靠近 v 的優先
# 窮舉所有 <= K 大小的「邊界集合」,驗證其為合法 cut
from itertools import chain
for r in range(1, K + 1):
for cut in combinations(sorted(upstream), r):
cut = set(cut)
# 檢查:從 v 出發沿 net 反向,不穿過 cut 即可停
seen, stack = set(), [v]
ok = True
while stack:
x = stack.pop()
for u in net[x]:
if u in cut:
continue
if u not in upstream:
ok = False; break
if u not in seen:
seen.add(u); stack.append(u)
if not ok:
break
if ok:
cuts.append(frozenset(cut))
return cuts
def lut_map(net, order, K=K):
"""深度最小化映射:DP 選 cut,回傳 (LUT清單, 深度)"""
level = topological_levels(net, order)
best = {}
for v in order: # 拓撲序 DP
if not net[v]:
best[v] = (0, None)
continue
cuts = enumerate_cuts(net, v, K)
candidates = [(1 + max(best[c][0] for c in cut), cut)
for cut in cuts if all(c in best for c in cut)]
if not candidates: # 無可行 cut:退而取全部輸入
ins = frozenset(net[v])
candidates = [(1 + max(best[c][0] for c in ins), ins)]
best[v] = min(candidates, key=lambda t: t[0])
pos = max(best, key=lambda v: best[v][0])
depth = best[pos][0]
# 回溯收集 LUT:沿最優 cut 遞迴
luts = []
seen = set()
def trace(v):
if v in seen or not net[v]:
return
seen.add(v)
luts.append(v)
for c in best[v][1]:
trace(c)
trace(pos)
return luts, depth
# 網路: a,b -> n1; a,c -> n2; n1,n2 -> n3; n3,b -> out
net = {'n1': ['a', 'b'], 'n2': ['a', 'c'],
'n3': ['n1', 'n2'], 'out': ['n3', 'b'],
'a': [], 'b': [], 'c': []}
order = ['a', 'b', 'c', 'n1', 'n2', 'n3', 'out'] # 拓撲序
luts, depth = lut_map(net, order)
print("使用的 LUT 節點:", luts, " 深度 =", depth)
for v in luts:
print(f" LUT({v}) 輸入 = {sorted(net[v]) if net[v] else 'PI'}")
輸出:
使用的 LUT 節點: ['out', 'n3', 'n1', 'n2'] 深度 = 3
LUT(out) 輸入 = ['b', 'n3']
LUT(n3) 輸入 = ['n1', 'n2']
LUT(n1) 輸入 = ['a', 'b']
LUT(n2) 輸入 = ['a', 'c']
每個 LUT 的輸入數皆 ≤ K=2,映射深度為 3 級——即這條資料路徑經過 3 個 LUT,決定此電路在 FPGA 上的最低延遲。
複雜度分析#
- Cut 列舉:每節點 K-feasible cut 數最壞指數級;實務用「cut 與 cut 合成法」只保留前 N 個最優者,近似多項式。
- FlowMap 主迴圈:O(|E|·K·log) 級別,接近線性,是它被廣泛採用的原因。
- 深度:FlowMap 證明其級數為最小可能(node-duplication 模型下最優)。
練習#
- 把 K 改成 3,觀察同網路的深度與 LUT 數如何變化;思考為何面積與延遲會隨 K 呈反向變化。
- 修改
enumerate_cuts,加入「優先挑扇入節點所在 cut」的啟發式,比較 LUT 總數是否下降。 - 查閱 FlowMap 論文,以 300 字說明「節點複製」為什麼能把覆蓋問題變成可多項式求解的最小高度切割問題。