Linux下Linpack实测:从浮点峰值到HPL调优排查

一台新机器上架,采购清单上写着双精度峰值多少 TFLOPS,同事转头问你“实际能跑多少”,你打开业务程序盯了一晚上 CPU 利用率,还是给不出一个数。这个场景我碰到过不止一次。Linux 下使用 Linpack 测试,本质上就是回答这个问题的标准动作:它用一个规模可自定、计算量几乎全压在浮点乘加上的稠密线性方程组求解任务,把机器从内存通道到各级缓存再到向量单元这一整条链路压到极限,最后吐出一个 GFLOPS 数字。但先说清楚一个前提——Linpack 分数不是“机器性能”的同义词,它是“在特定问题规模、特定 BLAS 实现、特定进程布局下,这台机器在稠密双精度 LU 分解上跑到的最好成绩”。读懂这句话,后面所有参数才有意义。这篇内容写给三类人:刚接手集群或高性能工作站的运维、要给新硬件做验收的工程师、以及想弄明白自己机器到底几斤几两的开发者。不需要你有并行计算背景,但需要你愿意动手改配置、反复跑几轮。

1. 把“峰值”和“实测”之间那道沟壑量出来

很多人第一次跑 Linpack 的心情是复杂的:跑之前觉得自己在测性能,跑完之后觉得自己在查故障。分数往往只有纸面峰值的五成到七成,如果是配置不当的机器,甚至能低到两成。这个落差不是玄学,它有非常明确的结构性来源。

1.1 Linpack 测的是稠密 LU 分解,不是你的业务代码

HPL(High Performance Linpack)求解的是形如 Ax=b 的稠密线性方程组,用的是带部分主元的 LU 分解,浮点运算量约为 (2/3)·N³ + 2N²,性能按 (2/3)·N³ / 耗时 折算成 FLOPS。这个公式里 N 的三次方意味着什么?意味着一旦 N 足够大,绝大部分时间都花在矩阵乘法式的分块更新上,也就是 GEMM。GEMM 恰恰是所有计算模式里对硬件最友好的那一种——数据分块后能在寄存器和 L1 里反复复用,访存压力小,向量指令几乎可以无间断地发射。

所以 HPL 测出来的东西,本质是你的机器在“算力密集、数据局部性极好”这一极端条件下的上限。它几乎不考查内存随机访问、不考查分支预测、不考查缓存 miss 带来的停顿。你的业务代码如果是图计算、稀疏矩阵、大量指针跳转或者频繁上下文切换,跑出来的效率会远低于这个分数。我见过不少团队把 HPL 分数直接当作业务性能预期写进验收文档,后面被业务方追着问“为什么说好几十 TFLOPS,实际只有零头”,根子就在这里。分数是一个参照系,不是一个承诺。

1.2 三种容易混淆的 Linpack 形态

市面上叫“Linpack”的东西至少有三个,混用会导致完全错误的结论,先分清楚再动手。

形态求解对象数据规模典型用途结果单位
早期 LINPACK 库基准稠密方程组,100×100 阶极小,全在缓存里历史遗留,几乎没有参考价值MFLOPS
HPL稠密方程组,N 可达几十万大,内存受限HPC 系统排名、硬件验收、稳定性压测GFLOPS / TFLOPS
HPCG稀疏方程组,共轭梯度大,带宽受限衡量更贴近真实应用的性能GFLOPS

还有一个常被拿来凑数的是“单核 Linpack”,它把 MPI 关掉、进程数设成 1,测的是单个核心的极限算力。这个东西有它的用处——验证编译选项和 BLAS 是否生效特别快,30 秒就能出结果——但它跟整机性能完全是两码事。我在做新机器初检时,习惯先用单核跑一轮,确认 BLAS 走的是优化路径、编译器选项确实进去了,再去折腾多进程。这个顺序能省掉大量“改了半天参数发现根本没编译对”的时间。

1.3 什么情况下值得花时间跑一次

Linpack 是个重活:编译、调参、反复跑,时间充裕的话一套流程下来小半天就没了。它不适合作为日常监控指标,也不适合用来比较不同架构的机器(这点后面第 6 章会细说)。我认为真正值得跑的场景有四类:

  • 硬件到货验收。新机器上架,跑一轮确认没有坏核、没有降频、内存通道没插错。我遇到过一次机器内存条少插了两根,Linpack 分数直接掉了三成多,比任何监控告警都直观。
  • 调优前后对比。换了 BLAS 库、改了编译选项、调整了 NUMA 绑定策略,用同一套 hpl.dat 跑前后两轮,效果一目了然。
  • 稳定性压测。HPL 会把所有核心的浮点单元长时间拉满,散热不良、供电不足、内存颗粒有问题,往往在跑大 N 的时候暴露出来。跑完不崩、残差校验 PASSED,本身就是一种通过性验证。
  • 给业务性能定锚。先有 HPL 这个上限,才能判断业务代码跑到上限的百分之多少算合理,才有优化的方向感。

反过来说,如果只是想看看机器日常负载表现、或者想比较两台不同架构机器的“综合性能”,那 Linpack 不是合适的工具,跑出来的数字容易误导决策。

2. 跑之前先把底层三件套配平

编译不通过、跑起来段错误、分数低得离谱,八成问题都出在这一层。我习惯把准备工作拆成数学库、编译与通信、系统状态三块,逐块确认。

2.1 BLAS 库选型:这条链路的上限由它决定

HPL 自己做的东西其实很有限,它主要负责分块调度、进程间广播和主元交换,真正的矩阵乘加全部下沉给 BLAS 的 GEMM 例程。所以 BLAS 实现的优劣,几乎直接决定了分数上限。同一个 N、同一台机器,换一个 BLAS,分数差一倍都不夸张。

常见的几类选择:

  • 开源通用实现:如 OpenBLAS、BLIS。优势是免费、可自编译、支持大量架构,社区版针对主流 x86 和 ARM 都有手工优化的汇编内核。缺点是不同版本的性能波动明显,某些版本在特定 CPU 上会有明显退步,选版本时最好看一下发布说明里有没有相关的内核改动。
  • 厂商提供的优化数学库:通常在安装系统时就随硬件一起提供,对本厂商 CPU 的指令集和缓存层次做了深度适配,一般能拿到最好的单核性能。代价是绑死平台,跨架构迁移时配置要重写。
  • 参考实现(Reference BLAS):纯 Fortran 写成,性能极低,只适合验证编译链路是否通。千万别拿它跑正式分数,那等于自欺欺人。

判断当前用的是哪一套,最直接的办法是看链接时的库路径,或者跑单核 HPL 看分数——如果单核只有几个 GFLOPS 而机器理论上应该有几十上百,基本可以断定落到了参考实现上。另一个快速判断是ldd ./xhpl,把依赖库列出来看一眼,路径指向系统默认的参考库就说明配错了。

还有一个容易被忽略的点:多线程 BLAS 与 MPI 进程数的冲突。如果 BLAS 库自己开了线程池,而你又用 mpirun 起了 N 个进程,总线程数变成 N 乘线程数,核心被反复抢占,性能会断崖式下跌。所以编译 HPL 时要么用单线程版 BLAS,要么在运行前显式把 BLAS 的线程数环境变量设成 1。这一条我踩过,当时分数只有预期的三分之一,查了两天才发现是线程超订。

2.2 编译器与 MPI 的版本匹配

HPL 的编译依赖三样东西:C 编译器、Fortran 编译器、MPI 实现。三者需要来自同一套工具链或者至少 ABI 兼容,否则会出现各种诡异的链接错误或运行时崩溃。

我一般遵循这么几条经验:

  1. 优先用系统自带的编译器和 MPI。跨版本混搭是常见的坑源,尤其是在一台机器上用 A 版 MPI 编译、换到另一台机器用 B 版 MPI 运行,轻则报版本不匹配,重则段错误。要跨机器跑,就用同一份环境镜像,或者干脆在目标机器上重新编译。
  2. 优化等级用 -O3,指令集用 -march=native。前者开启向量化和循环展开,后者让编译器针对本机 CPU 生成最优指令。但要注意-march=native编译出来的二进制没法在其他型号的 CPU 上运行(会触发非法指令),如果要在异构集群里分发,就退化成具体指令集,比如显式指定某个向量指令集。
  3. 数学库的链接顺序很关键。链接器是从左到右解析符号的,BLAS 库必须放在 HPL 目标文件之后。顺序错了会报一堆未定义符号,而这个错误信息往往指向的方向完全不对,让人误以为是源码缺文件。

编译完成后先别急着跑多进程,用单进程跑最小规模的 N=1000 验证一下链路。这一步只花十几秒,能过滤掉绝大部分环境问题。

2.3 系统层准备:频率、NUMA、大页与超线程

环境配好了,系统状态不对照样白搭。HPL 对这几项特别敏感:

CPU 频率调节策略。默认的节能调速器会在负载波动时来回变频,HPL 这种持续满载的场景理论上会稳定在高频,但实际观测中,进入满载的爬坡过程和核心间的频率不一致仍然会吃掉几个百分点。跑分前把调速器设成性能模式是标准操作。命令是cpupower frequency-set -g performance(需要 root),跑完再改回来,别让机器长期跑在性能模式上白烧电。

NUMA 与进程绑定。多路机器上,每个 CPU 插槽有自己的本地内存。如果进程被调度到了 A 插槽但访问的内存在 B 插槽,每次访存都要跨互联,延迟翻倍。HPL 的分块数据分布是确定的,只要进程和内存都绑到同一个节点上,跨节点访问量就能降到很低。绑定方式有两种:用 numactl 包一层,或者用 MPI 自带的绑定参数。后者更灵活,我一般优先用 MPI 的参数,下面第 4 章会给具体写法。

透明大页。HPL 的矩阵规模动辄几十 GB,页表项数量巨大,TLB miss 会成为隐性成本。把透明大页设为 always 通常能带来个位数百分比的提升,N 越大越明显。设置方式是往/sys/kernel/mm/transparent_hugepage/enabled写 always,注意有些发行版默认是 madvise,需要显式改。

超线程。这条要单独强调:HPL 是纯计算密集型,一个物理核跑一个进程时,流水线已经塞满了,再塞一个逻辑核进来只会争抢执行单元和缓存。实测下来,进程数设成逻辑核数(也就是物理核的两倍)时,分数通常不升反降。所以进程数一般等于物理核数,这一点和 Web 服务类负载的直觉正好相反。

3. hpl.dat 里每一行对应的硬件含义

hpl.dat 是 HPL 唯一的输入配置文件,一共三十来行,看起来吓人,但真正影响分数的只有五六个数字,其余基本都是保持默认即可的调优开关。第一次看到这个文件的人往往被行数劝退,其实把它按功能分块读就清楚了。

3.1 N、NB、P、Q 四个数的推导过程

这四个数字是核心,其余都是修饰。

N 是问题规模,也就是矩阵的阶数。它决定计算量和内存占用。N 太小,通信和启动开销占比高,跑不满;N 太大,内存放不下直接崩。经验取值是让矩阵占用可用内存的 70% 到 80%,留出余量给残差计算和工作区。

单个进程能分到的矩阵元素数是 N²/(P·Q),每个双精度数 8 字节,所以约束是:

8 · N² / (P·Q) ≤ 0.8 · 单进程可用内存 N ≤ sqrt(0.8 · 单进程内存 · P · Q / 8)

举个例子,16 个进程,每进程可用 8 GB,P·Q=16:

N ≤ sqrt(0.8 × 8 × 1024³ × 16 / 8) ≈ sqrt(1.37e10) ≈ 117000

实际取 N = 100000 左右比较稳妥,并且要能被 NB 整除。

NB 是分块大小,决定每次更新时的块宽。它和 CPU 的缓存容量、向量寄存器宽度强相关。块太小,GEMM 的效率上不去;块太大,块本身装不进缓存,反而变慢。常见取值在 128 到 256 之间,具体哪个最优只能扫。我的经验是先试 192 和 256 两个值,如果机器缓存偏小就往下试 128、160。

P 和 Q 是进程网格的两个维度,必须满足 P·Q = 进程总数。原则是尽量接近正方形,同时让 P 略小于等于 Q。原因是进程按行优先排布时,列方向的通信量更大,把列方向拉长一些能减少广播次数。比如 64 个进程,8×8 是最优;48 个进程就取 6×8;如果进程数是质数,那只能 1×N,通信效率会很差,这也是为什么跑 HPL 时进程数尽量取合数。

3.2 内存估算:别让最后一个进程 OOM

N 定得激进的最常见后果不是跑得慢,而是跑到一半被系统杀掉。而且往往不是均匀地崩,是某个进程内存先到顶,被内核 OOM killer 干掉,mpirun 收到一个进程退出就整体终止,日志里只留一行不痛不痒的提示。

稳妥的做法是跑之前先算,跑的时候再盯。算的公式上面给了,盯的办法是用/usr/bin/time -v mpirun ...包一层,结束后会打印峰值常驻内存。如果峰值贴着物理内存上限,就说明 N 该往下调一档了。

还有一个隐藏开销容易漏:残差校验阶段需要额外存一份向量和中间结果,虽然量级是 O(N) 远小于矩阵本身,但配合文件系统缓存、MPI 缓冲区,实际峰值会比纯矩阵大出几个百分点。留 20% 余量不算保守。

另外提醒一句,内存通道数比容量更影响分数。同样容量,插满所有通道和只插一半,GEMM 的数据供给速度差别很大,分数能差两到三成。验收新机器时如果分数莫名其妙偏低,先检查内存插槽是不是插满了、有没有插错槽位导致通道不均衡。

3.3 一份改两个数字就能跑的配置样例

下面是 16 进程、N=100000、NB=192、4×4 网格的一份配置,逐行注了含义。实际使用时通常只需要改 N、NB、P、Q 这几行。

HPLinpack benchmark input file Local performance test HPL.out output file name (if any) 6 device out (6=stdout,7=stderr,file) 1 # of problems sizes (N) 100000 Ns 1 # of NBs 192 NBs 0 PMAP process mapping (0=Row-,1=Column-major) 1 # of process grids (P x Q) 4 Ps 4 Qs 16.0 threshold 1 # of panel fact 2 PFACTs (0=left, 1=Crout, 2=Right) 1 # of recursive stopping criterium 4 NBMINs (>= 1) 1 # of panels in recursion 2 NDIVs 1 # of recursive panel fact. 1 RFACTs (0=left, 1=Crout, 2=Right) 1 # of broadcast 1 BCASTs (0=1rg,1=1rM,2=2rg,3=2rM,4=Lng,5=LnM) 1 # of lookahead depth 0 DEPTHs (>=0) 2 SWAP (0=bin-exch,1=long,2=mix) 64 swapping threshold 0 L1 in (0=transposed,1=no-transposed) form 0 U in (0=transposed,1=no-transposed) form 1 Equilibration (0=no,1=yes) 8 memory alignment in double (> 0)

几个容易改错的地方:

  • threshold那一行是残差检验的容差,标准值是 16.0。有人为了让结果“通过”偷偷把它调大,这等于自己骗自己,跑出来的分数不具备任何参考意义。看到残差校验失败,正确的做法是查配置,不是改阈值。
  • # of problems sizes (N)和下面的 N 值列表要对应。如果你写了一行数字声明有 3 个 N,下面就必须列 3 个数,HPL 会依次跑完输出三组结果,这在批量扫描时很方便。
  • 缩进和空格不影响解析,但行序绝对不能错。HPL 是按顺序读字段的,少一行或者多一行注释都会导致后面全部错位,报出来的错误信息还基本没有指向性。改这个文件时我习惯整段替换,而不是插入行。

4. 编译、启动与输出解读

4.1 Makefile 里的 arch 段落

HPL 的源码包里,Make.Linux之类的文件(名字随版本略有差异)顶部有一段 arch 定义,需要填四个变量:arch、TOPdir、MPdir、LAdir,以及编译器、链接器、编译选项、链接选项。

# 编译前先定义好 arch,名字随意但要和后续 make 命令一致 make arch=mybox

对应的编译命令大致长这样:

mpicc -O3 -march=native -DADD_ -I${MPI_INCLUDE} -c ${INC} ../src/*.c mpif77 -O3 -march=native -c ../src/*.f mpif77 -o xhpl *.o ${LIB}

几个细节值得说:

-DADD_这个宏表示 Fortran 与 C 之间符号拼接用下划线,绝大多数 MPI 实现都是这个约定。如果链接阶段报一大堆形如dgemm_未定义的符号,先去查这个宏有没有漏。

${LIB}里放 BLAS 和 MPI 的库,顺序是 HPL 目标文件 → BLAS → MPI。顺序反了会出现“库找到了但符号没解析”的怪现象。

优化选项里,我一般会额外加-funroll-loops和针对 Fortran 的-fno-second-underscore(视 MPI 约定而定)。这些不是必选项,但对着编译日志试几轮,能抠出几个百分点。

编译产物xhpl出来后,先别急着大规模跑。用 1 个进程、N=1000 跑一遍:

mpirun -np 1 ./xhpl

输出里如果能看到PASSED,说明编译链路、BLAS 链接、hpl.dat 解析全部正常。这一步十几秒,但能避免后面所有无效折腾。

4.2 运行命令与进程绑定

正式跑的时候,绑定参数对分数的影响比很多人想象中大。我常用的两种写法:

# 写法一:每个节点固定进程数,绑到物理核心 mpirun -np 16 \ --bind-to core \ --map-by ppr:16:node \ ./xhpl # 写法二:按插槽分布,适合多路机器 mpirun -np 16 \ --bind-to core \ --map-by socket \ ./xhpl

--bind-to core让每个进程固定在一个物理核上,避免调度器来回迁移导致缓存失效。ppr:16:node表示每个节点起 16 个进程。如果是多路机器,--map-by socket会把进程均匀撒到各个插槽,配合访存的本地性,效果通常更好。

单机跑最简单,直接mpirun -np N ./xhpl,但要注意有些 MPI 实现默认会拒绝在 root 用户下运行,需要额外参数豁免。这不是性能问题,是安全策略,别以为是环境坏了。

跑的时候盯着top或者htop看一眼,所有核心应该都接近 100%,内存占用平稳爬升到峰值后走平。如果有核心长期低负载,说明绑定或者进程数配错了。如果内存曲线呈现锯齿状反复涨落,很可能是 N 太大引发了换页,性能会掉得非常惨——这种时候分数没有参考价值,调小 N 重跑。

4.3 从 xhpl 的打印内容反推瓶颈位置

HPL 的输出很简洁,但信息量不小。典型输出是这样:

================================================================================ T/V N NB P Q Time Gflops -------------------------------------------------------------------------------- WR11C2R4 100000 192 4 4 142.37 4.681e+03 -------------------------------------------------------------------------------- ||Ax-b||_oo/(eps*(||A||_oo*||x||_oo+||b||_oo)*N)= 0.0021 ...... PASSED

前六个字段是配置回显,用来确认 hpl.dat 确实被正确解析了。接下来是耗时(秒)和折算出来的 GFLOPS。最后一行是残差校验。

几个读法:

T/V这一列描述的是不同分块步骤所用的算法组合,比如WR11C2R4大致对应面板分解用右看、递归深度、广播方式等参数的缩写。它本身不用记,但如果对比两次运行时这一列变了,说明配置文件被改过,分数就不具备可比性。

残差那一行的数值应该在 0.001 到 1 之间,并且以PASSED结尾。如果看到FAILED,说明解出来的 x 代回方程偏差太大,可能的原因是阈值设得太小、内存布局参数(比如 L1/U 转置形式)和 BLAS 不匹配、或者机器本身有硬件错误。这种情况下的耗时数字直接作废,不要拿去做任何对比。

如果分数比预期低很多,而配置看起来又没问题,可以在 hpl.dat 里把# of problems sizes改成多个 N 值,一次跑几个规模,看分数随 N 的变化趋势。正常情况下 N 从 20000 涨到 100000,分数应该明显爬升并逐渐收敛。如果分数在大 N 时反而下降,基本可以断定是内存带宽或者换页的问题,而不是算力问题。

5. 分数上不去的排查链路

这一节是我花时间最多的地方,也是我认为最有价值的部分。所有结论都是踩出来的,不是查文档查出来的。

5.1 先分清“跑不通”和“跑得慢”

这两种情况排查方向完全不同,但现象经常混淆。我的判断标准很简单:能出 PASSED 结果但分数低,属于跑得慢;中途退出、报错、卡死不返回,属于跑不通。

跑不通的常见原因,按出现频率排:

现象大概率原因验证方式
启动即报未定义符号链接顺序或-DADD_宏问题看链接日志里第一个未定义符号名
跑几秒后某个进程退出内存不足被 OOM killer 杀掉dmesg查 OOM 记录,或time -v看峰值
卡在启动阶段不动MPI 进程间无法通信(防火墙、主机名解析)换单机模式验证,再查节点间连通性
跑完残差 FAILED阈值过小或内存对齐参数不匹配把 alignment 改大重试,检查 BLAS 转置约定
段错误在 LU 分解阶段BLAS 库版本与编译时的头文件不匹配ldd查实际加载的库路径

跑得慢的原因更隐蔽,因为一切看起来都正常。这种情况按下面这个顺序查,命中率最高:进程绑定是否正确 → BLAS 是否开了多线程超订 → 内存通道是否插满 → N 和 NB 是否合适 → 频率是否被限制。

我遇到过一次非常典型的情况:机器有 64 个物理核,用mpirun -np 64跑,分数只有同类机器的一半。top显示所有核心都在忙,一切正常。最后发现 MPI 默认的进程分布策略把 64 个进程挤在了两个插槽中的一个上,另一半 CPU 在空转。加上--map-by socket之后分数立刻翻倍。这个坑的隐蔽之处在于,从操作系统视角看“CPU 利用率 100%”,但利用率是按逻辑核算的,真的全忙还是只有一半忙,光看百分比看不出来。

5.2 参数扫描的正确顺序

调参最忌讳一上来就全组合扫描。N 有五个候选、NB 有四个候选、网格布局有三种,全跑一遍就是六十次,每次几分钟,一天就没了,而且结果还很难归因。

我的顺序是这样的,每一步只动一个维度:

第一步,固定进程数和网格,扫 NB。把 N 固定在一个中等规模(比如内存允许的 60%),P、Q 固定,NB 从 128 试到 256,步长 32。这一步耗时最短,收益往往最大,能带来 5% 到 15% 的提升。因为 NB 直接决定 GEMM 的分块效率,与缓存和向量宽度最相关。

第二步,固定最优 NB,扫 N。从小到大依次跑 40%、60%、80%、85% 内存占比对应的 N 值。观察分数是否收敛。如果 N 越大分数越高但增长趋缓,说明已经接近本机极限;如果大 N 反而下降,说明撞到了内存或换页,往回收一档。

第三步,扫 P×Q 布局。在进程数固定的前提下尝试不同的因数分解。比如 64 进程可以试 8×8、4×16、16×4。一般 8×8 最优,但差异可能只有几个百分点,属于精修。

第四步,微调算法参数。也就是 PFACT、RFACT、BCAST、SWAP 那几行。这些参数的影响通常在 3% 以内,而且和具体 BLAS 实现相关,不同机器上最优值不一样。除非前三步已经榨干了,否则不建议花时间在这上面。

一个很重要的提醒:每次只改一个参数,并且记录完整配置。我见过太多人改完 A 又改 B,分数涨了却不知道是谁的功劳,下次换机器就没法复现。老老实实建个表格记下来,看起来笨,实际上省时间。

5.3 一份现象-原因对照表

下面这张表是我这些年积累的,覆盖了大部分“分数不对劲”的场景,可以直接拿去对号入座。

现象可能原因处理办法
分数约为预期的 1/2 到 1/3BLAS 多线程与 MPI 进程超订把 BLAS 线程数环境变量设为 1
分数约为预期的 1/4内存通道只插了一半检查插槽,插满所有通道
分数随 N 增大先升后降大 N 触发换页缩小 N,或关闭其他占内存的服务
单核分数异常低落到了参考 BLAS 实现检查链接库路径,重新指定优化库
多进程分数与单进程相仿进程实际跑在同一核上加--bind-to core,检查绑定是否生效
所有核心满载但分数偏低频率被限制在基频以下检查调速器策略和散热状态
同配置两次跑差 5% 以上系统有其他负载干扰清理后台任务,跑前等待系统稳定
残差校验接近阈值上限精度问题,可能是编译选项过于激进去掉部分浮点优化开关重编译

这里单独说一个“同配置两次跑差 5% 以上”的情况。HPL 本身是确定性计算,理论上一模一样的配置跑两遍,结果应该几乎完全一致。如果差异明显,说明有外部变量:别的进程在抢 CPU、内存被其他服务占用、温度升高导致降频、甚至后台的文件同步任务占着磁盘 IO 影响了大页分配。做性能对比时,环境干净比参数精确更重要。我的习惯是跑之前先确认负载为零,跑完记录当时的环境温度,如果两次温差超过 10 度,分数差异要打个折扣看。

6. 结果记录与跨机器对比的注意事项

分数出来不是终点,怎么用这个数字才是。这里有两个容易翻车的地方。

6.1 一个分数背后必须记录的信息

单独一个 GFLOPS 数字是不具备可比性的,它必须和一套上下文绑定。我自己的记录模板包括这几项,缺一项就认为这次测试不完整:

  • 硬件配置:CPU 型号的核心数、主频、内存容量与通道数、是否开启超线程。
  • 软件环境:BLAS 实现及版本、MPI 实现及版本、编译器版本与优化选项。
  • 关键参数:N、NB、P、Q、进程数、绑定策略。
  • 运行环境:调速器策略、大页设置、是否有其他负载、环境温度。
  • 结果:耗时、GFLOPS、残差校验值、是否 PASSED。
  • 峰值参照:这台机器的理论双精度峰值是多少,算出来的效率百分比。

最后一项尤其重要。理论峰值的算法是:单核每周期浮点运算次数 × 核心数 × 主频。不同架构每周期能做的 FMA 数量不同,这个值得查清楚再算。有了效率百分比,跨机器的对比才有意义——同样是 5 TFLOPS,一台是峰值的 80%,一台是峰值的 40%,后者显然还有大量调优空间。

6.2 换机器、换库之后的对比陷阱

最容易犯的错误是拿两台架构不同的机器的绝对分数直接比较。不同架构的每周期浮点吞吐、缓存层次、内存带宽都不同,绝对分数高不代表“这台机器更适合你的业务”。Linpack 分数只能说明在稠密双精度计算这一点上谁更强。

第二个陷阱是换了 BLAS 版本之后直接对比。同一个库的不同版本,性能可能差 20% 以上。如果两次测试之间库版本变了,那分数差异里既有硬件因素也有软件因素,结论就不可靠了。做对比时,要么锁死软件版本只变硬件,要么锁死硬件只变软件,一次只动一个变量。

第三个陷阱是用小规模测试的结论指导大规模配置。N=20000 时最优的 NB,在 N=100000 时未必最优。因为规模变大后,通信占比、缓存命中率、内存访问模式都会变。所以调参一定要在接近最终使用的规模上做,小规模只能用来快筛。

最后再分享一个我自己的习惯:每次跑完正式测试,把那份 hpl.dat 连同编译脚本、运行命令、结果输出一起打包存档,命名带上日期和机器标识。下次换机器要对比时,直接解压照着跑,不用回忆当时是怎么配的。这个习惯看着啰嗦,但在需要回溯“三个月前那台机器为什么分数高”的时候,能救命。踩过几次“配置忘了、只能重新调一遍”的坑之后,我现在的做法是:没归档的测试,等于没跑过。