
后仿跑到第七个小时终端上那行百分比数字已经二十多分钟没动过了。你盯着屏幕心里没底——它到底还在算还是早就卡死在某个时间步里出不来了前者的情况下关掉就白跑一晚上后者的情况下干等下去也是白耗一个下午。这种时候一份能告诉你这次run从几点开始、在哪个corner下跑、跑到哪一步、有没有命中报错关键字、波形存哪了的仿真过程状态记录比任何调试技巧都值钱。这篇聊的就是后仿post-layout simulation里的过程状态记录这件事。它听起来像是工程管理层面的杂活跟写testbench、调波形没半点关系但真到了仿真发散、X态乱飞、跑一次要十几个小时的阶段这套记录就是你唯一的抓手。它适合已经跑过前仿、手上有带寄生参数网表的人也适合刚接手别人后仿工程、面对一堆没有说明的日志文件一脸懵的人。下面我按为什么后仿的记录方式和前仿不一样—记录哪些字段—怎么让记录自动产生—出X态时怎么用—收敛失败和超长仿真怎么兜底—我踩过的坑这条线讲。1. 后仿的状态记录为什么不能照搬前仿那套1.1 一次run的代价从分钟级跳到小时级前仿的时候我们对状态这件事是很随意的。RTL层次的仿真一次跑完三五分钟觉得不对改两行重跑一遍就行。记录什么顶多是在终端里翻翻滚动历史。这个习惯延续到后仿就会出事。一旦网表换成带寄生RC的门级网表节点数从几万涨到几百万同样的激励仿真时间可能从八分钟变成十二个小时甚至更长。时间放大两三个数量级以后重跑一次从随手操作变成了需要排进日程的事情。代价变了记录的定位就变了。前仿的记录是为了复现问题后仿的记录首先是为了别让已经花掉的机时白白蒸发。你得能在不重跑的前提下判断这次run是正常结束、是超时、还是崩了它跑到哪个时刻上一次成功的run用的什么配置。没有这些你面对的就是一个黑盒而打开黑盒的成本是一次通宵。我个人的习惯是后仿开始之前先把预期时长估出来。方法不复杂先跑一个很短的时间窗比如几微秒仿真时间记录真实墙钟耗时再按目标仿真总时长线性外推最后乘一个1.5到2的余量系数。因为后仿后期由于网络进入深度充放电、事件密度变化单位时间的墙钟开销往往会比初期高。把这个预估值写进状态记录里之后每次看进度你心里就有个参照现在这个速度是正常范围还是已经慢得离谱了。这个预估值本身也是状态的一部分它能让下一次run的判断更快。1.2 后仿里最容易丢的三类现场X态、不收敛、时序违例后仿区别于前仿有三类问题几乎必然会遇到而它们共同的特点就是现场极其宝贵丢了就很难重建。第一类是X态。开了X传播分析很多仿真器叫xprop之后你会发现在前仿里能正常跑通的激励到了后仿里某些节点的波形全变成红线——这正是仿真波形是红线这种说法的来源红线通常意味着该信号处于未知态。X态可能来自未初始化的寄存器、复位逻辑覆盖不全、多驱动冲突也可能来自时序违例之后亚稳态的建模。它出现的时刻、首次出现的信号、传播路径都是定位问题的关键。第二类是不收敛。后仿里如果带了模拟或者混合信号部分瞬态仿真会因为寄生电容电阻让时间常数变得很小时间步被迫压缩很容易报出timestep too small之类的收敛失败。这类问题的现场就是收敛日志——哪一个时间点、哪一个节点、迭代了多少次没收敛。第三类是时序违例。SDF反标之后建立时间和保持时间的违例会在仿真里被标出来这些违例报告通常只打印一次滚过去就没了。这三类现场的共同点是它们都产生在仿真过程中而不是结束后一旦run被中断、日志被覆盖、波形没保存它们就永久消失了。记录的意义就是让这些瞬间在现场之外还能被找回。2. 一张够用的后仿状态记录表字段该怎么定2.1 最小可用字段集我见过太多团队用一张Excel表格手工记后仿状态前几天还能坚持项目一忙就断更最后表格里的信息比日志还不可信。要做状态记录第一件事是把字段定下来而且字段要足够少——少到你能坚持每一次run都记又足够全——全到你三个月后翻出来能看懂。下面是我常用的一套最小字段集配合表格说明每个字段的作用字段含义为什么必须有run_id本次run的唯一编号后续所有日志、波形、checkpoint都靠它关联起止时间戳墙钟开始/结束时刻判断是否超时、估算下次耗时设计版本代码仓库的commit或tag结果变了能追到是哪次改动网表寄生版本网表文件和SPEF/DSPF的指纹后仿最关键的输入版本SDF版本时序反标文件指纹时序违例相关问题的根源corner工艺角、电压、温度后仿多corner并行时区分结果仿真器及版本工具名加具体版本号换版本后行为变化能定位编译宏/选项关键编译开关xprop等开关直接影响X态表现目标仿真时长计划跑多长仿真时间对照实际进度实际结束状态正常/超时/崩溃/中断最核心的状态字段错误关键字命中grep到的ERROR/FATAL不翻日志就知道有没有事产物路径日志、波形、checkpoint路径找得到东西2.2 每一次run都必须带版本指纹后仿最让人抓狂的一类情况是我明明改了但结果没变化或者反过来结果变了但我不知道是哪一环变了。原因通常就是输入版本混乱网表更新了但寄生文件还是旧的SDF换了但没记录仿真脚本改过一版没人知道。所以每一个输入文件都要能算出一个指纹用哈希或者版本号都行。记录里存指纹不存最新版这种模糊描述。举个实际场景某次后仿X态突然消失了团队以为是自己的复位修改起了作用结果翻状态记录发现同一时间寄生文件被重新提取过一版。如果当时记录里没有寄生文件指纹这个方向可能要被误导两三天。版本指纹还有个附加好处就是能防止隐性重跑。后仿常有多corner并行如果状态记录里的配置指纹完全一致那就说明这两次run其实是重复劳动可以砍掉一个省机时。这在机时紧张的阶段是很实在的收益。2.3 用结构化文件落地别用Excel手记如果要让记录真正有用落地格式很关键。我推荐结构化文本比如JSON Lines——每条run一行JSON追加写方便程序化处理也方便用脚本做diff和聚合。示意结构如下{run_id:post_0412_a,start:2025-04-12T09:13:22,end:2025-04-12T21:40:05,design_commit:a1b2c3d,spef_hash:9f3e…,sdf_hash:77aa…,corner:ss_0p72v_125c,sim:vcs_2024.09,xprop:on,target_us:500,status:timeout,err_hits:[TIMESTEP_TOO_SMALL],log:runs/post_0412_a/sim.log,wave:runs/post_0412_a/wave.fsdb,ckpt:runs/post_0412_a/ckpt_312us}用JSON的好处是你之后想看所有超时的run所有命中收敛错误又用了某一版SDF的run一句脚本就筛出来了。而手填的Excel一旦字段写错、格式不统一检索就是灾难。结构化记录配上少量脚本这才是状态记录该有的样子。3. 让状态自己写下来把记录嵌进仿真的wrapper里3.1 用wrapper脚本包住仿真命令手工记状态坚持不了两周。真正能跑下去的方案是让记录在仿真启动和结束的时候自动发生。做法就是写一个wrapper脚本把仿真器的启动命令包在里面脚本负责记录开始时间、计算输入指纹、启动仿真、捕获退出码、记录结束时间最后把这一条追加进状态文件。一个简化过的bash wrapper大致长这样#!/usr/bin/env bash set -uo pipefail RUN_IDpost_$(date %m%d_%H%M%S) RUN_DIRruns/${RUN_ID} mkdir -p ${RUN_DIR} # 记录开始状态 START$(date -Iseconds) SPEF_HASH$(sha1sum $SPEF | cut -d -f1) SDF_HASH$(sha1sum $SDF | cut -d -f1) # 启动仿真日志落盘 $SIM_BIN -f run.f -l ${RUN_DIR}/sim.log RC$? END$(date -Iseconds) STATUS$([ $RC -eq 0 ] echo ok || echo fail_rc${RC}) HITS$(grep -Eo TIMESTEP_TOO_SMALL|FATAL|ERROR ${RUN_DIR}/sim.log | sort -u | paste -sd, -) printf {run_id:%s,start:%s,end:%s,status:%s,rc:%d,spef_hash:%s,sdf_hash:%s,err_hits:%s}\n \ $RUN_ID $START $END $STATUS $RC $SPEF_HASH $SDF_HASH $HITS runs/status.jsonl关键点有几个。一是set -uo pipefail不加-e因为仿真器返回非零是正常情况不能让脚本在中途退出否则就记不到结束状态。二是退出码一定要单独保存到变量再判断不能直接用$?做多次操作否则会被后续命令覆盖。三是err_hits用grep抓关键字这是把人翻日志变成程序翻日志的核心一步。3.2 日志分级和关键字抓取自动抓关键字这件事前提是你得先定义清楚哪些关键字算问题。不同仿真器、不同仿真类型的报错格式差别很大我一般会维护一张关键字表按严重程度分类级别典型关键字处理方式致命FATAL、ABORT、core dumped立即标记失败查现场错误ERROR、TIMESTEP_TOO_SMALL、NOT_CONVERGED标记失败记录命中的节点/时刻警告WARNING、WARN累计计数超阈值才关注可疑x、unknown、multi-driver后仿重点关注前仿可忽略注意警告这一类不能一律当没事。后仿里很多WARNING在数量少的时候无害但一旦同一个WARNING在日志里出现几百上千次往往意味着某个模块在反复进入异常状态这时候它就等同于错误信号了。所以我建议状态记录里除了命中关键字还要带上每个关键字的出现次数用关键字:次数的形式。这个次数信息在你事后排序哪些run最可疑时非常有用。3.3 checkpoint和续跑的状态对齐后仿时间太长的时候分段跑是常规操作。仿真器通常支持把某个时刻的完整状态存成checkpoint之后从这个checkpoint恢复继续跑。这里的状态记录要特别小心对齐问题checkpoint对应的仿真时刻必须记下来续跑的时候要确认是从这个时刻接着跑而不是从头再来或者跳过了某段。我遇到过一次尴尬的情况一个run中断在312微秒我存了checkpoint续跑时新流程误用了更早的checkpoint结果仿真重复计算了十几微秒波形里同一段激励出现了两次排查了半天才发现是续跑起点错了。如果状态记录里清清楚楚写着每个checkpoint对应的仿真时刻这种错误根本不会发生。所以字段里checkpoint路径要带时刻信息续跑脚本要读这个字段而不是靠人记。4. 后仿跑出X态状态记录怎么帮你把时间倒回去4.1 后端启动X和真实X的区别后仿里的X态大致分两种来源。一种叫启动X是仿真刚开始时很多寄存器还没被复位天然处于未知态随着复位信号到位这些X会自己消失。另一种是真实X是仿真跑到中途某个时刻由于功能逻辑问题或者时序违例突然产生的X而且它会顺着组合逻辑一路传播下去。区分这两种X直接决定了你要不要花时间排查。启动X基本可以忽略真实X必须查。但在长周期的后仿里这两者混在一起波形上就是一片红肉眼很难判断。这时候状态记录的价值就体现出来了如果你在record里记录了X首次出现的仿真时刻和信号名那么越早出现的X越可能是启动X、越晚出现的越可能是真实X这条经验规律就有了数据支撑。你可以先按首次出现时刻排个序把明显跑到很久之后才冒出来的X优先排查。4.2 用状态记录还原第一次发散的时刻仿真发散这个说法本质上描述的就是某个变量在迭代过程中越算越大、无法收敛或者状态传播失控。要定位发散点最有效的做法是找到第一次异常的那个时间点然后往前看一小段。但后仿日志动辄几百MB波形文件几个GB你怎么快速定位第一次异常我的办法是在状态记录里额外维护一个事件时间线。仿真过程中用脚本周期性地扫描日志把X首次出现的时刻、收敛失败的首次时刻、第一个时序违例的时刻都抽出来写进这条run的记录。等run结束你手上就有了一条浓缩的时间线直接跳到那个时刻看波形就行不用把整个GB级波形从头翻到尾。顺便提一个反直觉的现象有时候开了X传播分析xprop之后后仿反而冒出了前仿没有的X。这通常不是工具的问题而是X传播分析把前仿里被乐观处理掉的未知态如实地传播了出来。前仿在某些模式下遇到X会当作0或1继续算显得一切正常开启X传播分析工具会严格地让X往下传于是某个本就存在的隐患就暴露了。所以记录里是否开启xprop这个字段必须留着不然你会误以为是后仿引入了新的bug。4.3 记录里必须留的复现指纹X态这种东西找到一个之后你一定会想做的一件事是用最小激励复现它。而能不能复现在很大程度上取决于你有没有留住复现指纹。除了前面说的网表、寄生、SDF版本还有几个容易被忽略的随机种子如果激励里有随机成分、初始条件设置、复位时序的延迟参数、以及仿真器的求解精度设置。这几项任意一个变了X的出现时刻和路径都可能改变甚至消失。所以状态记录不是只记跑了什么还要记用什么条件跑的。我一般的做法是把这一批复现相关参数单独归到一个repro字段里run结束就固化不允许覆盖。日后要用的时候一条命令就能还原出当时的运行环境。5. 收敛失败与超长仿真状态记录的抗风险设计5.1 收敛日志该怎么读瞬态仿真不收敛日志里通常会有几类信号一是timestep too small说明时间步已经被压缩到下限还是算不下去二是迭代次数超限说明某个时间点的方程组在反复求解三是具体的节点名指出是哪里的电压电流异常。这些信息如果不记录重跑一次又是几小时。所以我的状态记录里针对带模拟部分的仿真会专门留一个字段存收敛最慢的若干个时间点和对应节点。实现方式很简单用脚本在日志里grep收敛相关的行把时间戳和迭代次数抽出来按迭代次数降序取前几名。这样一个字段就相当于把整段收敛日志压缩成了一句话出问题的时候直接看这句话就够了。还有个实用技巧如果同一个run里收敛困难的时刻集中在某几个时间窗那很可能对应的是电路里某个模块切换状态的瞬间比如时钟边沿或者电源切换。记录里把这些时间窗标出来你排查的目标就从整个仿真缩小到了几个瞬间效率差别很大。5.2 大后仿的分段与状态快照对于要跑一整夜甚至几天的大后仿我强烈建议做分段快照。把目标仿真时间切成若干段比如每50微秒一段每段结束就存一次checkpoint同时把这一段的进度、耗时、关键字命中情况写进状态记录。这么做的直接好处是万一在第七段崩了你只需要从第六段的快照续跑前面六段的机时不会浪费。分段还有个隐性好处就是让进度可视化。长仿真最折磨人的就是不知道进度分段之后每一段都是一个可观测的里程碑你可以看着状态记录里一个一个段被标记完成心里有数。而且分段之后单段的耗时数据也出来了你可以据此判断后面的段大概需要多久遇到某一段突然变慢也能及时发现异常。这里要提醒一点分段边界最好选在电路相对平静的时刻比如某个完整事务结束之后避开正在翻转的时钟沿附近。在状态切换的瞬间存快照恢复的时候容易引入额外的不确定性。这属于工程惯例上的细节但真会影响续跑结果的一致性。6. 我在后仿状态记录上踩过的几个坑第一个坑是只记成功不记失败。一开始我的状态记录只在run正常结束时写一条结果那些崩溃、超时的run全都没记录导致我根本不知道某个corner到底试过几次、每次错在哪。后来改成无论成功失败都写记录失败run的价值立刻显现——很多错误其实是重复的看一眼历史记录就知道该往哪个方向查。第二个坑是日志被覆盖。早期我没规范日志路径同一个run目录反复复用第二次运行直接把第一次的日志覆盖了出问题时想追溯前一次的状态什么都没了。后来强制每次run用独立目录且目录名带run_id和时间戳才算根治。第三个坑是关键字抓取太粗。最开始我只grepERROR结果漏掉了大量以其他措辞出现的错误比如工具自定义的缩写。后来把关键字表同步维护起来随着项目积累不断补充命中率才上来。这件事没有一劳永逸的做法关键字表是需要养的。第四个坑是状态记录和波形对不上。有一次我发现状态记录显示某个时刻有X但打开波形那个时刻看起来是正常的。查了半天原因是状态记录的时间戳用的是墙钟时间而波形用的是仿真时间两者混了。从那以后我把墙钟时间和仿真时间严格分两个字段再也没搞混过。这个坑不大但踩过一次就足够记一辈子。把这些做扎实之后后仿这个过程就不再是提交上去等结果的黑盒。你能随时知道它在哪、干过什么、为什么停这才是把机时真正用在了刀刃上。