大数跨境

从零理解组合逻辑仿真:并行求值与信号收敛

从零理解组合逻辑仿真:并行求值与信号收敛 ai算法芯片与系统
2026-10-18
4
导读:组合逻辑仿真的本质是信号收敛:输入变化后,仿真器在零时间 delta 循环中并行求值、迭代到队列为空。依赖图与拓扑排序是加速技巧,适用于纯组合、静态可确定的网络;动态控制流、NBA 和副作用仍需 de

 

组合逻辑在硬件里是并排的电路,信号像水流一样从输入流到输出。但在仿真器里,它却是另一番景象:没有物理延迟,没有门电路,只有事件队列和零时间的 delta 循环。这篇文章从最简单的组合逻辑出发,逐步解释仿真器如何并行求值、如何迭代收敛,以及依赖图作为一种加速技巧在其中扮演什么角色。

📌 本文讨论的是 SystemVerilog 仿真语义,不涉及具体算法实现。所有例子均为抽象的组合逻辑片段。


一、组合逻辑:算法串行,硬件并行

先看一个最简单的例子:


   
   
   
   
    
   
   
   
   assign s = a + b;
assign y = s << 1;

从算法上看,这是两步串行:

  • • 先算 s = a + b
  • • 再算 y = s << 1

第 2 步依赖第 1 步的结果。但在硬件里,这两个运算物理上并排存在,用导线连接:

信号从 a,b 进去,经过加法器,再经过移位器,从 y 出来。要点如下:

  • • 没有时钟,不需要节拍;
  • • 没有寄存器,不需要保存中间结果;
  • • 一拍都不用,纯粹靠传播延迟。

这就是组合逻辑:算法上的串行步骤,在硬件上展开成空间并排的电路。

交换两句 assign 的顺序,结果完全一样:


   
   
   
   
    
   
   
   
   wire [7:0] s;
wire [8:0] y;

assign y = s << 1;
assign s = a + b;

原因在于:

  • • assign 是并发语句;
  • • 仿真器按依赖关系求值,而不是按语句顺序;
  • • 只要依赖关系无环,顺序无关。

二、仿真器不综合,它解释过程

综合工具会把 if/else 展开成 MUX,把 for 循环展开成并行逻辑,最终生成门级网表。但仿真器不这么做。

仿真器的目标是模拟语言语义,包括:

语义
值依赖能表达吗
if/else
✅ 可展开成 MUX
case
✅ 可展开成 MUX
for
 循环
⚠️ 可展开,但可能爆炸
阻塞赋值立即生效
❌ 时间语义
NBA 读旧值
❌ 时间语义
多次赋值覆盖
❌ 过程语义
$display
 顺序
❌ 副作用
force/release
❌ 动态语义

只要设计里出现任何一项,纯静态展开就不够了。通用仿真器不能假设用户只写纯组合网络,所以必须用统一的调度机制兜底。


三、并行求值:delta 内同时进行

仿真器是单线程程序,不能真正并行求值。但它用 delta 循环模拟并行行为:

在同一个 delta 内,所有 RHS 读旧值,LHS 在 NBA 区统一更新。效果上等价于并行求值。

关键是同一个 delta 内的多个信号更新:


   
   
   
   
    
   
   
   
   assign x = a + b;
assign y = c + d;
assign z = e + f;

a 变化时,delta 1 里:

  • • x 重新求值;
  • • y 不变(c, d 没变);
  • • z 不变(e, f 没变)。

如果 a 和 c 同时变化,delta 1 里 x 和 y同时重新求值,互不影响。这就是并行求值。

同一个 delta 内,多个信号独立求值,互不依赖。这就是“并行”的体现。


四、NBA 的两阶段语义


   
   
   
   
    
   
   
   
   always_ff @(posedge clk) begin
    a <= b;
    c <= d;
end

执行要点:

  • • a 和 c 的 RHS 在 Active 区并行求值(读 b, d 的旧值);
  • • a 和 c 的 LHS 在 NBA 区同时更新。
特性
阻塞赋值
非阻塞赋值
更新时机
立即
NBA 区
RHS 读值
可能读到新值
永远读旧值
语句顺序
有影响
无影响
用途
组合逻辑
时序逻辑

RHS 读旧值,LHS 在 NBA 区统一更新。 这就是为什么 NBA 能模拟“所有寄存器同时更新”。


五、收敛过程:迭代到稳定

并行求值只做一轮还不够,因为信号之间有依赖:


   
   
   
   
    
   
   
   
   assign s1 = a + b;
assign s2 = s1 + c;
assign s3 = s2 + d;

迭代过程:

  • • delta 1:s1 更新;
  • • delta 2:s2 更新(依赖 s1 的新值);
  • • delta 3:s3 更新(依赖 s2 的新值);
  • • delta 4:没有新事件,收敛。

从图上看,可以把“下一轮迭代读上一轮结果”画成一条回边,表示迭代到不动点:

这条回边不是电路里的环,而是迭代求解的回边:每一轮 delta 用上一轮的输出作为输入,直到所有信号不再变化。

每一轮 delta 内部并行求值,轮与轮之间通过回边迭代,直到稳定。 迭代次数取决于信号传播的逻辑深度:

delta
更新的信号
1
s1
2
s2
3
s3
4
y
5
队列为空,退出

六、delta 循环的触发与退出

6.1 什么时候触发 delta 循环

只要一个信号在当前 delta 内被更新,而这个更新又触发了另一个进程或连续赋值重新求值,就会进入下一个 delta。

触发源
例子
为什么触发新 delta
连续赋值传播
assign y = s << 1;
 中 s 变化
y
 需要重新求值
always_comb
 输入变化
always_comb z = x + 1;
 中 x 变化
块需要重新执行
NBA 更新
always_ff
 中 s1 <= ma;
LHS 变化可能触发下游逻辑
阻塞赋值
always_comb
 中 a = b;
立即更新,可能触发敏感块
#0
 延迟
#0 x = y;
推迟到 Inactive 区,再触发

6.2 退出判据:队列为空

delta 循环的唯一退出判据是:

当前仿真时刻内,Active 队列和 NBA 队列都为空。

6.3 eval 的内部实现

仿真器的主循环伪代码:


   
   
   
   
    
   
   
   
   while (仿真时间未结束) {
    while (Active 队列非空 || NBA 队列非空) {
        // 一个 delta
        处理 Active 队列
        处理 Inactive 队列
        处理 NBA 队列
        处理 Observed 队列
    }
    推进仿真时间到下一个事件
}

内层 while 就是 delta 循环。 它一直执行,直到所有队列为空。


七、与真实电路收敛的相似性

delta 循环和真实电路的信号收敛,在逻辑语义上高度相似:

项目
delta 循环
真实电路
传播单位
一个事件
一个门延迟
求值方式
delta 内并行
物理并行
传播顺序
依赖关系驱动
物理连线
终止条件
事件队列为空
所有信号稳定
迭代次数
逻辑深度
门级数
失败情况
组合环,死循环
组合环,振荡

关键区别:

  • • 时间消耗:delta 循环是零时间,真实电路是纳秒级;
  • • 仿真时间:delta 循环不前进,真实电路持续前进;
  • • 毛刺:delta 循环通常不可见,真实电路可见;
  • • 传播速度:delta 循环无限快,真实电路受门延迟限制。

🎯 delta 循环是真实电路传播的“零时间抽象”。它不模拟延迟,但模拟顺序;不模拟毛刺,但模拟稳定值。两者最终稳定值一致,但时间模型不同。


八、有环时不收敛


   
   
   
   
    
   
   
   
   assign a = b & c;
assign b = a | d;

执行过程:

delta
动作
1
a 更新触发 b
2
b 更新触发 a
3
a 更新触发 b
4
b 更新触发 a
…
无限循环

事件队列永远非空,仿真器最后报:


   
   
   
   
    
   
   
   
   Zero-delay loop detected

真实电路里,组合环也会振荡。 所以综合工具拒绝这种设计。


九、加速技巧:依赖图与拓扑排序

delta 循环是语义兜底,保证任何合法设计都能被正确仿真。但它的代价是:每个 delta 只推进一级逻辑,迭代次数等于逻辑深度。对于纯组合网络,这意味着仿真器做了大量“本可以一次性算完”的工作。

依赖图和拓扑排序就是针对这个代价的加速技巧。

9.1 加速的核心思路

项目
朴素 delta 循环
依赖图加速
迭代次数
逻辑深度
1 次拓扑遍历
每轮工作量
只处理被触发的块
按拓扑序一次求值
适合场景
任意合法设计
纯组合、无过程语义
开销
低(事件队列)
中(构图 + 排序)
正确性
通用
仅纯 DAG

9.2 纯组合网络:拓扑排序一次性求值


   
   
   
   
    
   
   
   
   assign s1 = a + b;
assign s2 = s1 + c;
assign s3 = s2 + d;
assign y  = s3 << 1;

构建依赖图:

拓扑排序:s1 → s2 → s3 → y。按这个顺序求值,一次算完,不需要 4 个 delta。

💡 *这就是依赖图加速的本质:用静态分析把“逐级迭代”变成“一次遍历”。*

9.3 依赖图在仿真里的其他加速点

加速点
做法
收益
敏感列表推导
静态扫描 always_comb 读取的信号
减少无效触发
事件合并
同一 delta 内多个变化合并处理
减少队列操作
常量传播
提前算出常量表达式
减少运行时求值
死代码消除
去掉不影响输出的逻辑
减少事件数量
子表达式复用
公共子表达式只算一次
减少重复求值

9.4 依赖图在综合/STA/形式验证中的核心地位

依赖图不仅是仿真加速技巧,在静态分析工具里它更是核心数据结构:

工具
依赖图的作用
为什么离不开
综合
生成门级网表、做优化
需要静态结构
静态时序分析(STA)
找最长路径、计算 slack
纯组合 DAG 上严格成立
形式验证
构建布尔/字级公式,SAT/SMT 求解
需要静态依赖
等价检查
比较两个设计的依赖结构
图同构/包含
功耗分析
统计翻转活动
依赖传播路径
组合环静态检测
综合前发现非法反馈
图论直接判定

💡 在这些场合,依赖图不是“加速手段”,而是*唯一可行的方法。没有它,综合、STA、形式验证都做不了。*

9.5 加速的边界:为什么不能完全替代 delta 循环

依赖图加速有一个前提:依赖关系是静态可确定的。一旦涉及动态语义,这个前提就不成立:

边界
说明
为什么加速失效
动态控制流 if/else
 走哪条路径运行时才定
静态图只能保守画成全部依赖
NBA 两阶段语义
RHS 读旧值,LHS 统一更新
不是值依赖,是时间调度
阻塞赋值立即生效
后面的语句可能读到新值
顺序本身是语义
多次赋值覆盖
最后一次赋值胜出
过程语义,图无法表达覆盖
副作用顺序 $display
、$finish 必须按语句顺序
副作用不是数据流
force/release
运行时动态改变驱动
依赖关系随仿真状态变化
PLI/VPI
外部代码可读写仿真器状态
静态分析无法预知

以 if/else 为例:


   
   
   
   
    
   
   
   
   always_comb begin
    if (sel) y = a + b;
    else     y = c + d;
end

静态依赖可以保守画成 MUX:

这在综合里是正确的:综合工具确实会生成 MUX。但在仿真里,这丢失了两个信息:

  • • 运行时只执行一条分支,另一条分支的副作用(如 $display)不应该发生;
  • • 敏感列表是动态的:sel=1 时 y 只敏感于 a,b,sel,不敏感于 c,d。

所以仿真器不能把过程代码展开成 MUX,只能逐句解释执行,靠 delta 循环传播。

9.6 实际仿真器的混合策略

真实仿真器不是“要么依赖图,要么 delta 循环”,而是分层混合:

设计特征
依赖图加速
delta 兜底
只有 assign
✅ 拓扑排序
可选
有 always_comb 无控制流
⚠️ 部分
✅
有 if/case/for
❌
✅
有 NBA
❌
✅
有副作用
❌
✅
有 force/release
❌
✅
有组合环
❌ 只能静态报错
✅ 运行时检测

🎯 依赖图是*加速技巧,delta 循环是语义兜底。两者不是替代关系,而是分工关系:能静态确定的部分用图加速,不能静态确定的部分用事件迭代。通用仿真器选择“静态分析加速 + delta 循环兜底”,是为了同时兼顾性能和语义正确性。*


十、深度判定:理论可行,实际不用

理论上,可以根据依赖图的最长路径确定最大收敛步数:

无环 DAG 的最长路径有限,delta 次数 ≤ 路径长度。超过则必然有环。

但实际仿真器不这么做,原因在于:

方案
优点
缺点
精确深度
理论上严格
计算开销大,动态依赖难处理
迭代上限
实现简单,O(1) 开销
可能误判,需要调参
静态分析
无运行时开销
只适用于纯组合
delta 循环
通用,正确
可能多迭代,但开销小

实际工程倾向于“简单 + 通用 + 足够快”。 组合环检测通常用全局迭代上限,超过就报错。


十一、总结

问题
答案
概念上是并行求值吗
✅ 是,delta 内并行
然后收敛到稳定吗
✅ 是,delta 间迭代到队列为空
仿真器真的并行吗
❌ 单线程,靠两阶段调度模拟
delta 循环是什么
同一仿真时刻内的零时间迭代
触发条件
信号变化触发敏感块或连续赋值
退出判据
Active 和 NBA 队列都为空
无环时会收敛吗
✅ 一定收敛,迭代次数 ≈ 逻辑深度
有环时会怎样
❌ 死循环,报 zero-delay loop
和真实电路对应
稳定值一致,时间模型不同
依赖图是什么
✅ 一种仿真加速技巧
依赖图在哪最有效
纯组合网络、敏感列表、综合、STA、形式验证
依赖图的边界
动态控制流、NBA、副作用需要 delta
实际仿真器策略
静态分析加速 + delta 循环兜底

🎯 组合逻辑仿真本质上是一个信号收敛过程:输入变化后,仿真器沿依赖关系逐级迭代求值,直到所有信号不再变化。概念上就是并行求值,然后迭代到稳定。从图上看,这个过程可以表示为一组前馈节点加上一条回边——每一轮迭代把上一轮的输出作为输入,直到到达不动点。

在这个基础上,依赖图和拓扑排序是一种加速技巧:对纯组合、无过程语义的网络,用静态排序把“逐级迭代”变成“一次遍历”;对敏感列表推导、常量传播、死代码消除,也能显著减少运行时工作。依赖图在综合、STA、形式验证里更是核心数据结构。

但加速有边界:动态控制流、NBA 两阶段语义、副作用顺序、force/release、PLI 调用,都无法用静态图完整表达。所以通用仿真器选择“静态分析加速 + delta 循环兜底”:能静态确定的部分用图加速,不能静态确定的部分用事件迭代。两者分工,共同保证仿真器的性能和语义正确性**。delta 循环不是优化,而是语义正确性的最后一道防线。**

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 234
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读6.6k
粉丝0
内容234