记一次 QE 编译排障:从 FHC 收敛坑到 oneAPI/MKL 与 AOCL 优化
作为一个刚开始认真折腾第一性原理计算的新人,我原本以为编译 Quantum ESPRESSO 只是“把依赖装好,然后 cmake && make”这么简单。结果这一轮下来,我对 HPC 环境、数值库、编译器、运行时链接,以及“同样的版本号不代表同样的行为”这句话,有了非常痛的体会。
这篇文章记录一次完整的踩坑过程:我为什么重新在 CPU 集群上编译 QE,为什么一度被“同一个构型、同样版本的 QE,却在不同环境下一个能收敛一个不收敛”这件事气到,以及最后是如何把问题逐渐收束到 Libxc 的 FHC 选项、oneAPI + MKL 的优化路径,以及 AOCC 并不等于 AOCL 这几个关键点上的。
一、起因:2×9654 也不是万能的
我最开始的想法其实很朴素:手里明明有 AMD EPYC 9654 这样的 CPU 节点,跑一些 QE 单点构型重标注,应该不会太慢吧?
但实际测试下来,我发现我的 case 跑得并不理想。尤其是批量构型一多,单个 case 的耗时就很刺眼。于是我开始想:
- 既然 CPU 集群上已经装好了
Intel oneAPI和MKL; - 而我这批计算本质上又是 CPU 密集型;
- 那么是否应该在 Intel CPU 集群上重新编译一版 QE,走
oneAPI + MKL路线,看看能不能把性能和稳定性一起拉起来?
这个想法后来证明完全正确。只是我没想到,真正先把我绊倒的,不是性能,而是收敛性。
二、最让我气愤的一幕:同一个构型、同版本 QE,居然一个收敛一个不收敛
最开始我遇到的问题不是“算得慢”,而是“算不下来”。
症状很典型:某些构型在一个环境里可以继续往下跑,而在另一个环境里却报类似下面的错误:
| |
或者更直接一点,在第一步、前几步就崩掉。
最让我难受的是:QE 版本号明明一样。
我一度非常恼火。因为在一个 DFT 新手的直觉里,版本一样、输入一样、赝势一样,结果至少不应该差这么离谱。后来我才真正认识到,在 HPC 科学计算里,所谓“环境”从来都不只是一个版本号。它背后至少还包括:
- 编译器;
- MPI 实现;
- BLAS/LAPACK/FFT 后端;
- 外部库的版本与编译选项;
- 动态库还是静态库;
- 运行时究竟加载了哪一份库。
也正是在这一轮排查中,我第一次真正理解了:“软件版本相同”根本不等于“数值路径相同”。
三、第一轮排查:怀疑 MPI、怀疑脚本、怀疑链接,最后聚焦到 Libxc
起初我的排查路线比较朴素,主要看这些:
srun和mpirun的差异;pw.x究竟链接的是哪一套 MPI;LD_LIBRARY_PATH是否正确;libxc是否真的被启用;MKL是否被正确加载;- 运行时到底吃的是哪份
libxc。
其中一个重要发现是:不同节点上的 pw.x 二进制其实根本不是同一个构建结果。
虽然源码版本一致,但哈希值不同,依赖链也不同。换句话说,我之前以为自己在比较“同一程序在不同机器上的表现”,实际上是在比较“两套不同构建结果的 QE”。
进一步查下去,我开始逐渐把怀疑集中到 Libxc 上,尤其是对 SCAN 这类泛函的支持。
四、关键转折:FHC 才是那个真正影响“能不能算”的元凶之一
后来最关键的线索出现了:
我发现自己之前的 libxc 编译里,FHC 是开启的。
而在后续排查中,我注意到一个非常重要的事实:对于 SCAN 这类 meta-GGA 泛函,libxc 的 Fermi hole curvature(FHC) 选项可能直接影响数值稳定性。更具体地说,它不是“算得快不快”的问题,而是“同一个构型到底能不能稳定收敛”的问题。
这就解释了为什么我会遇到那么诡异的现象:
- 输入文件一样;
- 赝势一样;
- QE 版本号一样;
但某个构型在一套环境里会在前几步就报 too many bands are not converged,而在另一套环境里却能正常收敛。
后来当我重新编译一套 关闭 FHC 的 libxc 后,原本不稳定的构型居然就能继续稳稳往下跑,并最终完成收敛。这一刻我真的有点震撼: 一个编译选项,足以决定“同一个构型到底是算得下来,还是算不下来”。
也正因为这个经历,我现在对 libxc 的态度已经完全变了。以前总觉得它只是“QE 的一个依赖”,现在我会把它视为:
对某些泛函来说,
libxc的版本与编译方式,本身就是数值模型的一部分。
五、第二个坑:CMake 升太新,也会出问题
解决完 libxc/FHC 这条主线后,我又踩到了另一个很典型、但一开始并不显眼的坑:CMake 版本太新。
因为系统自带的 CMake 太旧,所以我当时一口气把 CMake 升到了 4.3.1。按直觉看,这似乎是“升级工具链、拥抱新版本”的标准操作。但实际情况是,新版 CMake 对一些旧模块、旧写法更严格,结果导致 QE 构建中的某些模块开始报兼容性问题。
最直接的影响包括:
- 内置 LAPACK 相关模块报错;
- MBD 相关模块出现兼容性问题;
- 某些旧语法需要额外策略兼容。
最后的处理思路不是回退一切,而是:
- 放弃 QE 内置数学库路线;
- 直接切到 oneAPI + MKL;
- FFT 路线指定为
Intel_DFTI; - 再通过追加
| |
让新版 CMake 对旧模块“网开一面”。
这一段很有代表性。它说明一件事: “新”不一定直接等于“更适合生产环境”。 在科学计算软件生态里,一个过新的构建工具,反而可能把原本就不够现代化的上层项目折腾出额外兼容性问题。
六、我最后采用的生产路线:oneAPI + MKL + no-FHC libxc
在 Intel CPU 集群上,我最终确定下来的主线是:
- 编译器:
mpiicx / mpiifx - MPI:Intel MPI
- BLAS/LAPACK:MKL
- FFT:
Intel_DFTI - Libxc:单独编译,关闭 FHC
- QE:CMake 构建
我的环境变量大致是:
| |
最终使用的 QE 配置命令核心部分是:
| |
而 libxc 这边,最关键的一条就是:
| |
这条看似不起眼,但在我的这批 SCAN 构型上,几乎就是决定生死的选项。
七、一个很容易误判的点:没报缺库,不代表你真的理解了链接方式
中间我还遇到一个非常容易让人误判的问题:
有时候 ldd pw.x 里看不到 libxc.so,但程序并不报缺库,而且照样能跑。
这让我一开始很困惑,以为是不是 libxc 根本没连上。后来才意识到,这里还要区分:
- 动态链接:
ldd能直接看到libxc.so - 静态链接:
ldd看不到,但构建和符号里实际上已经吃进去了
对于我这种刚开始折腾 DFT 环境的人来说,这又是一个很有教育意义的坑: “程序能跑”和“你知道它为什么能跑”是两件事。
科学计算环境里,真正麻烦的从来不是出错,而是那种“看起来能跑,但你并没有完全搞清楚依赖链”的状态。因为那会让后续复现和迁移都变得非常危险。
八、另一个重要认知:我其实只是用了 AOCC 编译器,并没有真正用到 AOCL
当 Intel 这套新环境跑顺之后,我又回头看了一眼自己之前在 AMD 9654 节点上的构建,结果发现了一个相当扎心的事实:
我之前只是用了 AOCC 编译 QE,但根本没有真正用 AOCL。
也就是说,我当时走的是:
- AOCC 编译器;
- 但 BLAS/LAPACK 用的是 internal;
- FFT 用的也是 internal。
难怪性能那么一般。
这一点后来让我印象特别深。因为在很多人(包括之前的我)的直觉里,会下意识地把:
- “用了 AMD 编译器”
- 和“用了 AMD 优化数学库”
这两件事混为一谈。
但实际上完全不是一回事。
AOCC 是编译器
AOCL 才是数学库加速栈
如果你只是用了 AOCC,却仍然把:
QE_BLAS_INTERNAL=ONQE_LAPACK_INTERNAL=ONQE_FFTW_VENDOR=Internal
写进 CMake 里,那本质上你并没有把 9654 这颗大 CPU 真正榨出来。
这也是我后来特别想写下来的原因之一。因为它真的很容易让人产生一种错觉:
“我明明已经在高端节点上编译了,为什么还是不快?”
答案可能并不是 CPU 不够强,而是你根本没有把正确的后端接上去。
九、结果:生产效率一下子进入了另一个量级
当 oneAPI + MKL + no-FHC 的 Intel 版本跑通后,我再回头看自己的批量任务,心态已经完全不一样了。
以前看着几千个构型,会本能地觉得:
- 这是不是又要跑很多天?
- 哪个节点上挂了怎么办?
- 收敛不稳会不会拖垮整批任务?
但当单个 24 原子构型在 4 核下不到两分钟就能算完时,量级立刻变了。 一旦批量并发开起来,原来觉得非常沉重的 DFT 重标注任务,突然就变成了一个可以认真规划、快速交付的工作流。
这一刻我真正体会到:
HPC 科学计算里的“调优”,从来不是锦上添花,而是决定你能不能把一件事从“理论可行”变成“工程上可执行”。
十、这次最大的收获,不是把 QE 编译成功,而是学会了如何看待“环境”
如果只是把结论写成一句话,那就是:
科学计算里,版本号只是入口,环境才是本体。
这次折腾之后,我对下面几件事的理解都比以前深得多:
1. libxc 不是无脑依赖
它的版本、编译方式,尤其是像 FHC 这样的选项,可能直接改变收敛行为。
2. 编译器不等于数学库
AOCC 不等于 AOCL;oneAPI 也不只是编译器,还包含了一整套数值后端生态。
3. “能跑”不够
真正的生产环境应该满足三件事:
- 能跑;
- 稳定;
- 快。
少任何一项,后面都要付出代价。
4. HPC 调优是科研生产力的一部分
有时候,手握 9654 这样的硬件“利器”,但没有把库链、FFT、BLAS、LAPACK、MPI、外部依赖配对好,那你其实并没有真正拥有它的性能。
结语:新手也能折腾,但最好别只靠运气
回头看,这整个过程其实非常像我刚接触第一性原理计算时的状态:
- 对软件栈的认识是碎片化的;
- 遇到错误时容易把问题归结为“玄学不收敛”;
- 对 HPC 环境里“编译方式”和“运行结果”之间的关系缺乏真正的直觉。
但也正是因为这一轮完整踩坑,我第一次认真意识到:
编译不是安装,部署不是复制,调优也不是可有可无的附加项。
尤其是在科学计算里,很多时候“为什么同样的 case 这个环境能算、那个环境不能算”,背后未必是什么高深的理论问题,而可能就是一个你之前根本不会在意的库选项。
所以如果你也在新环境里折腾 QE、CP2K、LAMMPS 这类软件,我想说的一点经验是:
- 不要只记“命令怎么敲”;
- 一定要顺手把构建参数、依赖版本、路径和踩过的坑写下来。
因为等你真的踩过一次之后就会发现, 最可怕的不是报错,而是你明明修好了,却不知道自己到底修好了什么。
而这,恐怕才是从“会运行程序”到“能掌控计算环境”之间,最重要的一步。