记一次 QE 编译排障:从 FHC 收敛坑到 oneAPI/MKL 与 AOCL 优化

记一次 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 的耗时就很刺眼。于是我开始想:

  1. 既然 CPU 集群上已经装好了 Intel oneAPIMKL
  2. 而我这批计算本质上又是 CPU 密集型;
  3. 那么是否应该在 Intel CPU 集群上重新编译一版 QE,走 oneAPI + MKL 路线,看看能不能把性能和稳定性一起拉起来?

这个想法后来证明完全正确。只是我没想到,真正先把我绊倒的,不是性能,而是收敛性


二、最让我气愤的一幕:同一个构型、同版本 QE,居然一个收敛一个不收敛

最开始我遇到的问题不是“算得慢”,而是“算不下来”。

症状很典型:某些构型在一个环境里可以继续往下跑,而在另一个环境里却报类似下面的错误:

1
2
Error in routine c_bands (1):
too many bands are not converged

或者更直接一点,在第一步、前几步就崩掉。

最让我难受的是:QE 版本号明明一样。

我一度非常恼火。因为在一个 DFT 新手的直觉里,版本一样、输入一样、赝势一样,结果至少不应该差这么离谱。后来我才真正认识到,在 HPC 科学计算里,所谓“环境”从来都不只是一个版本号。它背后至少还包括:

  • 编译器;
  • MPI 实现;
  • BLAS/LAPACK/FFT 后端;
  • 外部库的版本与编译选项;
  • 动态库还是静态库;
  • 运行时究竟加载了哪一份库。

也正是在这一轮排查中,我第一次真正理解了:“软件版本相同”根本不等于“数值路径相同”。


三、第一轮排查:怀疑 MPI、怀疑脚本、怀疑链接,最后聚焦到 Libxc

起初我的排查路线比较朴素,主要看这些:

  • srunmpirun 的差异;
  • pw.x 究竟链接的是哪一套 MPI;
  • LD_LIBRARY_PATH 是否正确;
  • libxc 是否真的被启用;
  • MKL 是否被正确加载;
  • 运行时到底吃的是哪份 libxc

其中一个重要发现是:不同节点上的 pw.x 二进制其实根本不是同一个构建结果。 虽然源码版本一致,但哈希值不同,依赖链也不同。换句话说,我之前以为自己在比较“同一程序在不同机器上的表现”,实际上是在比较“两套不同构建结果的 QE”。

进一步查下去,我开始逐渐把怀疑集中到 Libxc 上,尤其是对 SCAN 这类泛函的支持。


四、关键转折:FHC 才是那个真正影响“能不能算”的元凶之一

后来最关键的线索出现了: 我发现自己之前的 libxc 编译里,FHC 是开启的

而在后续排查中,我注意到一个非常重要的事实:对于 SCAN 这类 meta-GGA 泛函,libxcFermi 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 相关模块出现兼容性问题;
  • 某些旧语法需要额外策略兼容。

最后的处理思路不是回退一切,而是:

  1. 放弃 QE 内置数学库路线
  2. 直接切到 oneAPI + MKL
  3. FFT 路线指定为 Intel_DFTI
  4. 再通过追加
1
-DCMAKE_POLICY_VERSION_MINIMUM=3.5

让新版 CMake 对旧模块“网开一面”。

这一段很有代表性。它说明一件事: “新”不一定直接等于“更适合生产环境”。 在科学计算软件生态里,一个过新的构建工具,反而可能把原本就不够现代化的上层项目折腾出额外兼容性问题。


六、我最后采用的生产路线:oneAPI + MKL + no-FHC libxc

在 Intel CPU 集群上,我最终确定下来的主线是:

  • 编译器:mpiicx / mpiifx
  • MPI:Intel MPI
  • BLAS/LAPACK:MKL
  • FFT:Intel_DFTI
  • Libxc:单独编译,关闭 FHC
  • QE:CMake 构建

我的环境变量大致是:

1
2
3
4
5
6
7
8
9
# Libxc 相关路径
export LIBXC_SRC="/opt/libxc_src"
export LIBXC_BUILD="/opt/libxc_src/build"
export LIBXC_PREFIX="/opt/libxc/6.2.2-nofhc-ifx"

# Quantum ESPRESSO (QE) 相关路径
export QE_SRC="/opt/q-e-qe-7.3.1"
export QE_BUILD="/opt/q-e-qe-7.3.1/build"
export QE_PREFIX="/opt/qe-7.3.1-ifx-cmake-nofhc"

最终使用的 QE 配置命令核心部分是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
cmake "$QE_SRC" \
  -DCMAKE_C_COMPILER=mpiicx \
  -DCMAKE_Fortran_COMPILER=mpiifx \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_C_FLAGS="-O2" \
  -DCMAKE_Fortran_FLAGS="-O2" \
  -DQE_ENABLE_MPI=ON \
  -DQE_ENABLE_OPENMP=ON \
  -DQE_ENABLE_LIBXC=ON \
  -DQE_BLAS_INTERNAL=OFF \
  -DQE_LAPACK_INTERNAL=OFF \
  -DQE_FFTW_VENDOR=Intel_DFTI \
  -DCMAKE_PREFIX_PATH="$LIBXC_PREFIX" \
  -DLibxc_ROOT="$LIBXC_PREFIX" \
  -DCMAKE_INSTALL_PREFIX="$QE_PREFIX" \
  -DCMAKE_INSTALL_RPATH="$LIBXC_PREFIX/lib;/opt/intel/oneapi/mpi/2021.11/lib;/opt/intel/oneapi/compiler/2024.0/lib" \
  -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON \
  -DCMAKE_POLICY_VERSION_MINIMUM=3.5

libxc 这边,最关键的一条就是:

1
-DDISABLE_FHC=ON

这条看似不起眼,但在我的这批 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=ON
  • QE_LAPACK_INTERNAL=ON
  • QE_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 这类软件,我想说的一点经验是:

  • 不要只记“命令怎么敲”;
  • 一定要顺手把构建参数、依赖版本、路径和踩过的坑写下来

因为等你真的踩过一次之后就会发现, 最可怕的不是报错,而是你明明修好了,却不知道自己到底修好了什么。

而这,恐怕才是从“会运行程序”到“能掌控计算环境”之间,最重要的一步。

小葱的学术基站