首页 / 资讯中心 / 文章详情

MPI并行SCD模拟软件在Ubuntu与CentOS上的跨平台编译实战

MPI并行SCD模拟软件在Ubuntu与CentOS上的跨平台编译实战 ★ FEATURED ARTICLE
这也许就是并行随机团簇动力学模拟软件的常态昨天还在自己的 Ubuntu 工作站上编得欢今天把同一份源码丢到学校集群的 CentOS 节点上立刻被各种头文件、链接库、MPI 路径折腾得没脾气。随机团簇动力学Stochastic Cluster DynamicsSCD本身不是新方法它把材料辐照损伤过程中产生的空位、间隙原子以及它们形成的团簇当作一个个“物种”用耦合速率方程描述浓度随时间演化时间尺度能从纳秒直接推到秒级别这是分子动力学做不到的。因为方程数量动辄上千个物种计算矩阵反复求逆所以并行几乎成了标配。这篇文章我就把自己在 Ubuntu 和 CentOS 两套系统上编译运行一套 MPI 并行 SCD 模拟软件的完整过程写清楚给后面接手的老哥省点时间。如果你刚进组要跑模拟或者正想把老代码迁移到新机器上看这一篇基本能少走一半弯路。1. 为什么要跟两套发行版死磕1.1 SCD 模拟到底是干什么的为什么绕不开并行SCD 把材料里的缺陷按尺寸分类空位团簇 V1、V2、V3……间隙原子团簇 I1、I2、I3……每一类都对应一个浓度变量方程里既有团簇之间互相吞并的反应项又有来自辐照产生的源项还有热激活导致的溶解项。求解的时候通常会从这些方程推导出产率矩阵然后做时间推进。尺寸一旦到几千物种数量就是几千矩阵规模按平方涨单进程跑几步就开始冒汗。更麻烦的是系统存在明显的刚性吸收、发射、复合这些过程的速率可能差很多个数量级所以一般要用隐式格式推进隐式格式意味着每个时间步都要解一次大型稀疏或稠密线性方程组。这种计算结构天然适合按尺寸区间做领域分解把物种分成几段每个 MPI 进程管一段进程之间只需要交换相邻尺寸区间的通量。我手头编译的这套程序正好就是这种设计所以必须用 MPI 编译器来编译直接裸调 gfortran 是编不过去的。1.2 高校和课题组里最典型的“双系统分裂”我自己遇到的场景大概是多数人都会碰到的办公室工作站装了 Ubuntu 22.04图形界面、开发环境、调试工具都很顺手适合做小规模算例和代码调试但真正跑产量级模拟的机器是学校机房的老 CentOS 7它不追求新只求稳定而且机房管理员不喜欢给系统装太多额外软件包。这就逼着我必须保证同一份源码在两个发行版上都能编译。CentOS 7 默认仓库里的编译器还停在 GCC 4.8.5Ubuntu 上默认 GCC 可能已经 13、14 了这种版本差就是源码编译最大的变数来源。1.3 我到底想要得到什么动手之前先想清楚终点我需要一个编译好的 scd_mpi 可执行文件、一个能改的输入文件模板、一份可重复执行的构建说明。如果只是在一台机器上碰巧跑通那不叫编译成功叫运气好。两个发行版都跑通才能排除源码里的隐式平台依赖比如某个头文件只有新版 glibc 才有或者 Makefile 里写死了某个绝对路径。下面所有内容都围绕这条主线展开。2. 环境准备编译器、MPI、数值库一次装齐2.1 Ubuntu 侧的命令和理由在 Ubuntu 上我的习惯是先建一个干净实验环境别带着一堆旧版本库去碰运气。基础安装命令是sudo apt update sudo apt install -y build-essential gfortran libopenmpi-dev openmpi-bin liblapack-dev libblas-dev这里每个包都不是白装的。build-essential 提供 gcc、g、makeSCD 源码里混着 C 和 Fortran两者都缺一不可。gfortran 是 Fortran 编译器因为老牌数值模拟代码十有七八是 Fortran 写的。libopenmpi-dev 提供 MPI 的头文件和动态库openmpi-bin 提供 mpirun、mpicc、mpif90 这些运行时工具。liblapack-dev 和 libblas-dev 是数值线性代数库程序里解线性方程组要用。有人会问如果源码里没显式调用 LAPACK 函数是不是可以不装别省这个事很多间接依赖绕来绕去最后还是落在 BLAS 上等到链接报错再回头补反而更浪费时间。2.2 CentOS 侧的命令和那个著名的大坑CentOS 7 上装依赖要比 Ubuntu 麻烦一点。第一件事是加 EPEL 软件源因为很多数值库和开发包不在默认仓库里。完整命令是这样sudo yum install -y epel-release sudo yum groupinstall -y Development Tools sudo yum install -y gcc-gfortran openmpi openmpi-devel lapack-devel blas-devel装完以后有个特别容易踩的坑CentOS 的 OpenMPI 装完并不会自动把 mpif90 加进 PATH它被放在/usr/lib64/openmpi/bin下面。第一次编译时我直接敲 mpif90结果 shell 回一句command not found当时还以为是安装没成功。后来才明白要做两件事一是把/usr/lib64/openmpi/bin加进 PATH二是把/usr/lib64/openmpi/lib加进 LD_LIBRARY_PATH。不更新第二个编译虽然可能过但运行的时候会报找不到libmpi.so。这两个环境变量我建议直接写进~/.bashrc免得每次开新终端又重新 export。2.3 为什么 MPI 实现要统一选 OpenMPI 还是 MPICH很多老代码对 MPI 实现不挑标准接口都能编译但链接库名有区别。OpenMPI 的主库叫libmpi.soMPICH 的主库是libmpich.soMakefile 里写的是-lmpi还是-lmpich决定了你装错之后那些莫名其妙的undefined reference。我一开始在 CentOS 上顺手装了 MPICH结果 Makefile 里写的是-lmpi链接阶段报了几十个未定义函数查了半天才发现是库配对问题。后来统一改用 OpenMPI两边的编译参数一致麻烦少一大半。如果你所在集群管理员只提供了 MPICH 的 module那也别硬来把 Makefile 里的-lmpi改成-lmpich即可但改完一定要重新make clean否则旧的目标文件残留会导致更诡异的错误。2.4 三步验证环境是否真正就绪装完依赖我习惯先做三个验证缺一个都别急着编译。第一步gfortran --version看编译器版本第二步which mpirun mpirun --version看 MPI 是否在 PATH第三步也是最关键的执行mpif90 -show它会老老实实告诉你 mpif90 到底调用了哪个底层编译器、自动加了哪些 include 和库路径。在 Ubuntu 上mpif90 -show输出里通常已经带了/usr/lib/x86_64-linux-gnu/openmpi/include这样的目录在 CentOS 上可能只是裸的 gfortran 调用这基本预示着你后面编译会遇到mpif.h找不到的报错。提前用这一步摸清底细比等 make 报错再查高效得多。3. 源码配置和编译Makefile 才是真正的灵魂3.1 拿到源码包后的第一件事不是敲 make我见过太多人解压源码以后直接make报错再上网搜索。老练一点的流程是先ls -la看有没有 README、INSTALL、Makefile再cat README。这种长期维护的 SCD 代码README 里通常会写清楚依赖的最低版本、推荐编译方式、以及一个冒烟测试输入文件。我还会顺手看看 Makefile 里有没有写死FC gfortran。如果写死了并行代码基本不可能编过因为 MPI 的模块和头文件路径只有通过 mpif90 这类包装器才能自动注入裸 gfortran 根本不知道 MPI 头文件在哪。3.2 Makefile 关键变量逐个拆解我手头这套 SCD 源码的 Makefile 最终被改成这样FC mpif90 CC mpicc CFLAGS -O2 -g -Wall FFLAGS -O2 -g -Wall -fopenmp LDFLAGS -llapack -lblas -lm OBJ scd_main.o scd_rules.o scd_cluster.o scd_io.o EXE scd_mpi all: $(EXE) $(EXE): $(OBJ) $(FC) $(FFLAGS) -o $ $(OBJ) $(LDFLAGS) %.o: %.f90 $(FC) $(FFLAGS) -c $ -o $ clean: rm -f *.o *.mod $(EXE)这里最核心的就是FC mpif90。有些朋友会自作聪明把FC留着 gfortran然后手动加-I/usr/lib/x86_64-linux-gnu/openmpi/include这也不是不行但一旦程序里 use 了 MPI module还牵涉到 mpi_f08 等等复杂接口手动路径分分钟漏项。直接交给 mpif90所有头文件和库路径它内部帮你处理。-fopenmp要不要开得看代码是否真的用了 OpenMP。如果源码里实际没写!$omp parallel加不加其实无影响如果写了那就必须加否则运行时会忽略所有并行区性能直接变回串行。3.3 双平台差异处理别再只盯着系统环境变量Ubuntu 和 CentOS 最大的区别就在库路径上。Ubuntu 的 BLAS/LAPACK 通常在/usr/lib/x86_64-linux-gnuldconfig 默认索引好了CentOS 的 OpenMPI 在/usr/lib64/openmpi这套路径经常不在默认搜索列表。我最后在 Makefile 头部维护了两套变量通过注释切换# Ubuntu 22.04 一般不需要额外指定 # MPI_INC : # MPI_LIB : # CentOS 7 / RHEL 7 需要显式指定 MPI_INC : -I/usr/lib64/openmpi/include MPI_LIB : -L/usr/lib64/openmpi/lib然后把它们插进 FFLAGS 和 LDFLAGSFFLAGS : -O2 -g $(MPI_INC) LDFLAGS : $(MPI_LIB) -llapack -lblas -lm这样做的意义是把平台差异收敛到 Makefile 内部而不是靠每个开发者手动改自己的环境变量。团队协作时只要大家统一用这个 Makefile不管谁在哪个发行版上跑都能走通。如果你用的操作系统更老比如 CentOS 6 或者某些定制 RHEL可能连mpif90都没有而只有mpif77那就得把 FC 改成mpif77并核对源码里 Fortran 语法版本这种情况我后面会再提一嘴。3.4 编译现场实录从报错到跑通我记录一下第一次在 CentOS 7 上编译时最典型的一连串错误。执行make -j4之后控制台输出mpif90 -O2 -g -c scd_main.f90 scd_main.f90:2: Error: Cant open included file mpif.h我当时第一反应是源码不完整去网上找 mpif.h 下载折腾了半天。后来用mpif90 -show一看才发现CentOS 的包装器并没有自动带上 include 路径。解决办法就是我在 3.3 里写的显式加MPI_INC : -I/usr/lib64/openmpi/include报错立刻消失。接着又遇到链接阶段报错undefined reference to sgemm_ undefined reference to dgesv_这说明 LAPACK 和 BLAS 没有正确链接。当时的 Makefile 里 LDFLAGS 只写了-lm没有-llapack -lblas。补上之后还有一个容易忽略的点库必须写在目标文件后面。如果命令写成mpif90 -o scd_mpi -llapack -lblas scd_main.o ...有些老版本链接器会忽略库里的解析照样报 undefined reference。正确顺序是把 .o 文件放前面库放最后。最后一步是编译成功后的运行验证。我用ldd ./scd_mpi看过依赖发现libmpi.so.12显示not found。那时候才明白CentOS 上 OpenMPI 的运行库路径没有被 ldconfig 索引需要在~/.bashrc里加export LD_LIBRARY_PATH/usr/lib64/openmpi/lib:$LD_LIBRARY_PATH加上之后ldd输出干干净净程序终于能跑了。4. 并行运行、输入参数与结果验证4.1 输入文件里到底要写什么每个参数为什么那么重要一个典型的 SCD 输入文件长这样# SCD input for Fe-Cr dilute alloy under irradiation temperature 873.0 # K production_rate 1.0e-6 # dpa/s initial_vacancy 0.0 # dimensionless fraction initial_interstitial 0.0 max_cluster_size 5000 t_max 1.0e4 # seconds dt_factor 0.1 output_freq 500 seed 42这些参数我一个一个说一下背后的逻辑。temperature 决定缺陷迁移和发射的速率常数Arrhenius 关系里温度是指数项差个 50 度结果可能就是数量级差异。production_rate 是辐照产额单位常用 dpa/s表示每秒每个原子被撞击离开格点的概率这个量直接给方程提供源项。initial_vacancy 和 initial_interstitial 是初始浓度通常在淬火或辐照初始时刻给一个偏离平衡态的值。max_cluster_size 直接决定方程组规模5000 就是 10000 个物种空位和间隙原子各半如果再翻倍到 10000内存和计算时间都会暴涨。t_max 是模拟总时长SCD 的价值就在于能推到普通方法无法到的宏观时间。dt_factor 是自适应步长控制参数它越小时间步越短结果越稳但计算量越大我一般先跑小规模试算看稳定性再根据振荡情况调。seed 是随机数种子并行时每个进程会有自己的随机流但种子是全局的这个参数对结果可复现性非常重要。4.2 mpirun 的参数选择并行度别一拍脑袋定编译完成后运行命令很简单mpirun -np 4 ./scd_mpi input.dat log.out 21但我每次都会先看一眼 log.out。如果程序支持按尺寸分区正常输出应该类似rank 0: managing cluster size 1 .. 1250 rank 1: managing cluster size 1251 .. 2500 rank 2: managing cluster size 2501 .. 3750 rank 3: managing cluster size 3751 .. 5000如果看到这种输出说明按尺寸分区成功MPI 通信没有出问题。并行度怎么选我的经验是先看 max_cluster_size 有多大每个进程分到的尺寸区间如果低于 10通信成本会超过计算收益那就不如用更少进程跑。反过来5000 个尺寸用 8 个进程通常能接近 6 倍加速再多就要看机器线程数和内存带宽了。还有一个重要禁忌如果 Makefile 里开了-fopenmp但代码本身没有真正的并行区别在 mpirun 里面顺手设一堆线程。我习惯运行之前先执行export OMP_NUM_THREADS1防止某些隐式 OpenMP 运行时库自动创建线程池白白增加几百毫秒的开销。4.3 判断跑得对不对拿串行结果当尺子程序跑完以后不要急着去看物理结果先确认数值本身是对的。我用两个办法。第一个办法是小规模对照把 max_cluster_size 改成 200然后用 1 个进程跑一次再分别用 2 个、4 个进程跑同一组输入最后diff三个输出文件。SCD 如果用的是确定性算法相同输入相同随机种子输出应该逐位一致或者至少在浮点误差范围内一致。如果出现明显分歧多半是进程内通信有问题比如某个数组没有做 MPI 归约或者边界尺寸重复计数了。第二个办法是看守恒性辐照缺陷总量减去复合掉的量应该等于当前缺陷总量。SCD 方程本身满足质量守恒如果 log 里出现总量明显涨跌的警告那就要回头检查方程组装部分。4.4 一个简单的加速比测试我拿 max_cluster_size5000 的算例做过一次小规模加速比测试。单进程大概耗时 320 秒2 进程 180 秒4 进程 98 秒8 进程 63 秒。这个数据看起来没有理想线性加速但已经算正常。瓶颈主要在 MPI 进程之间的边界通量通信以及每轮矩阵求解前各进程需要做一次全局归约归约的耗时随着进程数增长是亚线性的。如果哪天发现 8 进程比 4 进程还慢那基本不是算法问题而是进程绑定没有做好。我给几个可操作的排查方向检查是不是超线程检查是不是把 MPI 进程全塞在了同一个物理核上检查是不是 OpenMPI 默认用了共享内存但内存带宽不够。使用mpirun --map-by core --bind-to core可以强制把进程绑定到独立核心很多时候这一句就能把加速比拉回来。5. 我实际踩过的坑报错大赏与排查方法5.1 头文件“失踪”类问题这一类是新手遇到最多的。列个速查表症状可能原因解决动作Cant open included file mpif.hMPI include 路径没被编译器自动发现在 Makefile 里显式加-I/usr/lib64/openmpi/include或-I/usr/lib/x86_64-linux-gnu/openmpi/includempif90: command not found包装器不在 PATH 里export PATH/usr/lib64/openmpi/bin:$PATH或使用module load openmpimpicc: No such file or directory只装了运行时没装 devel 包Ubuntu 装 libopenmpi-devCentOS 装 openmpi-develError: Type mismatch between actual argument and dummy argumentMPI 版本太新旧代码用老接口尝试加-stdlegacy或者换用 F90 标准接口我特别想提醒一点不要在互联网上随便搜一个 mpif.h 下载来放到源码目录里这样虽然可能骗过编译器但运行时会因为库版本匹配问题崩溃。正确的处理永远是让编译器通过正确的 MPI 加载路径找到属于同一版本的头文件。5.2 动态库加载失败ldd 永远是好工具编译过了但运行时报error while loading shared libraries: libmpi.so.12: cannot open shared object file这个问题我在 Ubuntu 上几乎没碰到过在 CentOS 上碰到过至少三次。原因就是 CentOS 的 OpenMPI 安装目录没有进 ldconfig 索引。排查方法很简单ldd ./scd_mpi | grep not found一目了然。解决办法是在/etc/ld.so.conf.d/下新建一个 openmpi.conf写入/usr/lib64/openmpi/lib然后执行sudo ldconfig。如果你没有 root 权限就把 LD_LIBRARY_PATH 写进自己的 ~/.bashrc。实测在 CentOS 7 上OpenMPI 的库版本可能是 libmpi.so.12而 Ubuntu 22.04 上则可能是 libmpi.so.40两个平台的版本号相差很大所以千万不要把一个平台编译好的二进制直接拷到另一个平台跑即使拷贝了也要重新ldd确认。5.3 MPI 版本冲突导致的“幽灵”死锁还有一种情况在黑盒排查里很容易让人抓狂编译正常单进程运行正常但mpirun -np 2一执行就卡住不报错也不继续。我踩过一次那台机器的主机名在 /etc/hosts 里没有正确解析到回环地址MPI 初始化的进程间握手一直等不到对方。排查思路是先用mpirun --mca btl tcp,self强制限定 Bootstrap 走 TCP 回环如果这样就通了说明问题出在网络接口上。再打开另一个终端用hostname看主机名然后编辑 /etc/hosts 把主机名指向 127.0.0.1。集群环境里还有可能是防火墙挡住了 OpenMPI 默认用到的 SSH 端口段这种情况下可以设置OMPI_MCA_plm_rsh_agent指向一个不需要密码的 ssh 或者直接用 Slurm 启动。5.4 尾行细节Makefile 里的并行和你以为的并行最后是一个容易忽略的常识make -j4只是编译器任务的并行不直接反映程序本身的 MPI 并行度。有些人编译时发现-j4报了错就开始怀疑代码写得有问题其实这往往是目标文件依赖关系没有在 Makefile 里写全多个编译任务同时生成同一个 .mod 文件导致冲突。遇到这种报错先make clean然后老老实实用make -j1串行编译一遍基本都能过。这种问题在 Fortran 项目中尤其常见因为 .mod 文件是编译器生成给其他源文件 include 的并行编译时如果模块 A 还没生成完依赖它的 B 已经开编了就会报找不到模块。知道这个原理之后我就再没被这种“并发编译问题”吓到过。6. 把重复劳动固化下来以及一点经验沉淀6.1 一份跨发行版的自动构建脚本编译这事干过一次之后就不想再手敲第二次。我写了一个build.sh逻辑很简单判断发行版类型分别装依赖然后统一进入 make 阶段#!/bin/bash set -e if [ -f /etc/debian_version ]; then sudo apt update sudo apt install -y build-essential gfortran libopenmpi-dev openmpi-bin liblapack-dev libblas-dev MPI_INC MPI_LIB elif grep -qi centos\|rhel /etc/os-release; then sudo yum install -y epel-release sudo yum groupinstall -y Development Tools sudo yum install -y gcc-gfortran openmpi openmpi-devel lapack-devel blas-devel export PATH/usr/lib64/openmpi/bin:$PATH export LD_LIBRARY_PATH/usr/lib64/openmpi/lib:$LD_LIBRARY_PATH MPI_INC-I/usr/lib64/openmpi/include MPI_LIB-L/usr/lib64/openmpi/lib else echo Unsupported OS, configure manually. exit 1 fi make clean || true make FCmpif90 CCmpicc \ FFLAGS-O2 -g $MPI_INC \ LDFLAGS$MPI_LIB -llapack -lblas -lm这份脚本不是万能的但对于绝大多数干净的 Ubuntu 和 CentOS 环境已经能完成从装依赖到出二进制的全部流程。如果你用的是集群强烈建议不要用 root 跑脚本因为大部分集群禁止 root 执行 mpirun另外集群上安装依赖需要管理员权限脚本里的 sudo 部分要视情况手动完成。6.2 Slurm 作业脚本的正确姿势集群上最终跑模拟时用 Slurm 提交作业是常态。我的作业脚本模板是#!/bin/bash #SBATCH --job-namescd_test #SBATCH --nodes1 #SBATCH --ntasks4 #SBATCH --cpus-per-task1 #SBATCH --time00:30:00 #SBATCH --partitioncompute module purge module load gcc openmpi export OMP_NUM_THREADS1 mpirun -np 4 ./scd_mpi input.dat这里--cpus-per-task1不是随便写的它保证每个 MPI 进程只占用一个 CPU 核心避免和 OpenMP 线程抢资源。module purge也很重要如果不 purge 掉系统默认模块可能莫名其妙继承到一套不同版本的 Intel MPI和源码用 OpenMPI 编译出来的 ABI 对不上运行报错的形态很隐蔽。提交之后可以用squeue -u 用户名看任务状态跑完在 slurm-作业号.out 里看输出。6.3 过完这一遍之后我的三点体会首先别小看“读 README”这件事。老代码的维护者往往把最关键的编译前提写在第一页很多人跳过这段直接踩坑反而多花几个小时。其次动态库路径和 MPI 包装器路径要区分开一个影响编译一个影响运行实际中经常有人把前者的配置错误带到了后者的排查里越查越偏。最后不建议为了追求省事而把编译好的二进制跨发行版直接复制除非你能保证依赖库版本完全一致否则老老实实到目标机器上重新编译顺便验证一遍整个构建流程才是正道。如果你接下来要在这个基础上做更复杂的事我个人建议把容器化也纳入规划。把 Ubuntu 或 CentOS 的环境、依赖和编译产物都写进一套容器定义文件后续在任意 Linux 主机上都能直接拉起一次性构建再也不用来回折腾系统的差异。当然这是后话先把眼前这套 SCD 软件在两个发行版上编利索了后面的扩展自然就顺了。
阅读完成 · 觉得有帮助?
咨询建站