SpyGlass 内存超限:Out of Memory 与 OOM Killed 速查
更新时间:2026-10-23。FAQ 速查页,属于 EDA 工具排错系列。SpyGlass 跑大 SoC 的 lint_rtl 或 CDC goal 时,进程被撑爆是家常便饭:elaborate 阶段峰值内存随设计规模近乎线性上涨,CDC 跨域组合检查更是指数级胃口。这类故障的麻烦在于报错花样多但根因都是内存——有时是明白写着 Out of memory,有时是 C++ 层面的 std::bad_alloc,最坏的情况是进程凭空消失只留下 shell 一句 Killed。本篇与 SpyGlass 运行卡死/超时 分工:那篇是「不动」(swap 颠簸拖慢也算那边的前兆),这篇是「被撑爆」;与 SpyGlass 段错误崩溃 是近缘——OOM killer 杀进程和段错误在日志里都像「猝死」,判别方法本篇给出。
本文要回答的问题
- SpyGlass 没报错就没了,日志戛然而止,是谁杀的?
- Out of memory 是设计太大,还是 -jobs 开太多?
- 内存明明还有剩,为什么报 std::bad_alloc?
- 不换大内存机器,怎么把峰值压下来跑完?
一、报错原文与现象
Error: Out of memory. Cannot allocate 2147483648 bytes. (SG-11103)
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
Killed (shell 层一句话,SpyGlass 日志无任何报错)
bsub: JOB 9527 killed: MEMLIMIT 64 GB exceeded
malloc(): memory corruption (fast)先建立映射:Out of memory 与 std::bad_alloc 是进程自己申请内存失败(撞了 ulimit 或物理耗尽);Killed 无工具报错则是被内核 OOM killer 选中杀掉;MEMLIMIT exceeded 是 LSF 调度层的配额杀的;malloc(): memory corruption 多半是 32 位模式地址空间撞墙的变体。四种死法对应四条排查线,别混。
二、可能原因总览
| 报错现象 | 可能原因 | 处置 |
|---|---|---|
| Killed、日志戛然而止 | OOM killer 按峰值杀进程 | dmesg 确认,降峰值或换大机 |
| Out of memory(工具自报) | 峰值内存超物理 RAM 或 ulimit -v | time -v 看峰值,调限制或降压 |
| std::bad_alloc | 单次分配过大,常伴 32 位模式撞墙 | 确认 64 位模式,拆 goal 分跑 |
| MEMLIMIT exceeded | bsub 内存限额估小 | 按历史峰值重设 -M/-R |
| CDC/RDC goal 阶段爆内存 | 跨时钟域组合检查爆炸 | 加约束剪域,拆层次分跑 |
| lint_rtl elaborate 阶段爆 | 大 SoC 全量展平超 RAM | 层次化 lint 只跑 block 级 |
| 越跑越慢终至被杀 | 内存不足 swap 颠簸 | 降 -jobs,加内存 |
| 启动即失败或递归处死 | ulimit -s/-v 太小 | 调栈与虚拟内存上限 |
三、逐条排查展开
进程凭空消失,先问内核要口供。 Linux 内存耗尽时 OOM killer 挑最胖的进程杀,被杀者收不到任何错误码——SpyGlass 日志戛然而止,shell 只留一句 Killed。口供在内核日志里:
dmesg -T | grep -iE "out of memory|oom|killed process" | tail -5看到 Out of memory: Killed process xxxx (spyglass) 就实锤了。注意与段错误区分:段错误 shell 报 Segmentation fault 且 dmesg 里留的是 segfault at 记录而不是 oom 字样——判别细节见 SpyGlass 段错误崩溃,两条线处置完全不同。
峰值内存到底涨了多少,让 time 说话。 别凭感觉判断「机器不够」,用 /usr/bin/time -v 包一层重跑(小规模试跑即可),日志末尾的 Maximum resident set size 就是峰值 RSS:
/usr/bin/time -v spyglass -batch -project run.prj -goal lint/lint_rtl 2>&1 | tail -20拿到峰值后对照三样东西:机器物理 RAM、bsub 限额(有的话)、ulimit -v。谁最小谁就是瓶颈。一个经验值:lint_rtl 全量 elaborate 的峰值大致随设计实例数线性涨,百万门级 SoC 冲到几十 GB 不稀奇;CDC goal 在约束不全时比 lint 贵数倍。
-jobs 是每个 job 各吃一份内存。 SpyGlass 的 -jobs 8 不是 8 个线程共享内存,而是派生多个工作进程各自加载设计数据——内存近似按 job 数翻倍。内存紧张时第一刀就是降 -jobs:8 个 job 跑 2 小时被杀,不如 2 个 job 跑 6 小时跑完。同理,同机多作业互挤(别的同事的 run 也在同一台)会把总量撑爆,调度侧用 bsub -R "rusage[mem=64G]" 按内存预留,从源头消灭互挤型 OOM。
32 位与 64 位模式差异要确认。 老流程脚本里如果显式或默认走了 32 位模式,单进程地址空间上限约 4 GB,大设计必撞 std::bad_alloc 或 malloc 报错——哪怕机器有 128 GB 内存也救不了。检查启动日志里的模式字样,确认走 64 位;ulimit -a 顺手看 -s(栈)与 -v(虚拟内存)两项,深层 Tcl 递归或大型规则树展开会先撞栈上限,官方环境一般要求 ulimit -s unlimited。bsub 作业继承提交时的 ulimit,要改就写进提交封装脚本。
CDC/RDC 组合爆炸按「剪域」治。 CDC goal 内存爆炸的典型原因是时钟/复位约束不全:工具把未约束的 clock 当异步处理,跨域路径组合数指数膨胀。处置顺序是先把 SDC/SGDC 约束补齐(缺的时钟关系用 cdc_false_path 类约束剪掉),再把 goal 拆开分跑:cdc/cdc_setup_check 与 cdc/cdc_verify 分开,必要时按层次拆 block。约束补齐往往一步把峰值砍掉一大半,比换机器便宜得多。
「以前能跑、最近开始爆」先对比再加资源。 同一设计历史上跑得动、最近开始 OOM,变量出在输入侧:设计规模涨了、约束改了(比如删了一批 cdc_false_path)、goal 从 lint 加到了 CDC、或者工具升了版本(新版本 2025.06 之后部分 goal 默认检查项变多)。把上次成功 run 的日志翻出来,对比各阶段的内存报告行——峰值涨在哪一阶段,压力就源于哪一阶段。盲目换大内存机器可能掩盖一个本该被发现的约束回归,对比先行是纪律。
全量展平吃不下就走层次化。 lint_rtl 对大 SoC 全量 elaborate 是峰值最猛的场景。层次化 lint 流程只跑 block 级、顶层用抽象模型(abstract view)拼装,峰值内存按最大 block 算而不是全芯片——这是大项目存活的基本功,别等全芯片跑不动再返工拆。goal 本身也可以拆:lint/lint_rtl 与 lint/lint_turbo 分跑、不同 rule group 分批,失败重跑也只重跑失败那一段。
SpyGlass内存报错 -> 按报错字样分流
按报错字样分流 -> Killed无工具报错: dmesg查OOM口供
按报错字样分流 -> Out of memory自报: time -v量峰值RSS
按报错字样分流 -> bad_alloc报错: 确认64位与ulimit
按报错字样分流 -> MEMLIMIT被杀: 按历史峰值重设限额
dmesg查OOM口供 -> 单job峰值超RAM: 降jobs并拆goal分跑
dmesg查OOM口供 -> 多作业互挤: bsub按内存预留
time -v量峰值RSS -> elaborate阶段爆: 层次化lint只跑block
time -v量峰值RSS -> CDC阶段爆: 补约束剪域后分跑
确认64位与ulimit -> 栈上限不足: 封装脚本设ulimit
降jobs并拆goal分跑 -> 峰值档案防复发
bsub按内存预留 -> 峰值档案防复发
层次化lint只跑block -> 峰值档案防复发
补约束剪域后分跑 -> 峰值档案防复发
按历史峰值重设限额 -> 峰值档案防复发
栈上限不足: 封装脚本设ulimit -> 峰值档案防复发
SpyGlass内存报错.style: { fill: "#FDECEC"; stroke: "#D9534F" }
按报错字样分流.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
Killed无工具报错.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
Out of memory自报.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
bad_alloc报错.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
MEMLIMIT被杀.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
dmesg查OOM口供.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
time -v量峰值RSS.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
确认64位与ulimit.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
单job峰值超RAM.style: { fill: "#FDECEC"; stroke: "#D9534F" }
多作业互挤.style: { fill: "#FDECEC"; stroke: "#D9534F" }
elaborate阶段爆.style: { fill: "#FDECEC"; stroke: "#D9534F" }
CDC阶段爆.style: { fill: "#FDECEC"; stroke: "#D9534F" }
降jobs并拆goal分跑.style: { fill: "#FEF3E2"; stroke: "#E08A1E" }
bsub按内存预留.style: { fill: "#FEF3E2"; stroke: "#E08A1E" }
层次化lint只跑block.style: { fill: "#FEF3E2"; stroke: "#E08A1E" }
补约束剪域后分跑.style: { fill: "#FEF3E2"; stroke: "#E08A1E" }
按历史峰值重设限额.style: { fill: "#FEF3E2"; stroke: "#E08A1E" }
栈上限不足: 封装脚本设ulimit.style: { fill: "#FEF3E2"; stroke: "#E08A1E" }
峰值档案防复发.style: { fill: "#F1ECFD"; stroke: "#7A5BD4" }颜色按语义分角色:红是报错入口与命中的根因,蓝是排查动作,橙是修复,紫是收尾防复发。主线是「报错字样直接决定第一查」——Killed 找 dmesg、自报 OOM 找 time -v、bad_alloc 先确认 64 位与 ulimit、MEMLIMIT 找历史峰值;修复线收敛到「降 jobs/拆 goal → 补约束/层次化 → 调度预留」三级递进,能压则压、压不住才拆块。
四、预防与纪律
- 每个项目建峰值档案:用 time -v 记录各 goal 的峰值 RSS,bsub 限额按峰值 1.3 倍配置,lint 与 CDC 分开建档。
- -jobs 按机器内存反推上限:单 job 峰值乘以 job 数留 20% 余量,不许拍脑袋开 16。
- 约束先行:CDC goal 跑前必须过约束完整性检查,剪域是性价比最高的内存药。
- ulimit 写进作业封装:
-s unlimited、64 位模式在提交脚本里固化,不依赖交互环境。 - 大设计从立项就走层次化 lint:block 级签核加顶层 abstract,别等全芯片跑爆再拆。
这一篇在知识体系里的位置
本篇是 EDA 排错速查簇中 SpyGlass「资源配额」节点的 FAQ 页。同簇互参:进程「不动」类见 SpyGlass 运行卡死/超时(swap 颠簸的前兆在那边);进程「猝死」的另一条线见 SpyGlass 段错误崩溃;同为内存主题的另一工具对照见 PrimeTime 资源/内存超限;SpyGlass 流程全景见 SpyGlass RTL Lint 使用指南。
一句话总结
SpyGlass 内存超限按报错字样分流:Killed 查 dmesg 的 OOM 口供、自报 Out of memory 用 time -v 量峰值、bad_alloc 先确认 64 位与 ulimit、MEMLIMIT 按历史峰值重设;修复按「降 jobs 拆 goal → 补约束剪域/层次化 lint → 调度按内存预留」三级递进,长期靠峰值档案与约束完整性把内存问题从救火变规划。