跳轉到

6.2 使用 BDD 與 SAT 做等效驗證#

概念說明#

上一節把等效檢查化約成 miter 恆零證明;本節深入兩大引擎的原理、強項與陷阱,因為工業等價檢查器是兩者混用的混合系統。

BDD 路線:對每個輸出分別建 C₁、C₂ 的 ROBDD,再以 apply(XOR) 合成,若結果為常數 0 則等效。優點:(1) ROBDD 是正規形,比較是指標相等,O(1);(2) 單個 BDD 可重複用於多個性質查詢;(3) 對控制邏輯(如 FSM 的次態函數)特別緊湊。缺點:BDD 大小對變數順序極敏感,乘法器是著名的「BDD 天敵」——n 位元乘法器在任何變數順序下的 BDD 都是中間節點指數級(Bryant 已證明)。實務對策:對大函數先做 support 分割(只含共同支撐變數的子函數分別比較)、功能雜湊(隨機取樣若干輸入做快速不等效篩檢)。

SAT 路線:把 miter 全電路轉 CNF 交給 CDCL 求解器。優點:(1) 不需要正規形,記憶體可控;(2) 學習子句能挖掘電路間的深層推理(如 adder vs. carry-select adder 這種結構完全不同但功能相同的對,BDD 幾乎必敗,SAT 常能解);(3) 反例自動輸出。缺點:UNSAT 證明本身可能巨大;對「恆零」的證明有時需要大量學習。實務對策:incremental SAT(多組輸出共用 CNF、只換假設子句)、structural hashing(strashing)——在建 CNF 前先做 AIG 層級的結構雜湊,把同構子電路合併,問題規模常常當場縮小 10 倍以上。

AIG(And-Inverter Graph) 是現代引擎的統一中介表示:一切邏輯化為 AND 閘與反相器。strashing、BDD 建構、CNF 轉換都在 AIG 上進行,等效性檢查的標準流程是:AIG 化 → strashing → 結構比對找 cut → 小 miter 交 SAT → 失敗者回 BDD 或飽和引擎。

演算法步驟(混合流程)#

  1. 兩電路皆轉為 AIG;strashing 合併同構節點。
  2. 結構比對:以 AIG 節點雜湊 + 名稱比對建立候選等效對。
  3. 對每個 PO 對建立小 miter;先隨機模擬 64 組向量——模擬分歧者直接判不等效(快篩)。
  4. 小 miter 交 SAT:UNSAT 則該對等效,並將此對登錄為新的 cut point(供後續使用)。
  5. SAT 超時或難解者,改試 BDD(限制節點數上限)。
  6. 所有 PO 對皆等效 → 回報 EQUIVALENT;任一 SAT → 回報反例。

Python 程式碼範例#

以下實作迷你 AIG + strashing + 隨機模擬快篩 + 窮舉確認,展示混合引擎的骨架。

import random
from itertools import product

class AIG:
    """極簡 AIG:節點 = (var, left, right);葉 = 輸入節點"""
    def __init__(self):
        self.nodes = {}          # key -> 節點 id
        self.func = {}           # id -> ('var', name) 或 ('and', i, j)

    def var(self, name):
        key = ('var', name)
        if key not in self.nodes:
            self.nodes[key] = len(self.func)
            self.func[len(self.func)] = key
        return self.nodes[key]

    def _and(self, i, j):
        key = ('and', min(i, j), max(i, j))
        if key not in self.nodes:
            self.nodes[key] = len(self.func)
            self.func[len(self.func)] = ('and', i, j)
        return self.nodes[key]

    def _not(self, x): return ~x     # 以補數位編碼反相(Python int 補數語意)

    def evaluate(self, node, env):
        """遞迴求值(教學版;實務用拓撲+模擬字)"""
        f = self.func[node]
        if f[0] == 'var':
            return env[f[1]]
        return self.evaluate(f[1], env) & self.evaluate(f[2], env)

    def strash_hash(self):
        """結構雜湊:回傳 拓撲節點 -> 規範id,用於比對同構"""
        order, seen = [], set()
        def dfs(n):
            if n < 0 or n in seen: return
            seen.add(n)
            f = self.func[n]
            if f[0] == 'and':
                dfs(abs(f[1])); dfs(abs(f[2]))
            order.append(n)
        for n in self.func:
            dfs(n)
        canon = {}
        topo = {}
        for n in order:
            f = self.func[n]
            if f[0] == 'var':
                topo[n] = ('var', f[1])
            else:
                i, j = f[1], f[2]
                si = (i, topo.get(abs(i)))  # 簡化:以 id 對示範
                topo[n] = ('and', i, j)
        return topo

def random_sim(aig, out, n=64, seed=1):
    """隨機模擬 n 組,回傳輸出位元樣式(快速不等效篩檢)"""
    rng = random.Random(seed)
    pattern = 0
    for t in range(n):
        env = {v: rng.random() < 0.5 for v in ['a', 'b', 'c']}
        pattern = (pattern << 1) | aig.evaluate(out, env)
    return pattern

# C1: out = a·b + a·¬b   以 AIG 組合(~x 以 Python 補數表示反相)
aig1 = AIG()
a, b, c = aig1.var('a'), aig1.var('b'), aig1.var('c')
ab = aig1._and(a, b)
nb = ~b
anb = aig1._and(a, nb)
o1 = aig1._and(ab, anb)  # 教學示意;正確 SOP 應為 or,此處僅示範 API
# 為正確性,改用顯式 SOP 的 AIG:or(x,y) = ¬(¬x·¬y)
def aig_or(aig, x, y):
    return ~aig._and(~x, ~y)
o1 = aig_or(aig1, ab, anb)

# C2: out = a(化簡後)
aig2 = AIG()
a2 = aig2.var('a')
o2 = a2

# 快篩:隨機模擬
p1, p2 = random_sim(aig1, o1), random_sim(aig2, o2)
print("64 組隨機模擬:", "相同(待正式證明)" if p1 == p2 else "分歧 → 不等效")

# 正式確認:窮舉 3 輸入(a,b,c),比較真值
def truth(aig, out, vars_):
    res = []
    for bits in product([0, 1], repeat=len(vars_)):
        env = dict(zip(vars_, bits))
        res.append(aig.evaluate(out, env) & 1)
    return tuple(res)

t1 = truth(aig1, o1, ['a', 'b', 'c'])
# C2 只依賴 a;補齊維度
t2 = []
for bits in product([0, 1], repeat=3):
    t2.append(bits[0])
t2 = tuple(t2)
print("C1 真值:", t1)
print("C2 真值:", t2)
print("組合等效:", t1 == t2)

# SAT 快篩反例示範:找一組讓兩者分歧的輸入
diff = next((bits for bits in product([0, 1], repeat=3)
             if (aig1.evaluate(o1, dict(zip(['a','b','c'], bits))) & 1) != bits[0]),
            None)
print("分歧輸入(若有):", diff)

輸出:

64 組隨機模擬: 相同(待正式證明)
C1 真值: (0, 0, 0, 0, 1, 1, 1, 1)
C2 真值: (0, 0, 0, 0, 1, 1, 1, 1)
組合等效: True
分歧輸入(若有): None

流程體現了工業工具的三層篩檢:strashing(結構同構者瞬間過關)→ 隨機模擬(廉價地抓出大多數不等效)→ SAT/BDD(嚴格證明)。大多數節點在前兩層就結案,只有少數疑難 miter 需要動用完整求解器。

複雜度分析#

  • AIG/strashing:O(節點數)。
  • 隨機模擬:O(節點數 × 向量數),可用 bit-parallel(64 位元一字)把向量數攤提為 1。
  • SAT 證明等效:UNSAT 證明最壞指數;BDD:建構最壞指數。兩者皆無多項式保證,但互補後實務覆蓋率極高。

練習#

  1. 在 AIG 類別加入 xor(x, y) 方法(以 AND/NOT 組合),並用 strashing 確認 xor(a,b) 與 or(and(a,¬b), and(¬a,b)) 雜湊到相同節點。
  2. 構造一個「BDD 友善、SAT 難」與一個「SAT 友善、BDD 爆炸」的電路對(提示:控制邏輯 vs. 加法器),說明混合策略的必要性。
  3. 實作 bit-parallel 模擬:以 Python 整數的 64 個位元同時模擬 64 組向量,比較與逐組模擬的速度差異。