调试 PETSc 程序这件事,我估计每个用数值库的人都经历过一段“瞎猜期”。报错信息看不懂、NaN 乱飘、并行跑着跑着就崩了,好不容易加了一堆 printf,重新编译又得等十分钟。后来我把 PETSc 的调试选项吃透了才发现,大多数问题根本不需要猜,它自己就把现场帮你留好了。这篇就系统梳理一下我平时最常用的 PETSc 调试选项,从编译期准备、运行时崩溃、调试器联动到日志分析,给出可以直接抄的实战方案。
2. 动手调试前的准备工作
2.1 带着调试信息编译:很多坑从这里开始
先说一个很多人踩过的最初级的坑:PETSc 默认的构建模式实际上是带调试信息的。也就是说,你直接用默认配置 ./configure 得到的库,编译时是带 -g -O0 的,碰到断言失败、越界访问时,报错堆栈是能看的。但有些人为了性能,configure 的时候喜欢加一句 --with-debugging=0,把优化打开。这时候如果程序崩了,你拿到的堆栈经常是符号错乱、行号漂移的,调试难度直接翻倍。
我个人的建议是:日常开发和排查问题,至少保留一套带调试信息的 PETSc 库。哪怕你最后跑性能测试要用 release 版,也建议单独建一个编译目录,比如 petsc-debug 和 petsc-opt,在 PETSC_DIR 和 PETSC_ARCH 之间切换。这样遇到诡异问题,先切回 debug 版复现一次,很多时候不用进调试器,PETSc 的 PetscError 就能把出错文件、行号、错误码说得明明白白。
如果你确实需要 release 版做性能验证,那也别忘了在 configure 时加上 --with-debugging=0,并且确认编译器开启了 -O2 之类的优化。但调试用的库千万别开优化,否则单步执行时变量被优化掉、断点打不上的情况会让人十分恼火。
提示:检查当前库是否是 debug 版,可以用
petsc_config_summary或直接跑一个测试程序,看它输出的配置字符串。如果库是 release 版,也不必重新编译整个 PETSc,你只需要把编译目录指到 debug 版即可,绝大多数应用代码不需要动。
2.2 认识 PETSc 的运行时选项:选项数据库的传递规则
PETSc 的调试选项绝大多数是运行时选项,也就是不用改代码、不用重新编译,启动命令时在可执行文件后面加参数就行。这套机制叫“选项数据库”(Options Database),每个 PETSc 程序启动时都会创建一个全局的数据库,把命令行参数按“前缀 + 选项名 + 值”的格式存进去。
比如:
bash复制./ex2 -ksp_monitor -ksp_rtol 1e-10
其中 -ksp_monitor 是布尔选项,表示打开 KSP 求解器的收敛历史打印;-ksp_rtol 1e-10 是带值选项,把相对残差容差设为 1e-10。PETSc 里各类对象(Vec、Mat、KSP、SNES、TS)都有一套自己的选项前缀,调试相关的选项多挂在 -start_in_debugger、-on_error_attach_debugger、-info、-log_view、-malloc_debug 这些全局名字下,不区分前缀。
掌握选项数据库的传递规则很重要:PETSc 的 PetscInitialize 会把命令行参数解析一遍,但你也可以在集合里用 PetscOptionsInsertFile 或者 PetscOptionsSetValue 在代码里注入选项。命令行优先级最高,代码内注入次之,PETSc 配置文件优先级最低。调试时我常用的是命令行临时加参数,不动代码,复现成本最低。
2.3 调试相关选项速查表
这里给出一张我常用的调试选项速查表,基本上遇到崩溃、NaN、内存问题时,第一时间从这些里面挑:
| 选项 | 作用 | 典型使用场景 |
|---|---|---|
-start_in_debugger |
程序启动时自动进入调试器 | 需要从头单步跟踪初始化流程 |
-start_in_debugger gdb |
指定用 gdb 启动 | Linux 下最常用 |
-on_error_attach_debugger |
出错时自动拉起调试器 | 崩溃时想看现场调用栈 |
-on_error_attach_debugger gdb |
出错时用 gdb 附加 | 配合 -start_in_debugger 二选一即可 |
-malloc_debug |
开启内存分配跟踪 | 定位越界写、重复释放 |
-malloc_dump |
退出时打印未释放内存 | 检查内存泄漏 |
-fp_trap |
捕获浮点异常(SIGFPE) | 定位 NaN/Inf 第一步出现的位置 |
-trap_for_signal |
捕获常见信号并停进调试器 | 段错误现场自动保留 |
-check_pointer |
检查指针合法性 | 排查野指针、空指针 |
-info |
输出详细内部调试信息 | 查看具体对象的底层操作 |
-log_view |
输出完整日志和性能报告 | 性能分析、确认阶段执行次数 |
-options_left |
打印未被识别的选项 | 检查选项名是否拼错 |
-help |
列出程序可识别的全部选项 | 快速确认某个选项是否生效 |
看到表里有些选项名你可能觉得眼熟,但实际用起来还是有不少细节。我下面逐个展开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
3. 让崩溃现场自动交到调试器手里
3.1 两个调试器启动选项的实战配置
调试 PETSc 程序最舒服的打开方式,不是等它崩了以后你再 gdb ./ex2 core,而是让它崩的那一刻自动把调试器拉起来。PETSc 提供了两个非常关键的选项:-start_in_debugger 和 -on_error_attach_debugger。
-start_in_debugger 是在程序一开始就进入调试器,适用于你想看初始化阶段的问题,比如矩阵创建、向量分配、选项解析这些。而 -on_error_attach_debugger 则是程序检测到错误时才把调试器拉起来,这在你排查“运行到一半才崩”的场景下更高效。两者可以分别用,也可以联合用,但一般情况下我建议先只用 -on_error_attach_debugger,避免每次启动都被打断浪费生命。
用法上,直接传参数即可:
bash复制./ex2 -on_error_attach_debugger gdb
程序一旦触发 SETERRQ 或 PetscError,会自动停在一个新的 gdb 会话里,这时你可以输入 bt 查看完整的调用栈。
有一个小坑必须提醒:如果你跑的是 MPI 并行程序,默认情况下每个进程都会尝试启动调试器,这会导致终端被多个调试器抢着用,非常乱。解决办法是加 -start_in_debugger_error 或配合 -debugger_nodes 之类的进程筛选,也可以直接手动指定只让 0 号进程进入调试器。后面并行调试那一节我再细讲。
3.2 调试器选型和加载脚本
PETSc 支持的调试器种类不少,常见的有 gdb、lldb、ddd、totalview。Linux 上我基本都用 gdb,macOS 上可以指定 lldb,比如:
bash复制./ex2 -on_error_attach_debugger lldb
但 PETSc 默认调用的调试器未必是你系统里装的那个,所以最好显式传一个名字。还有一点,PETSc 在启动调试器时会自动加载一个脚本文件,脚本里有它预先定义的打印函数,方便你查看 PETSc 对象的内容。这个脚本路径一般位于 PETSc 安装目录下的 lib/petsc/bin 或 conf 中,名字类似 gdbinit 或 petsc-gdb.py。
你可以把那些 PETSc 提供的 gdb 辅助命令手动加载到自己的 .gdbinit 里,这样在普通 gdb 调试场景下也能用 pvec、pmat 之类的命令。我之前一直以为只能靠 PETSc 自动拉起调试器时才能用这些命令,后来才发现只要 source $PETSC_DIR/lib/petsc/bin/petsc-gdb.py 就能在当前会话里直接激活。这一点对写单元测试、手动写小程序复现问题时特别方便。
提示:如果 gdb 提示找不到符号,先确认
PETSC_ARCH是不是指到了 debug 库,再用file ex2看二进制里是否有调试段。如果没符号,从编译期重新构建不可避免。
3.3 手动打断点:不用每次靠崩溃
自动附着调试器只是兜底手段,平时我更习惯手动打断点。PETSc 程序里经常需要关注的函数包括 KSPSolve、SNESSolve、MatMult、VecSetValues、PetscOptionsSetValue 等,在 gdb 里直接:
gdb复制break KSPSolve
run
它会停在函数的入口,然后你可以用 print 查看参数,用 step 进入函数。不过 PETSc 的函数内部套了很多层结构体和宏,直接看源码里的参数名经常会发现它们被层层包装。这时候我更推荐在调用端的“用户代码”打断点,也就是你的 main 函数里调 KSPSolve 的那一行,先看用户数据是否正确,再决定是否深入 PETSc 内部。
另外,打断点调试时建议同时打开 -info,这样 PETSc 会把你当前执行的顶层操作打出来,你可以把调试器里的位置和日志里的阶段对应起来。不用急着在 PETSc 内部函数里翻来翻去,很多时候问题出在你的矩阵或向量组装阶段,并不在求解器阶段。
4. 调试器里的 PETSc 操作
4.1 用辅助命令查看 Vec 和 Mat 内容
进入调试器后,最痛苦的事莫过于面对一个 Vec 或 Mat 结构体,不知道它里面是什么数据。PETSc 提供了几个辅助命令,我很推荐花十分钟熟悉一下:
在 gdb 里加载 PETSc 提供的 Python 扩展后,可以直接用:
gdb复制pvec v
pmat A
其中 v 是当前作用域里 Vec 类型的变量名,A 是 Mat 类型的变量名。pvec 会把向量的维度、长度、局部长度以及元素内容打印出来;pmat 会把矩阵的非零结构、非零元素数量以及数值打印出来。对于大规模矩阵,这样可能刷屏,但你可以先用 set print elements 20 限制打印数量。
如果没有加载 Python 扩展,也能用底层方式查:print *v 会显示 Vec_Seq 结构体,里面有 array 字段指向实际数据。然后你可以在 gdb 里直接看这块内存:
gdb复制print *v
print v->array[0]
不过这种方式对非 Seq 类型(比如 MPI 向量、CUDA 向量)就不太直观,所以我更建议优先配置好 Python 扩展。
4.2 查看 KSP 收敛历史和残差
调试线性求解器时,经常要确认迭代过程是否正常:残差是单调下降还是震荡?是否到了最大迭代次数?这些信息其实在运行时用 -ksp_monitor 就能打出来,但有时你在调试器里已经停在某个断点上,想临时看当前 KSP 的状态,也能用辅助命令:
gdb复制pksp ksp
如果你没加载辅助命令,也可以手动查看结构体,比如查看 ksp->its(迭代次数)、ksp->rnorm(当前残差范数)、ksp->reason(终止原因)。其中 reason 是枚举值,像 KSP_CONVERGED_RTOL、KSP_DIVERGED_ITS 这些,需要你对照头文件里的定义来理解。由于 PETSc 版本不同枚举值会变化,最稳妥的办法还是运行程序时加上 -ksp_view 和 -ksp_monitor,把收敛信息直接打印到标准输出,再配合调试器看具体变量。
4.3 在调试器里查看和修改选项数据库
还有一种调试技巧,就是直接在调试器里查询或修改 PETSc 的选项数据库。比如你运行了一个没有加 -ksp_max_it 的程序,但断点处想临时把最大迭代次数改小,就可以在 gdb 里调用 PETSc 的 API:
gdb复制call PetscOptionsSetValue(NULL, "-ksp_max_it", "50")
call KSPSetFromOptions(ksp)
这里 NULL 表示使用全局选项数据库;第二个参数是选项名,第三个是值。改完后再调用 KSPSetFromOptions,让 KSP 重新读取选项,可能还需要重新调用 KSPSetUp。这个手法很适合做“不改代码、不重新编译”的参数扫描,调试时特别有效。
另外,你可以用 call PetscOptionsGetAll(NULL, &n, &prefixes, &names, &values, ...) 之类的函数把当前数据库里的所有选项打出来,确认程序启动时究竟解析了哪些参数。我遇到过几次“选项没生效”的诡异情况,最后都是靠这招发现是拼写错误或者选项被某个 prefix 占用,根本没有进入预期对象的解析逻辑。
5. 日志、信息输出与内存崩溃排查
5.1 用 -log_view 看阶段耗时,反查异常
-log_view 是 PETSc 里一个很强大的诊断选项。它会在程序结束时输出一份日志摘要,包含每个事件(比如 MatMult、KSPSolve、SNESFunctionEval)的调用次数、总耗时、平均耗时、浮点运算次数、内存增减等。大部分人都拿它做性能热点分析,但调试时它同样有用。
比如你怀疑某个矩阵乘法在迭代过程中算了太多次,-log_view 里能看到具体次数。如果次数和你的预期不符,说明求解器配置有问题,比如没设收敛容差导致一直迭代到最大次数,或者每次重新组装矩阵时触发了一次 MatAssemblyBegin/End,多了不少同步开销。
-log_view 对并行程序还会按进程编号输出汇总和最大值、最小值,所以也能帮你发现负载严重不均衡的问题。不过注意,-log_view 会和调试器的输出混在一起,建议在调试时把日志重定向到文件:
bash复制./ex2 -log_view > log.txt 2>&1
然后再开调试器,避免终端信息被冲掉。
5.2 -info 与 -verbose:把内部操作可视化
-info 可能是 PETSc 里最啰嗦的输出选项。它会打印每个对象初始化、启动、销毁过程的详细信息,包括所有选项解析、对象类型选择、内存分配等。用它的典型场景是:你怀疑某个选项根本没进到对应对象的初始化逻辑里,或者某个对象被意外重复创建了。
不过 -info 全开时的输出量巨大,即使是小例子也足以刷屏几百行。我个人习惯是先跑一遍全量 -info,把输出存到文件里,再按关键词 grep,比如:
bash复制./ex2 -info > info.log 2>&1
grep -i "ksp" info.log | head -100
这样能找到 KSP 相关操作的执行记录。如果你用的 PETSc 版本较新,部分组件还支持 -verbose 级别更高的输出,可以和 -info 叠加使用。但这个选项对性能影响很大,只建议在调试小规模算例时开。
5.3 内存调试选项:越界、泄漏与野指针
PETSc 有自己的一套内存分配和跟踪机制。平时跑大规模算例时,它默认会用系统 malloc,但开启 -malloc_debug 后,PETSc 会对所有内部内存分配做边界检查、重复释放检测和泄漏追踪。这个选项对排查“莫名其妙段错误”和“内存越界写导致变量被改”非常有效。
做法很简单:
bash复制./ex2 -malloc_debug
如果程序里有越界写,PETSc 会在检测到问题时打印出错位置和分配信息。配合 -malloc_dump,程序退出时会列出所有尚未释放的内存分配点,并给出文件和行号,定位泄漏很快。
但这里要特别提醒一句:-malloc_debug 只是针对 PETSc 内部的内存管理函数(PetscMalloc、PetscNew 这类),你自己用裸 malloc、new 或 std::vector 分配的内存,它管不到。所以如果你怀疑是自己代码里的越界,建议配合 valgrind 或 AddressSanitizer 使用,比如:
bash复制./configure --with-debugging=1 COPTFLAGS="-O0 -fsanitize=address" FOPTFLAGS="-O0 -fsanitize=address" CXXOPTFLAGS="-O0 -fsanitize=address"
用 ASan 编译出的 PETSc 会比较慢,但定位越界问题非常准。我在矩阵组装阶段遇到过一次 std::vector 扩容后旧内存被写坏的问题,就是靠 ASan 第一发命中的。
5.4 捕获浮点异常:NaN 到底在哪一步产生的
NaN 或 Inf 是科学计算里让人极其头大的问题。它不会在产生的那一刻立刻报错,而是默默传播,直到后面的某个运算里才会显现异常。-fp_trap 就是专门用来在浮点异常(除零、无效操作、溢出等)发生时立即停住程序的选项。
bash复制./ex2 -fp_trap
开启后,只要 CPU 浮点单元里置了异常标志,PETSc 就会把错误报告出来并停进调试器,然后你可以 bt 看到发生异常的那一层函数调用。注意,CPU 的浮点异常默认是关闭的,所以如果没有 -fp_trap 的干预,很多非法浮点操作其实是在静默进行,等到结果被打印或进入分支判断时才暴露。
不过 -fp_trap 不一定能抓到所有情况,尤其是跨 MPI 通信后 NaN 在别的进程产生的情况。这时我常用 -info 配合分进程输出的方式去定位。还有一个笨办法,但确实有效:在用户代码里定期 PetscCheckFinite 或调用 PetscIsNan 检查向量、矩阵中的数据,二分法缩小问题范围。为了性能通常不开这个检查,但调试时绝对值得。
6. 常见问题与排查技巧
6.1 高频报错排查表
下面这张表是我在带学生和日常开发中遇到频率最高的几个报错/异常场景,按经验强烈建议收藏:
| 现象 | 首选排查选项 | 补充排查手段 |
|---|---|---|
| 程序启动即分段错误 | -start_in_debugger gdb |
-malloc_debug + valgrind |
| 运行到一半崩掉,无任何输出 | -on_error_attach_debugger gdb |
检查 MPI 进程数,逐个进程附加 |
| 求解结果全是 NaN | -fp_trap |
检查初始条件、边界条件、矩阵组装 |
| 结果数值飘移,不收敛 | -ksp_monitor -ksp_view |
检查预条件子、网格规模 |
| 内存持续增长 | -malloc_dump |
配合 -info 查对象重复创建 |
| 选项加了好几个都不生效 | -options_left |
PetscOptionsGetAll 打印数据库 |
| MPI 程序只有部分进程崩 | -on_error_attach_debugger gdb + 节点筛选 |
-info 分进程重定向 |
| 矩阵/向量内容诡异 | pvec / pmat |
-mat_view / -vec_view 直接输出 |
| 计算慢到不可接受 | -log_view |
阶段耗时对比 |
注意,-mat_view 和 -vec_view 不是严格意义的调试选项,但我在核查“矩阵到底组成了什么样子”时经常用,它们会把矩阵/向量的内容按行打印出来。小规模算例直接看输出,大规模算例可以配合 -mat_view binary 格式导出后写脚本分析。
6.2 并行程序调试的两点心得
并行程序的调试比串行麻烦一个数量级。第一原则是:先在 1 个进程下跑通,再逐步增加进程数。很多问题在 mpiexec -n 1 下根本不会出现,一旦超过 1 个进程就崩,这时候优先怀疑通信逻辑、邻接关系、局部数组边界这些和进程数强相关的部分。
第二原则是:当 MPI_Abort 或段错误只在某个进程出现时,不要指望 PETSc 自动拉起的调试器能直接成为你的救命稻草。你需要控制哪个进程进入调试器。PETSc 提供了类似 -debugger_nodes 0 这样的参数来限定节点,也可以直接设置环境变量:
bash复制mpiexec -n 4 ./ex2 -on_error_attach_debugger gdb
默认每个进程都会尝试弹调试器,非常混乱。我通常的做法是给每个进程单独分配一个终端或使用 xterm -e 启动调试器,但最省事的还是用 -start_in_debugger_error 配合只让 rank 0 停下,其余进程继续跑。这样虽然其他进程可能因为 rank 0 停住而阻塞在通信调用上,但至少你有一个明确的现场。
提示:并行调试时先把
-log_view关闭,它会让状态更复杂。真需要看日志,就为每个进程单独重定向到不同文件,比如用mpiexec -n 4 ./ex2 -info -info_file rank_%d.log这种形式(具体语法看 PETSc 版本),避免输出交错。
6.3 选项没生效?先查 -options_left
我见过太多“我明明加了选项却没用”的案例,包括我自己也踩过。PETSc 的选项数据库有两个常见“吞参数”的原因:一是选项名拼写错误,PETSc 遇到不能识别的前缀时不会立刻报错,只有在程序退出时通过 -options_left 才会提示;二是选项被对象特定的前缀屏蔽了,比如你给了 -ksp_max_it 100,但程序中创建 KSP 时指定了前缀 -foo_ksp_max_it 100,那么它只认带 foo_ 前缀的那个参数。
所以每次加完选项后,我几乎必加 -options_left 跑一遍:
bash复制./ex2 -ksp_max_it 100 -options_left
如果输出里有类似 "Variable: ksp_max_it not used" 的提示,那说明这个选项没被任何对象消费。接下来排查范围就缩小了:要么是拼写,要么是前缀不匹配,要么是对象根本还没被创建。这个方法比空猜高效得多。
6.4 一个小技巧:给关键调试状态写个脚本
调试其实是个体力活,如果每次启动都要输一大串参数,我很推荐把调试参数写进一个文件,用 PETSc 的 -options_file 引入:
bash复制./ex2 -options_file debug.opts
文件内容可以这样写:
text复制-on_error_attach_debugger gdb
-fp_trap
-malloc_debug
-ksp_monitor
-ksp_view
这样既能保证复现环境一致,又避免命令行里参数太多看花眼。更进一步,你甚至可以写多个 .opts 文件,比如 memdebug.opts、trapnan.opts、ksp_debug.opts,按问题类型快速切换。这个习惯帮我在面对不同算例时省了不少时间。
7. 关于调试,最后想多说几句
我在实际使用中发现,PETSc 调试选项的价值不只是“出错了看一眼堆栈”,更是帮你建立一种对数值软件运行状态的可观测感。选项数据库几乎把整个库的执行路径都暴露给了你,这不光是救命工具,也是理解 PETSc 内部机制的一条捷径。我见过不少朋友碰到问题第一反应是去翻源码,但很多时候用 -info 加 -log_view 把执行路径过一遍,问题出在哪一层已经非常清晰了。
还有一个小经验:调试选项要和版本配套。PETSc 迭代很快,3.x 各版本之间的选项名和行为会有些微差别。写这篇时我参考的是当前常见稳定版的行为,如果你用的版本较老或较新,跑 -help 永远是确认选项是否存在的最快途径。最后再分享一个小习惯:每次拿到一个新的 PETSc 环境,我先跑一个最小的 ex1 或 ex2,把 -help、-options_left、-log_view 的输出存下来,作为这个环境的基础档案。这样之后出现任何“怪现象”,先和你自己的基线对比,很多问题几分钟就能定位。
