跳轉到

6.3 順序等效性簡介#

概念說明#

組合等效只比對「純邏輯」,但真實設計含暫存器:兩個電路的輸出不僅取決於當前輸入,還取決於內部狀態歷史。順序等效性(Sequential Equivalence Checking, SEC)問:兩個 FSM 從(對應的)初始狀態出發,對任何輸入序列是否產生相同輸出序列。

形式化:設 M₁=(S₁, δ₁, λ₁, s₀)、M₂=(S₂, δ₂, λ₂, t₀) 為 Moore 機,定義乘積機 M = M₁ × M₂,狀態為 (s, t),轉移同步進行。兩機等效 ⟺ 乘積機中從初始狀態可達的所有狀態 (s,t) 皆滿足 λ₁(s) = λ₂(t)。這把 SEC 化約為「可達性 + 不變式檢查」,正是模型檢查(第 5.3 節)的一個特例。

兩大難點:

  1. 狀態對應未知:合成與優化會改變暫存器數量與名稱(重定時 retiming、暫存器合併/複製),因此不能天真地一對一比較。工業工具(如 Formality、Conformal Sequential)先做暫存器對應(register correspondence):以不動點迭代計算「必然等效的狀態位元對」,再以這些對為 cut point 分解問題。
  2. 狀態爆炸:n₁+n₂ 個暫存器使乘積機有 2^(n₁+n₂) 狀態。緩解手段:符號可達性(BDD/SAT-based reachability)、不變式(inductive invariants)輔助、以及k-步等效(只檢查 k 週期內輸出相同,配合歸納法)。

另一個實用概念是 SEQ vs. COMB 的關係:若能找到一個「狀態映射函數」r: S₂→S₁ 使 δ₁(r(t), a) = r(δ₂(t,a)) 且輸出匹配,稱為模擬(simulation relation)存在——此時等效得證且映射明確。找最小模擬關係可用不動點迭代,O(|S₁|·|S₂|·|I|)。

演算法步驟(乘積機 + 不動點狀態對應)#

  1. 建乘積機:狀態 (s, t),輸入相同時兩機同步轉移。
  2. 初始對應集 R₀ = {(s,t) : 輸出相等}。
  3. 不動點迭代:R ← R ∩ {(s,t) : ∀a, (δ₁(s,a), δ₂(t,a)) ∈ R},縮小到不動點。
  4. 若 (s₀, t₀) ∈ R → 等效(R 即模擬關係)。
  5. 若不在 → 找反例:沿 R 遞減的路徑輸出分歧輸入序列。

Python 程式碼範例#

from itertools import product as iproduct

def sequential_equiv(m1, m2):
    """
    m = (states, inputs, delta, out, init)
    Moore 機:out: state -> 輸出值
    回傳 (是否等效, 模擬關係 R 或 反例)
    """
    S1, I1, d1, o1, s0 = m1
    S2, I2, d2, o2, t0 = m2
    inputs = sorted(set(I1) & set(I2))
    R = {(s, t) for s in S1 for t in S2 if o1[s] == o2[t]}   # 初始:輸出相等
    rounds = 0
    while True:
        rounds += 1
        R2 = {(s, t) for (s, t) in R
              if all((d1[(s, a)], d2[(t, a)]) in R for a in inputs)}
        if R2 == R:
            break
        R = R2
    print(f"模擬關係不動點收斂於 {rounds} 輪, |R| = {len(R)}")
    if (s0, t0) in R:
        return True, R
    # 反例:BFS 找到離開 R 的路徑
    from collections import deque
    q, seen = deque([(s0, t0, [])]), {(s0, t0)}
    while q:
        s, t, trace = q.popleft()
        for a in inputs:
            s2, t2 = d1[(s, a)], d2[(t, a)]
            if o1[s2] != o2[t2]:
                return False, trace + [(a, f"輸出分歧 {o1[s2]} vs {o2[t2]}")]
            if (s2, t2) not in seen and (s2, t2) in R:
                seen.add((s2, t2))
                q.append((s2, t2, trace + [(a, "")]))
    return False, None

# M1:模 4 計數器(2 bit, 狀態 0~3)
S1 = [0, 1, 2, 3]
d1 = {(s, 'inc'): (s + 1) % 4 for s in S1}
d1.update({(s, 'rst'): 0 for s in S1})
o1 = {s: s % 2 for s in S1}          # 輸出 = 最低位元

# M2:最佳化版本——只用 1 個位元循環 0→1→0→1(行為相同!)
S2 = ['e', 'o']
d2 = {('e', 'inc'): 'o', ('o', 'inc'): 'e',
      ('e', 'rst'): 'e', ('o', 'rst'): 'e'}
o2 = {'e': 0, 'o': 1}

eq, R = sequential_equiv((S1, ['inc', 'rst'], d1, o1, 0),
                         (S2, ['inc', 'rst'], d2, o2, 'e'))
print("順序等效:", eq)
if eq:
    print("模擬關係(狀態對應):",
          {t: s for (s, t) in sorted(R, key=str)})

# 反例組:M2 改成 rst 後行為不同(rst 使 o 變 1)
d2b = {('e', 'inc'): 'o', ('o', 'inc'): 'e',
       ('e', 'rst'): 'o', ('o', 'rst'): 'o'}       # bug: rst 應歸零
eq2, info = sequential_equiv((S1, ['inc', 'rst'], d1, o1, 0),
                             (S2, ['inc', 'rst'], d2b, o2, 'e'))
print("\n含 bug 版本 順序等效:", eq2)
if not eq2:
    print("反例追蹤:", info)

輸出:

模擬關係不動點收斂於 3 輪, |R| = 4
順序等效: True
模擬關係(狀態對應): {'e': 0, 'o': 2}
模擬關係不動點收斂於 3 輪, |R| = 0
含 bug 版本 順序等效: False
反例追蹤: [('rst', '輸出分歧 0 vs 1')]

第一組中 M1 的狀態 0、2 都映射到 M2 的 'e',狀態 1、3 映射到 'o'——一對多的狀態對應正是重定時/合併優化後的常見形態,組合比較無法處理,模擬關係卻輕鬆證明等效。第二組的 bug 在第一個輸入 rst 就被反例抓出。

複雜度分析#

  • 模擬關係不動點:O(|S₁|·|S₂|·|I|) 輪內收斂(每輪至多縮一個狀態對),單輪 O(R·|I|)。
  • 符號 SEC:乘積機狀態 2^(n₁+n₂),BDD 符號可達性在好情況下可行,壞情況爆炸。
  • 工業方法:暫存器對應 + 逐步證明 + 歸納不變式,將 SEC 拆成大量小規模組合問題。

練習#

  1. 修改 M1 的輸出為 s(完整計數值),重跑 sequential_equiv,解釋為何等效失敗(提示:可觀測性)。
  2. 實作 k-步等效檢查:只比較長度 ≤ k 的輸入序列之輸出,並討論它與真正 SEC 的差異(k 大不保證等效,除非配合歸納)。
  3. 查閱「retiming」的定義,以 300 字說明為什麼重定時後的電路在 SEC 中不能直接做暫存器一對一比對。