接到新节点验收任务的第一天,我登上去第一件事就是查MPI环境,结果干干净净,连个mpirun都没有。这批机器要在一周内跑出基准数据,用户点名要HPCG的成绩。HPCG不是普通跑分工具,它测的是真实应用里最常见的稀疏迭代求解模式,比HPL那种密集矩阵更能反映实际负载特征。但前提是得先把MPICH配好,再把HPCG编译通过、跨节点跑起来。这一套流程看着简单,实际每一步都有坑:版本选不对、configure选项填错、ssh免密没配齐、Makefile模板选错,甚至hpcg.dat里一行数字写错都能让整个测试白等一小时。
这篇文章就是完整的实操记录。我会把从MPICH源码编译到HPCG最终跑出分数的全过程走一遍,说明每个关键选择的理由,并把容易翻车的地方单独列出来。不管你是临时接手集群的运维,还是刚学MPI想给自己搭环境的学生,照着这份记录走一遍,基本能绕开我踩过的那几个大坑。
1. 为什么选 MPICH + HPCG 这个组合
1.1 MPICH 在 MPI 实现里的定位
MPI 的主流实现就这么几个:MPICH、OpenMPI、Intel MPI、MVAPICH。我这次选 MPICH,不是因为 OpenMPI 不好,而是因为 MPICH 的兼容性和部署形态更适合裸机验收场景。
MPICH 是整个 MPI 生态里最传统的实现,Fortran 和 C 绑定支持非常稳,很多科学计算软件在configure阶段会优先探测mpich,尤其是那些老牌的分子动力学、气象模式代码。OpenMPI 同样优秀,但它默认的进程管理器和运行参数更多,出了问题排查链路反而长。Intel MPI 性能确实好,但绑定 Intel 编译器生态,对 GCC 用户不够友好。MVAPICH 则是 MPICH 的 InfiniBand 优化分支,适合专用高性能网络,普通千兆环境用不上。
MPICH 还有一个好处是构建过程透明。你自己指定 prefix,自己选编译选项,装完之后的库路径、头文件路径全在掌握之中。这对后续排查 MPI 程序"链接了错误的库"这类问题非常有帮助。
1.2 HPCG 测的不是理论峰值,是真实手感
HPL 测的是稠密矩阵 LU 分解,这种操作计算密度极高,内存访问很规整,所以能把 CPU 的浮点单元压得很满,跑出来的分数能够接近理论峰值。HPCG 完全不同。它求解的稀疏线性方程组来自 27 点差分模板,每一步迭代都要做稀疏矩阵乘、预条件子操作和多次全局归约。这些操作的访存模式极其散乱,而且频繁的全局通信让 MPI 环境的好坏直接暴露出来。
打个比方:HPL 像是测一辆跑车在笔直赛道上的极速,HPCG 则是测它在闹市区走走停停的真实通勤速度。绝大多数科学计算应用恰恰是后一种模式,所以 HPCG 的分数非常贴近实际应用体感。
现在 Top500 排名也要求同时提供 HPL 和 HPCG 两套成绩,就是因为 HPL 太容易"刷"到理论峰值,而 HPCG 想刷上去难得多。
1.3 用 HPCG 做环境验收的几个理由
这次验收的任务不只是跑个分,还要验证整个 MPI 环境是否健康。HPCG 是个天然的测试集:
- 编译快,一个 xhpcg 可执行文件几分钟就出来,不依赖庞大的第三方库。
- 对 MPI 部署的完整性极其敏感。任何一个节点 ssh 不通、任何一台机器的 MPICH 库路径不对、任何一条网络链路异常,跑分要么直接失败,要么断崖式下跌。
- 规模弹性大。单节点可以跑 64^3 的小规模验证,多节点可以跑 512^3 以上的大规模测试,能覆盖从功能验证到性能压测的全过程。
后面几章就按这个思路展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MPICH 源码编译:别在版本和配置选项上翻车
2.1 装之前先把家底盘清楚
编译 MPICH 之前,我习惯先把机器的软硬件情况摸一遍。原因很简单:CPU 核数决定后续 hostfile 里 slots 怎么写,内存大小决定 HPCG 能跑多大的题,编译器版本决定 MPICH 能启用哪些优化。
bash复制cat /etc/os-release
uname -r
lscpu | grep -E "Socket|Core|Thread|CPU\(s\)"
gcc --version
free -h
这套命令在每台参与计算的节点上都要执行一遍。别偷懒只查管理节点,我曾遇到过登录节点是 16 核,计算节点是 64 核的情况,按登录节点核数写 hostfile,跑起来直接有一半计算核心闲置。还有一次发现某节点 GCC 版本还是 4.8,编译 MPICH 时会因为 C++ 标准支持不足而失败。
编译器版本如果太老,建议先升级 GCC。MPICH 4.x 对 C++14/C++17 有依赖,GCC 8 以下容易在 configure 阶段报一些莫名其妙的错误。实际经验是 GCC 9 以上比较稳妥。
2.2 版本怎么挑
很多教程会让你 apt install mpich 或者 yum install mpich,这种安装方式省事,但我建议源码编译。系统源里的 MPICH 版本往往滞后,比如 Ubuntu 20.04 默认源里是 3.3.x,而目前 4.x 已经稳定很多年,ch4 网络层的共享内存和通信优化差距很大。
我去官网选的是 MPICH 4.3.1 稳定版。下载解压:
bash复制wget https://www.mpich.org/static/downloads/4.3.1/mpich-4.3.1.tar.gz
tar xzf mpich-4.3.1.tar.gz
cd mpich-4.3.1
如果有条件,下载后顺手做一下 sha256 校验,毕竟这是要部署到集群所有节点的基础软件,安全习惯不能丢。
2.3 configure 选项怎么取舍
MPICH 的 configure 选项里有两个比较容易纠结的点。
第一个是 --disable-fortran。如果这台机器以后不跑 Fortran 写的 MPI 程序,建议禁用,能省掉 gfortran 编译绑定的一堆麻烦。但如果你的主要应用是 WRF、CESM 这类 Fortran 老牌代码,就不要禁用,否则后续应用编译时找不到 mpif90,又得重编一遍 MPICH。HPCG 是纯 C++,不需要 Fortran 绑定,我这次直接禁用。
第二个是网络设备层。MPICH 4.x 默认使用 ch4 设备,底层会自动检测共享内存、TCP 等通信方式。如果你没有 InfiniBand,就老老实实不要加 --with-device=ch4:ofi,否则 configure 阶段会要求系统里有 libfabric,裸机上经常没有装,导致配置失败。默认 auto 就够了。
我的完整编译命令:
bash复制./configure --prefix=/opt/mpich \
--disable-fortran \
CFLAGS="-O3" CXXFLAGS="-O3"
make -j$(nproc)
sudo make install
CFLAGS="-O3" 让 MPI 库本身也开启优化,MPICH 默认编译级别偏保守,跑 HPCG 这种性能基准测试时,库本身的优化级别会影响最终成绩。
2.4 环境变量与安装验证
安装完成后,把 MPICH 的 bin 和 lib 路径写进系统环境变量,最好写到 /etc/profile.d/mpich.sh,这样所有节点、所有用户登录后都能直接用:
bash复制export PATH=/opt/mpich/bin:$PATH
export LD_LIBRARY_PATH=/opt/mpich/lib:$LD_LIBRARY_PATH
验证安装是否正常:
bash复制which mpiexec
mpiexec --version
mpiexec -n 2 hostname
最后一条命令会在本机拉起两个进程,分别输出主机名。如果能正常输出两次主机名,说明单机 MPI 通信已经通了。
2.5 系统里已存在 OpenMPI 时怎么办
这是最常见的坑。很多 Linux 发行版自带 OpenMPI,二进制路径是 /usr/bin/mpirun。MPICH 装好之后,如果 PATH 里 /opt/mpich/bin 排在 /usr/bin 前面,那 mpiexec 用的是 MPICH 的,而某些应用编译脚本里会写死 /usr/bin/mpirun,结果就是运行时和编译时用了两套 MPI,轻则报环境变量冲突,重则程序直接崩溃。
我的习惯是检查一下 which mpiexec,确认指向的是 /opt/mpich/bin/mpiexec。如果发现不对,要么调整 PATH 顺序,要么直接卸载系统自带的 OpenMPI 包。
3. 跨节点跑 MPI:hostfile、SSH 免密与通信层排查
3.1 hydra 启动器依赖的三件事
MPICH 的进程管理由 hydra 完成,它默认通过 SSH 在远程节点上启动进程。这也意味着跨节点运行 MPI 程序之前,有三件事必须搞定,缺一不可。
第一是 SSH 免密登录。hydra 启动远端进程时不会交互式输密码,必须在所有节点间建立免密通道:
bash复制ssh-keygen -t rsa -b 4096
ssh-copy-id user@node01
ssh-copy-id user@node02
第二是主机名解析。hostfile 里如果写主机名,就必须保证每台机器都能通过 /etc/hosts 或 DNS 解析到其他机器。只写 IP 也可以,但有些 MPI 程序会校验节点名,反而多一层麻烦,不如统一配好 /etc/hosts:
bash复制192.168.1.11 node01
192.168.1.12 node02
192.168.1.13 node03
192.168.1.14 node04
第三是可执行文件和库的路径一致。最省心的方式是 NFS 共享 /opt/mpich 和工作目录。如果集群没有共享存储,那就用 rsync 把所有节点上的 /opt/mpich 同步一遍。注意 LD_LIBRARY_PATH 也要同步写进每台机器的环境变量,否则远程节点上即使有二进制,也会因为找不到 libmpi.so 而失败。
3.2 hostfile 与 slot 怎么写
hostfile 的格式很简单,每行一个节点,slots 后面写该节点最多允许启动多少个进程:
code复制node01 slots=16
node02 slots=16
node03 slots=16
node04 slots=16
slots 的值我建议按物理核数来写,不是逻辑线程数。因为 HPCG 这类计算密集程序,超线程不但不提升性能,还会因为进程抢同一物理核的资源反而变慢。如果每台机器核数不同,就分别写,比如:
code复制node01 slots=64
node02 slots=32
node03 slots=32
一个很容易忽略的细节:hostfile 每行末尾不要有多余空格或隐藏的 Windows 换行符,否则 hydra 解析时可能把节点名理解成带空格的错误地址,报出让人摸不着头脑的连接失败。我建议用 cat -A hosts 检查一下非可见字符。
3.3 跑通跨节点的第一道检查
配置好之后,先不要直接跑 HPCG,用最简单的方式验证跨节点进程拉起:
bash复制mpiexec -f hosts -n 64 hostname
这条命令会在 4 个节点上各启动 16 个进程,最后打印出 64 行主机名。如果每行主机名都是 node01 到 node04 循环出现,说明跨节点拉起成功。如果报错,对照下面的排查表:
| 报错信息 | 问题根源 | 解决办法 |
|---|---|---|
| Host key verification failed | 目标节点不在 known_hosts 里 | 用 ssh-keyscan 提前写入 |
| Permission denied, please try again | SSH 免密没配好 | 重新执行 ssh-copy-id |
| mpiexec: command not found | 远程节点环境变量没配 | 把 PATH 写入远程节点 ~/.bashrc |
| Connection timed out | 网络不通或防火墙拦截 | 检查 ping 和防火墙状态 |
| getaddrinfo failed | 主机名解析失败 | 检查 /etc/hosts |
3.4 通信层默认选择的坑
MPICH 4.x 默认会在同一节点内部走共享内存,跨节点走 TCP。对于千兆以太网环境,这个默认行为不需要改。但如果集群有 InfiniBand 或 RoCE 高速网络,TCP 通信会浪费掉 RDMA 的低延迟优势,HPCG 的多节点扩展性会明显变差。
这种情况需要在编译 MPICH 时显式启用 ofi 设备,并安装 libfabric:
bash复制./configure --prefix=/opt/mpich \
--disable-fortran \
--with-device=ch4:ofi \
--with-libfabric=/path/to/libfabric
普通验收场景没有高速网络就跳过这一步,默认 TCP 已经能跑通,只是多节点性能数字不会太好看。
4. HPCG 获取与编译:坑最密集的一段
4.1 下载并确认版本
HPCG 的下载页面目前有两个阵营:3.1.x 和 4.0。4.0 是后来推出的更新版本,支持 OpenMP 混合模式,也包含了一些针对常见 CPU 的优化实现。新部署建议直接上 4.0,因为它的代码结构和 Makefile 模板对现代编译器更友好。
我这次用的是 HPCG 4.0。下载解压之后,目录结构里有几个关键东西:setup.py、若干 Makefile.* 模板文件,以及 src 源码目录。真正需要关心的只有两个问题:用哪个 Makefile 模板,以及怎么指定 MPI 编译环境。
4.2 Makefile 模板选择
先看看源码目录里有哪些模板:
bash复制ls Makefile.*
常见的有 Makefile.SKL_mpich、Makefile.X99_intel、Makefile.POWER9 等。这些模板对应不同的 CPU 微架构和 MPI 实现。如果机器是 Intel Skylake 以后的架构,且用 MPICH,Makefile.SKL_mpich 是很理想的选择。
没有完全匹配的模板时,不要慌,复制一个最接近的然后手动改。要改的核心变量就这么几个:
makefile复制MPI_DIR = /opt/mpich
MPI_CXX = $(MPI_DIR)/bin/mpicxx
CXXFLAGS = -O3 -march=native
LDFLAGS = -lm
MPI_CXX 必须指向 MPICH 自带的 mpicxx 包装编译器,不能用裸的 g++。CXXFLAGS 里的 -march=native 会让编译器根据当前机器的 CPU 指令集做本地优化,这是 HPCG 跑分里性价比最高的优化选项。如果目标集群里有新旧不同的 CPU,-march=native 可能导致节点间指令集不兼容,这种情况下改用 -O3 -march=x86-64-v3 这类更保守的设置。
4.3 用 setup.py 还是直接 make
HPCG 提供了交互式配置脚本:
bash复制python3 setup.py
按提示选择编译器类型、MPI 路径、优化级别,脚本会生成对应的 Makefile。这种方式对新手比较友好,但交互过程有点啰嗦,而且如果脚本里预设的选项和你系统实际情况不符,反而更容易误导。我更推荐直接复制模板然后手动 make:
bash复制cp Makefile.SKL_mpich Makefile
make
编译成功后,可执行文件在:
bash复制./bin/xhpcg
4.4 常见编译错误速查表
HPCG 编译阶段的问题,90% 集中在前三步,下面是我整理的错误对应关系:
| 报错信息 | 问题根源 | 解决办法 |
|---|---|---|
| mpicxx: command not found | PATH 里没有 /opt/mpich/bin | 先 source 环境变量文件 |
| fatal error: mpi.h: No such file or directory | 用了裸 g++ 而不是 mpicxx | 检查 Makefile 中 MPI_CXX |
| undefined reference to PMPI_* | 编译和链接用的不是同一套 MPI 包装器 | 先 make clean 再重编 |
| cannot find -lmpi | 链接器找不到 libmpi | LDFLAGS 加 -L/opt/mpich/lib |
| OpenMP 相关报错 | OpenMP 线程库缺失或编译器不支持 | 加上 -fopenmp 并确认 GCC 支持 |
4.5 编译产物自检
得到 xhpcg 可执行文件之后,先在当前目录运行一次小规模自检,不要带任何参数:
bash复制./xhpcg
它会在当前目录生成默认的 hpcg.dat,然后跑一个 64^3 的小问题。如果几秒后能看到 Final HPCG result 类似的行,说明编译产物正常,可以进入下一阶段。
5. 算多大的题:内存估算与 hpcg.dat 参数设计
5.1 理论内存账
HPCG 的稀疏矩阵来自三维 27 点差分格式,每个未知量大约有 27 个非零元素。求解过程中还要维护多组 double 向量,所以总内存需求粗略按每个未知量 500 字节估算比较靠谱。实际运行中还需要给 MPI 缓冲和系统留余量,按 0.6KB 到 0.7KB 每点做规划更保险。
N 的计算公式是 nx * ny * nz。几个常见规模的估算:
| nx,ny,nz | 总未知量 N | 估算内存 |
|---|---|---|
| 64 | 262144 | 约 130 MB |
| 128 | 2097152 | 约 1 GB |
| 256 | 16777216 | 约 8 GB |
| 512 | 134217728 | 约 64 GB |
这个表可以直接作为选规模的参考。之前我在 64GB 内存的节点上直接跑了 512^3,结果内存被撑爆,进程直接被 OOM Killer 杀掉,日志里什么都来不及输出。后来老老实实按内存先算一遍再开跑。
5.2 进程网格划分的整除约束
这是 HPCG 部署里最容易翻车、也最容易被教程忽略的细节。
HPCG 会把所有 MPI 进程组织成二维进程网格,比如 16 个进程可以排成 4x4,64 个进程可以排成 8x8。约束条件有两个:
npx * npy = 总进程数nx必须能被npx整除,ny必须能被npy整除
如果 nx 不能被 npx 整除,HPCG 不会立刻报错,而是会在程序输出里打出一大段状态信息,然后计算过程变得极其缓慢甚至直接崩溃。最稳的做法是:先把进程数分解成二维网格,再用网格维度去反推 nx 和 ny,并且让它们都取 2 的幂。
比如 64 个进程排成 8x8,nx=512, ny=512,512 能被 8 整除,完全没问题。又比如 128 个进程排成 8x16,nx=640, ny=640,640 能被 8 和 16 整除,也没问题。如果拿不准,就优先让 nx 和 ny 相等,并且保持 2 的幂。
5.3 hpcg.dat 怎么写
进入运行目录,创建一个 hpcg.dat 文件:
text复制HPCG benchmark input file
University of Tennessee
256 256 256
60
0
前两行是描述文本,程序会原样读取跳过。第三行是 nx ny nz。第四行是运行时间,单位秒。第五行的 0 代表使用默认的布局方式,保持 0 就行。
运行时间这个参数要单独强调一下。本地联调阶段设 30 到 60 秒足够,HPCG 会在达到设定时间后自动收敛输出最终结果。但如果你拿它做正式基准测试,需要把时间拉长,规则允许的情况下通常跑 1 小时才更有统计意义。时间设太短,比如 5 秒,性能结果波动会非常大,几次跑出来的数字能差百分之三四十。
5.4 不同配置下的规模建议表
根据节点数量和内存总量,我常用这样一组起始规模:
| 配置 | 总进程数 | 进程网格 | nx,ny,nz | 估算内存 |
|---|---|---|---|---|
| 单节点 16 核 | 16 | 4x4 | 256 | 约 8 GB |
| 2 节点 32 核 | 32 | 4x8 | 320 | 约 16 GB |
| 4 节点 64 核 | 64 | 8x8 | 512 | 约 64 GB |
| 8 节点 128 核 | 128 | 8x16 | 640 | 约 128 GB |
注意这几个规模只是"能跑"的起点,不一定是最优。如果你的节点内存特别大,可以适当加大规模,让每个进程处理的未知量在 100 万到 300 万之间,这样既不会因为问题太小导致通信开销占比过大,也不会因为内存不够而失败。
6. 从单节点到多节点:实测过程与性能数字解读
6.1 第一个能看的成绩单
规模确定之后,正式跑分命令如下:
bash复制mpiexec --bind-to core -f hosts -n 64 ./xhpcg
--bind-to core 这个参数强烈建议加上。它让 MPI 进程在启动时绑定到具体物理核心,避免多个进程在运行过程中被操作系统调度器来回迁移,成绩会稳定不少。
输出末尾会出现类似这样的内容:
text复制Optimization type .................................. OPTIMIZATION
Final HPCG result is:
OPTIMIZATION, PERFORMANCE
CG, 198.4 GFLOP/s
这个数值就是 HPCG 最终成绩。我第一次跑的时候没有绑核,同样 64 进程连续跑三次,成绩分别是 176、198、187 GFLOP/s,波动很大。加上 --bind-to core 之后,三次结果基本稳定在 195 到 199 之间。
