ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

后仿仿真过程状态记录:日志、波形与检查点断点续跑

后仿仿真过程状态记录:日志、波形与检查点断点续跑 1. 后仿为什么必须做状态记录从一个跑挂的 case 说起后仿这东西做数字电路验证的同行都不陌生——把布局布线之后、带着真实寄生参数的网表再拉起来跑一遍仿真。前仿我们在 RTL 层面写代码、跑 case波形干干净净几秒钟出一个结果后仿完全是另一个世界网表是门级甚至晶体管级的动辄几百万上千万个实例一条 case 跑几个小时到几十个小时很正常中间还可能因为时序违例、X 态传播、工具吃光内存而中途挂掉。这时候“仿真过程状态记录”就不是锦上添花而是能不能把这活干下去的前提。你不知道它跑到哪一步挂的不知道挂之前电路处在什么状态不知道重跑要不要从头再来——这活基本就没法推进。我在做数字后端验证的这几年踩过最狠的一个坑一条后仿跑了三十多个小时眼看要进入关键场景结果仿真器被系统的 OOM 直接杀掉日志里只留下一句Killed波形文件因为缓冲区没刷盘只剩半截save又没提前做只能从头再来。那次之后我给自己立了个规矩——任何一条超过两小时的后仿先把状态记录这套东西搭好再开跑。标题里说的“后仿 仿真过程状态记录”听起来像两个独立的事实际上它们是绑死的后仿的场景决定了状态记录要做到什么粒度而状态记录的质量又直接决定后仿能不能按时交付。1.1 前仿和后仿差的不只是时间很多刚从 RTL 前仿转过来的人会低估后仿觉得“不就是换个网表再一遍吗”。真上手跑一次就知道差别是量级上的。RTL 仿真只有几千到几万个进程后仿是几百万门级实例每个实例都带延时信息事件调度密度完全不是一个数量级。前仿一次波形 dump 全信号也就几百 MB后仿哪怕只 dump 顶层端口波形也能轻松上几十 GB。再加上 SDF 反标带来的时序检查hold、setup 违例会不断触发警告甚至错误仿真速度会被进一步拖慢。正因为这种量级差异后仿的“失败模式”也跟前仿完全不同。前仿挂掉多半是代码写了$finish没走到或者断言直接报了个错后仿挂掉可能是仿真器分段内存爆掉、可能是某条路径出现无限循环的事件、可能是 X 态沿着未初始化的寄存器大面积扩散、也可能是波形写盘把磁盘写满了。这些失败往往发生在跑了很久之后如果没有过程状态记录你连复现都困难更别说定位。我的经验是后仿的调试成本90% 花在“重跑”上。把状态记录做扎实等于把每次重跑的时间省下来。1.2 状态记录到底该记哪几类信息“状态记录”这个词有点笼统我第一次跟新人解释的时候用的是“飞机的黑匣子”这个类比。黑匣子不是记一个数而是同时记飞行姿态、高度、速度、语音、告警。后仿的状态记录也是一组信息我的做法是分五类仿真进度当前跑到第几个 test、第几个 phase、仿真时间戳走到了多少纳秒用一个 heartbeat 日志每隔固定仿真时间或墙钟时间打一条。运行日志按 INFO/WARNING/ERROR/FATAL 分级每条带仿真时间和墙钟时间戳方便区分“是设计的问题”还是“是环境跑太久”。波形分阶段、分层次 dump不一定全量但关键窗口必须留且要保证异常中断时波形尽量完整。检查点checkpoint / snapshot在可控的时间点保存仿真器内部状态出事之后能从最近的检查点接着跑而不是从零开始。资源与宿主状态进程内存占用、磁盘剩余、CPU 负载的采样记录用于事后判断是不是资源问题导致的失败。这五类信息缺哪一类出事的时候都会让你难受。只有日志没有检查点你得从头重跑只有检查点没有资源采样你不知道为什么老在差不多的时间点挂只有波形没有进度日志你连波形对不对应用例都判断不了。1.3 哪些人最需要这套东西如果你只是偶尔跑一条小 case、十几分钟出结果那状态记录可以简单点日志加波形就够了。但下面这几类场景我强烈建议按完整方案来搭回归验证regression里跑后仿一次几十上百条 case夜里跑第二天看结果。没有进度日志你早上看到的只有一堆状态未知的目录。长时间场景 case启动、复位、低功耗模式切换、大流量突发这类需要跑到很深仿真时间的 case动辄十几小时起步。跨团队协作的后仿你产出的仿真结果要交给后端或者系统组复现日志和检查点就是证据链。资源紧张的机器上跑大仿真共享集群、内存吃紧更需要资源看护来预判 OOM。一句话总结只要一条后仿“跑挂了要心疼”状态记录就值得投入。而它投入的其实不多——半天搭一次后面每条 case 都能复用。2. 后仿环境与状态记录的整体设计思路搭状态记录之前得先把后仿环境本身理清楚因为状态记录是挂在仿真流程上的流程组织得乱记录也记不明白。我见过不少团队的目录结构是“一堆零散的脚本加一堆时间戳目录”跑完自己都找不到哪个日志对应哪条 case。所以在讲记录细节之前先讲整体设计思路这部分决定了后面能不能自动化、能不能复现。2.1 后仿输入的“四件套”和第一道自检后仿能跑起来依赖四样东西门级网表、标准单元库.db / .lib / .v 模型、寄生参数文件SPEF / DSPF、延时文件SDF。这四样里任何一个版本对不上后仿的结果就不可信。我的习惯是在仿真最开始做一次自检把每个文件的路径、大小、MD5 打印到日志头部。别小看这一步我曾经遇到过 SDF 版本拿错的情况——用的是上一轮布局布线的延时结果后仿时序怎么都对不上查了两天才发现是文件选错了。SDF 反标的方式有几种一种是编译期用工具选项指定另一种是在 testbench 里用系统任务显式标注initial begin $sdf_annotate(../../sdf/chip_top_max.sdf, tb_top.u_dut, , sdf_annotate_max.log, MAXIMUM); $sdf_annotate(../../sdf/chip_top_min.sdf, tb_top.u_dut, , sdf_annotate_min.log, MINIMUM); end这里我一般会把 SDF 标注的日志单独落到一个文件里因为标注过程中的 warning比如某个单元没匹配到、某个 path 被忽略往往是后仿问题的源头混在主日志里会被淹没。标注完还要看一眼标注成功率如果低于某个阈值比如 95%就要回头查库和网表是不是版本不匹配。提醒后仿的“四件套”一定要在流程里固定路径和命名规则不要靠人手动选。人一旦疲劳选错文件是迟早的事。2.2 把状态记录分成三层来设计我把状态记录按“时间尺度”分成三层这个思路是从运维那套监控体系借过来的用在后仿上意外地合适。第一层是秒级/分钟级的心跳层。用 testbench 里的一个独立进程或者仿真器的周期回调每隔固定仿真时间比如每 1us 仿真时间打一条心跳内容包含当前仿真时间、当前 test phase、已消耗墙钟时间、当前仿真器内存占用。这一层不追求信息量追求的是“能一眼看出跑到哪了”。第二层是事件级的日志层。设计的每个关键事件——复位释放、配置写入完成、开始收包、开始发包、错误注入、check 结果——都打一条结构化日志带上仿真时间和一个唯一标识。这一层是事后分析的主力。第三层是检查点层。在关键 phase 切换点或者每隔固定的仿真时间调用仿真器的快照机制保存状态。出事之后从最近的快照恢复把损失的时间控制在一个 phase 之内。三层是叠加关系缺一层都有短板。只有心跳没有事件日志出问题时知道“跑到哪”但不知道“发生了什么”只有事件日志没有检查点定位完了还得从头跑只有检查点没有日志你根本不知道该恢复到哪个点。2.3 工具选型别为了新特性牺牲可复现性现在主流仿真器都支持某种形式的检查和恢复机制。Questa/ModelSim 的save/restore、Xcelium 的快照功能、VCS 的相关能力各家叫法和命令不同具体用法建议以你手上版本的手册为准。我的选型建议只有一条优先选团队里大家都熟悉、且脚本化程度高的方案而不要为了某个新特性去赌可复现性。原因很实际后仿往往不是一个人跑而是团队共享一套脚本。如果只有你的工具版本支持某个检查点特性别人复现不了这套东西的价值就打了对折。我更偏向用“通用做法 工具无关的封装”来搭检查点则是能支持就用不支持就退化成“分 phase 断点重跑 日志”也能接受。具体到版本管理我会把仿真器的版本号、脚本的 commit、依赖的文件 MD5 都写进日志头部。后面出了结果对不上先比这几个字段八成的问题当场就能解释。3. 关键细节让状态记录真正能用的参数与写法思路讲完落到具体的参数和代码写法。这一部分是我踩坑最多的地方——很多参数看着不起眼设错了要么日志刷爆磁盘要么关键信息全丢。3.1 日志分级、时间戳、心跳日志分级这件事我强烈建议一开始就做对因为后面改起来要动所有打印点。我的分级标准是INFO正常进度包括 phase 进入退出、关键配置写入。WARNING可继续运行但需关注比如 SDF 标注部分未匹配、时序检查违例。ERROR预期之外但有恢复路径比如某次 check 失败但可以继续。FATAL致命直接决定这条 case 是不是废了。时间戳要同时记录仿真时间和系统时间。仿真时间用于和波形对齐系统时间用于判断“这段是不是卡住了”。我踩过一次坑只看仿真时间感觉每纳秒都在推进实际是因为某条路径产生了海量 delta cycle系统时间已经过去了几个小时。加了系统时间戳之后一眼就看出“仿真时间走得很慢是卡在某处了”。心跳日志的写法我喜欢用一个独立的 always 块或者 fork 出来的进程。用$time判断是否到了下一个心跳周期initial begin real last_hb; forever begin #(HEARTBEAT_INTERVAL); $fdisplay(hb_fd, [HB] simtime%0t wall%0t mem%0dMB phase%s, $time, $realtime, get_mem_mb(), cur_phase); $fflush(hb_fd); end end这里有个关键点心跳日志一定要$fflush。仿真器为了性能会把文件写入缓冲起来如果不 flush进程被杀时最后一批日志就丢了——而恰恰是“最后一批”包含了你最想看的信息。这个坑我遇到过一次日志到崩溃前十几分钟就断了就是没 flush。实操心得日志文件按 test 名和启动时间命名例如run_$(date %Y%m%d_%H%M%S)_$(TEST).log。事后找起来省事也方便脚本自动扫描失败。3.2 波形 dumpdump 多少比 dump 不 dump 更重要后仿波形是个很容易把磁盘吃干的东西。RGB 图省事在顶层一把$dumpvars(0, tb_top)跑两小时发现磁盘满了仿真跟着一起死。我的原则是分层 dump关键窗口优先非关键信号用触发式 dump。常用的做法是用 FSDB如果工具支持而不是 VCD因为 FSDB 压缩率高得多同样信号量能小一个数量级。逐渐打开的方式是先只 dump 顶层端口和关键状态机等仿真推进到需要细看的时间窗口时再$fsdbDumpvars打开更深的层次。也可以配合$dumpoff/$dumpon控制开关。initial begin $fsdbDumpfile(wave.fsdb); $fsdbDumpvars(0, tb_top, all); // 先只 dump 顶层 end task dump_deep_window(); $fsdbDumpvars(1, tb_top.u_dut); // 进入关键窗口再加深 $fsdbDumpvars(1, tb_top.u_dut.u_core); endtask波形文件还有个写盘的问题仿真器会周期性把缓冲刷到磁盘但如果崩溃太突然最后一段波形可能不完整。我的经验是把刷盘周期调短一点工具一般有对应选项代价是稍微慢一点但换来的是崩溃时波形尽量完整。这个取舍在跑长时间的 case 时非常值得。3.3 检查点与断点续跑的实现思路检查点的核心价值是“把重跑的时间从几十小时压缩到一个 phase”。各家仿真器的命令不同我用一套通用的思路描述具体命令查手册即可。思路是把一条长 case 拆成若干个 phase每个 phase 结束点做一次快照。快照包含仿真器的进程状态、内存里的信号值、已经反标的延时信息。出事之后从最近一次快照恢复重新执行后面的 phase。恢复的速度比从零编译加反标快得多尤其是编译阶段。以 Questa 系为例save/restore的基本形态是# 在仿真运行过程中do 文件里 run 1000 ns save phase1_ckpt run 1000 ns save phase2_ckpt # 出事之后重新进入仿真器 restore phase1_ckpt run -all这里有几个细节要特别注意。第一快照和延时的关系有些工具在 restore 后会重新做一次时序反标可能出现差异要在恢复后校验 SDF 标注日志和之前一致。第二快照文件往往不小几百 MB 到几 GB 都正常做好磁盘规划。第三不要在所有 phase 都存快照挑切换点存否则磁盘很快就满了。提醒恢复之后第一件事是打一条日志记录“从哪个快照恢复的、恢复时的仿真时间”否则你事后根本分不清某条日志是原始运行还是恢复后的运行。4. 实操搭一套能断点续跑的后仿流程前面都是原则和细节这一节把我实际用的流程组织方式摊开来讲。这套结构我用了两三年改了几次算比较稳定。4.1 目录结构和文件组织目录结构我按“输入 / 运行 / 输出”三分核心是运行目录按 case 隔离postsim/ ├── input/ # 只读输入 │ ├── netlist/ # 门级网表 │ ├── libs/ # 标准单元库 │ ├── spef/ # 寄生参数 │ └── sdf/ # 延时 ├── scripts/ # 脚本 │ ├── compile.sh │ ├── run.sh │ └── monitor.sh ├── runs/ # 每次运行一个目录 │ └── 20260101_1030_case_A/ │ ├── log/ │ ├── wave/ │ ├── ckpt/ │ └── run.info └── reports/关键约定有三条输入目录只读任何人不得在里面改东西每次运行的产物全部落在runs/时间戳_case下互不干扰run.info里记录这次运行用到的所有文件路径和 MD5作为复现凭据。这三条看着啰嗦但出过一次“两边日志对不上、结果是同一个目录被两个人同时跑”的事故之后你就会觉得很值。4.2 编译与运行脚本编译我单独拆一个脚本因为后仿编译很慢检查点恢复时不想重新编译。核心是固定工具选项和库路径#!/bin/bash set -euo pipefail NLinput/netlist/chip_top.v LIBSinput/libs/*.v SDFinput/sdf/chip_top_max.sdf vcs -full64 -sverilog v2k \ -debug_accessall \ -neg_tchk sdfverbose \ definePOSTSIM \ ${NL} ${LIBS} \ -l compile.log # 编译产物留在工作目录后续 run 复用运行脚本负责拉起仿真、记录环境、启动监控#!/bin/bash set -euo pipefail RUN_DIRruns/$(date %Y%m%d_%H%M%S)_${TEST} mkdir -p ${RUN_DIR}/{log,wave,ckpt} # 落一份运行信息作为复现凭据 { echo test${TEST} echo start$(date -Iseconds) echo host$(hostname) echo netlist_md5$(md5sum input/netlist/chip_top.v | awk {print $1}) echo sdf_md5$(md5sum input/sdf/*.sdf | awk {print $1}) } ${RUN_DIR}/run.info # 后台挂监控采样内存和磁盘 ./scripts/monitor.sh ${RUN_DIR} MON_PID$! # 跑仿真 ./simv -l ${RUN_DIR}/log/sim.log \ TEST${TEST} \ CKPT_DIR${RUN_DIR}/ckpt \ WAVE_DIR${RUN_DIR}/wave kill ${MON_PID} 2/dev/null || truerun.info里这几行看着不起眼但它是后面所有“为什么结果对不上”的排查起点。我甚至会把仿真器版本也加进去工具升级导致行为变化这种事也是会发生的。4.3 失败重试与资源看护监控脚本是我最推荐所有人都加的一环它可能就是几十行 shell但能救命#!/bin/bash RUN_DIR$1 LOG${RUN_DIR}/log/resource.log while true; do ts$(date -Iseconds) rss$(ps -o rss -p $SIM_PID 2/dev/null | awk {print $1/1024}) disk$(df -m ${RUN_DIR} | awk NR2{print $4}) echo ${ts} mem_MB${rss:-0} disk_free_MB${disk} ${LOG} sleep 30 done它每 30 秒采样一次内存和磁盘剩余。事后翻这份日志如果发现内存曲线一路往上冲到某个阈值就断那基本可以确定是 OOM如果磁盘剩余一路下降到底那就是波形或者日志把盘写满了。知道死因才知道怎么修。失败重试的逻辑我写得更保守不自动重跑而是自动生成“重跑命令”。因为后仿挂掉往往有真实原因无脑重跑可能掩盖设计缺陷。脚本根据是否有可用检查点输出从哪个快照恢复的命令人工确认后再执行。5. 常见问题与排查实录这一节是我这几年遇到的后仿高频问题按“症状—原因—排查—解决”的思路整理配合一份速查表。5.1 后仿发散、不收敛怎么查“后仿发散”这个词有点跨领域在模拟仿真里指数值迭代不收敛在数字后仿里我更常遇到的是“仿真时间推进极慢甚至停滞”。症状是心跳日志里仿真时间几乎不动但墙钟时间飞快流逝。常见原因有几个组合逻辑里存在振荡回路前仿被理想化掩盖了、某个异步路径没有做同步处理、SDF 反标后某些路径延时为负或者过大导致事件反复调度。排查思路我一般是先看心跳日志确认卡住的时间点再在那个时间点附近打开深度波形 dump看是否有信号在高频翻转。如果某个 net 在几个 delta 内反复翻转基本就是振荡。解决上先确认是不是 testbench 驱动有问题比如时钟和复位顺序不对再确认是否是网表里的组合环。后仿不像前仿会自动帮忙屏蔽一部分问题会裸露出来。// 一个简单的振荡检测断言帮助定位反复翻转 property p_no_glitch_fast(stable_sig); (stable_sig) $stable(stable_sig) throughout (1ns); endproperty5.2 后仿为什么跑出 X 态X 态传播是后仿的经典难题。很多从前仿迁移过来的 case前仿干干净净后仿一跑一片红。核心原因是前仿有很多“理想化处理”会在后仿消失未初始化的寄存器、未连接的输入端口、三态总线在反标延时下的竞争、以及把 X 当作“dont care”来优化的逻辑在后仿里会被真实地传播出来。排查上我一般先用xprop相关的配置让 X 的传播行为更贴近实际硬件不同工具的选项名不同VCS 有-xprop系列配置具体查手册。然后从 X 产生的源头往下游追常见源头有三类存储器未初始化、复位域没覆盖全、时钟切换时的毛刺。定位到源头后要么在 testbench 里补初始化要么在 RTL 层面加复位覆盖。前仿跑不出问题不代表设计没问题后仿把 X 暴露出来其实是帮你提前挡住了流片风险。实操心得给 X 态排查单独留一个波形文件只 dump 出现 X 的那几条路径的相关信号比在全量波形里大海捞针快得多。5.3 波形是红线、restore 失败这类坑“波形全是红线”基本就是 X 态的可视化表现和 5.2 是同一个问题的两个视角排查路径也一样。真正恶心的是“restore 失败”这类检查点问题。我遇到过的 restore 失败场景有这么几种快照对应的编译产物被删了检查点和编译结果要一起保留、恢复时的工具选项和保存时不一致比如 save 时开了某个 definerestore 时没开、以及快照文件本身写坏了多半是保存时正好赶上了磁盘写满或者进程被杀。对策其实不复杂快照和编译产物放在一起恢复命令里带上和原始运行完全一致的选项保存快照前先确认磁盘有足够余量。我还会在 save 之后立刻打一条日志记录快照文件的 MD5恢复时先校验一遍能省掉很多“为什么恢复后行为不一样”的困惑。5.4 常见问题速查表症状常见原因快速排查处理建议仿真时间推进极慢组合环振荡、异步路径未同步看心跳日志时间点加深度波形定位翻转 net修 testbench 或网表波形大面积红线X 态传播从 X 源头往下游追补初始化、覆盖复位域、用 xprop 配置仿真进程被杀内存 OOM看资源采样日志内存曲线减小 dump 范围、增大内存、加检查点日志到崩溃前断掉文件缓冲没刷对比最后一条日志时间关键日志加$fflush波形文件半截刷盘周期太长看波形最后时间点缩短刷盘周期、分窗口 dumprestore 后行为不同编译产物或选项不一致校验快照与编译的 MD5快照与编译产物同目录选项写进脚本SDF 标注警告多版本不匹配看标注日志匹配率核对网表、库、SDF 版本磁盘被写满波形或日志过大看磁盘剩余曲线分层 dump、日志轮转、及时清理这张表我打印出来贴在工位上新来的同事遇到问题先对一遍能自己解决大半。最后说一个我自己的体会后仿这件事本质上是在跟不确定性打交道。你不知道这条 case 会不会在某个诡异的时间点挂掉不知道挂了之后还能不能救回来。而状态记录就是用来把这种不确定性压到最低的那套工具。日志写细一点心跳刷勤一点检查点挑好时机存资源盯着看——这些动作单个看都很琐碎但合在一起就是把“一条后仿跑三十小时然后白跑”变成“一条后仿跑三十小时挂在哪、为什么挂、从哪接着跑”清清楚楚。现在我带新人第一课不是教他们怎么写 test而是先教他们把这套状态记录搭起来——因为后面所有的调试都建立在它之上。
返回列表