高性能计算训练营

HPC 性能分析完整指南:从理论峰值到经验 Roofline

本指南由两部分组成:

  • 练习 1:用公式和命令,手动算出机器的理论性能上限(FLOP/s 峰值、内存带宽、机器平衡)
  • 练习 2:用 Empirical Roofline Tool (ERT) 实测同一台机器,画出经验 Roofline 图

理论值画天花板,实测值告诉你"实际上限"。两者对照,才是完整的性能画像。


第一部分:理论性能计算

目标:拿到任意一台 Linux 机器,用几条命令 + 三个公式,估算它的三个理论性能上限。 这是理论峰值(理想上限),不是实测值——实测要靠 STREAM(内存)、HPL(浮点)这类基准。


0. 速查卡(只想照做,就看这张表)

想算 公式 变量从哪里来
FLOP/s 峰值 核心数 × 每核每周期 FLOP × 主频 lscpu + 微架构表(第 2.3 节)
内存带宽 已插内存的通道数 × 速率(MT/s) × 8 字节 sudo dmidecode -t memory(第 2.4 节)
机器平衡 内存带宽 ÷ FLOP峰值 上面两个算完再除

⚠️ 三个最容易算错的地方,先记住:核心数别用超线程后的逻辑数每核每周期 FLOP 不是看 flag 就能定,要查微架构内存带宽按"已插了几根条子"算,不是按 CPU 支持几个通道算


1. 三个概念,用你熟悉的话讲

1.1 FLOP/s 峰值 —— "算得有多快"

  • FLOP(Floating-point Operations,浮点运算次数)= 一次浮点运算(加/减/乘/除都算一次)。
  • FMA(Fused Multiply-Add,融合乘加)= a×b+c 一条指令做完,官方计作 2 个 FLOP(1 次乘 + 1 次加)。现代 CPU 的浮点峰值几乎全靠 FMA。
  • 向量化(SIMD):一条指令同时算多个数。具体点就是:CPU 的一种单指令多数据(Single Instruction, Multiple Data)并行技术——一条指令同时处理多个数据,而不是一条指令只处理一个数据。你写 C/C++ 时用到的 _mm256_add_ps_mm512_fmadd_pd 就是这类。向量越宽(128/256/512 位),一次算的数越多。
  • 每周期 = 每个时钟周期;主频 2.0 GHz = 每秒 20 亿个周期。

所以:

1
FLOP/s = 核心数 × 每核每周期能做多少次 FLOP × 每秒周期数(主频)

1.2 内存带宽 —— "喂数据有多快"

DDR 内存每根通道(channel)每个传输周期搬 64 bit = 8 字节。速率用 MT/s(兆传输/秒)标称,直接拿来乘即可:

1
内存带宽 = 已插内存的通道数 × 速率(MT/s) × 8 字节/传输

注意是"已插的通道数":某通道没插内存条,那个通道就不参与搬数据。一台双路服务器常说"8 通道 × 2 插槽 = 16 通道",是指满配;实际插了几根,就是几个通道在干活(详见第 2.4 节)。

1.3 机器平衡 —— "这台机器是算得快,还是内存快"

1
机器平衡 = 内存带宽 ÷ FLOP峰值    单位:Byte/FLOP

含义:每做 1 次浮点运算,平均有多少字节的内存数据可以供应。

⚠️ 反过来写也常见:FLOP峰值 ÷ 内存带宽,单位 FLOP/Byte,叫"运算强度"或 Roofline 的"转折点(ridge point)"。两者互为倒数,别混:

写法 公式 数值大小
Byte/FLOP 带宽 ÷ 峰值 小(约 0.05~0.6)
FLOP/Byte 峰值 ÷ 带宽 大(约 2~20)

用途(Roofline 模型):某个计算 kernel 的"算术强度"(每搬 1 字节数据做几次 FLOP)

  • < FLOP/Byte(即受内存限制) → 性能被带宽卡住;
  • > FLOP/Byte → 性能被计算卡住。

本文统一用 Byte/FLOP 作为"机器平衡",需要时顺便给出倒数。


2. 动手:在你的 Ubuntu 服务器上收集数据

下面的命令都能在 Ubuntu 20.04 上跑。sudo 那条需要 root 权限。

2.1 核心数、主频、型号

1
lscpu

输出很长,只看这几行(下面是个双路服务器示例):

1
2
3
4
5
6
7
8
9
Architecture:          x86_64
CPU(s): 112 # 逻辑处理器总数 = 物理核 × 超线程
Thread(s) per core: 2 # 超线程:每核 2 线程
Core(s) per socket: 56 # 每个插槽的物理核数 ← 用这个
Socket(s): 2 # 插槽(CPU 颗数)
Model name: Intel(R) Xeon(R) Platinum 8480+
CPU max MHz: 3800.0000 # 最大睿频
CPU min MHz: 800.0000
Flags: ... avx512f ... avx2 ... fma ...

只取关键字段:

1
lscpu | grep -E 'Model name|Socket|Core|Thread|CPU max MHz|CPU MHz'

关键:核心数 = Socket(s) × Core(s) per socket不要CPU(s)(那是把超线程算进去了)。超线程的两个线程共享同一套浮点单元,算峰值只认物理核

2.2 判断 SIMD 宽度(看 flags)

1
lscpu | grep -o 'avx512f\|avx2\|sse2\|fma' | sort -u
输出里出现 指令集 单条向量最宽
avx512f AVX-512 512 位
avx2(无 avx512f AVX2 256 位
只有 sse2 SSE 128 位
fma 支持 FMA 现代 CPU 基本都有

⚠️ flag 只能告诉你"一条指令最宽到多少位",不能直接定每周期 FLOP。因为每周期能做多少个 FMA,取决于"有几个多宽的 FMA 执行单元"——这是微架构决定的,CPU 型号相同才相同。两个反例:

  • Intel Skylake-SP 报 avx512f,确实每周期 2 个 512 位 FMA;
  • AMD Zen4 也报 avx512f,但它内部是把 512 位拆成 2 个 256 位来做,每周期有效吞吐和"2×256 位"一样;
  • AMD Zen1 / 海光 Hygon 报 avx2(256 位指令),却只有 2 个 128 位 FMA 单元,256 位指令要"双泵"执行,每周期吞吐等于"2×128 位"。

所以必须查微架构表(下节),不能只看 flags。

2.3 每核每周期 FLOP(查表,机器读不出来,得按型号对)

通用公式:

1
每核每周期 FLOP = FMA单元数 × (单元宽度 ÷ 每个元素位数) × 2
  • 每个元素位数:FP64=64 位、FP32=32 位;
  • ×2 是因为一次 FMA 算 2 个 FLOP。

常见 CPU 对照表(FMA单元 × 单元宽度 就是微架构决定的部分):

CPU 类型 每核 FMA 单元 单元宽度 FP64/核/周期 FP32/核/周期
消费级 Intel Core(Skylake 及以后,AVX2) 2 256 位 16 32
Intel Xeon 可扩展(Skylake-SP → Sapphire/Emerald Rapids) 2 512 位 32 64
AMD Ryzen / EPYC(Zen2/Zen3/Zen4,2×256 位) 2 256 位 16 32
AMD Zen1 / Zen+ / 海光 Hygon Dhyana 2 128 位(256 位指令双泵) 8 16
Intel 小核 E-core(Gracemont) 1 256 位 8 16

例:2 个 512 位 FMA → FP64 = 2 × (512/64) × 2 = 32/核/周期;FP32 = 2 × (512/32) × 2 = 64/核/周期。

拿不准就查你 CPU 型号的官方规格或架构资料(关键词:<型号> FMA units per corevector widthflops per cycle)。

2.4 内存通道数与速率

1
sudo dmidecode -t memory | grep -E 'Locator|Size|Type:|Speed'

读输出的要点:

  1. 数"已装的内存条":看 Size 有数值的条目(如 Size: 32 GB),跳过 Size: No Module Installed 的空槽。
  2. 通道数 = 已插条子的数量(前提:每通道只插 1 根,服务器的标准插法)。别按 CPU 的满配通道数算。
  3. 速率:看 Speed(MT/s,DIMM 标称)和 Configured Memory Speed(内存控制器实际跑的速率)。两者不同时,用 Configured(实际跑的)

输出示例(每插槽 8 通道、但只插了 2 根 DDR4-2933):

1
2
3
4
5
Size: No Module Installed      Locator: DIMM1    Bank Locator: P0 CHANNEL A
Size: 32 GB Locator: DIMM3 Bank Locator: P0 CHANNEL B ← 装了
Size: No Module Installed Locator: DIMM5 Bank Locator: P0 CHANNEL C
Size: 32 GB Locator: DIMM8 Bank Locator: P0 CHANNEL D ← 装了
Type: DDR4 Speed: 3200 MT/s Configured Memory Speed: 2933 MT/s

上例:装 2 根 → 只 2 个通道在干活 → 带宽 = 2 × 2933e6 × 8 ≈ 46.9 GB/s(不是满配 8 通道的 187 GB/s)。

没有 sudo 权限?按 CPU 型号查每插槽满配通道数,再问管理员插了几根条子。


3. 套公式:汇总填表

把第 2 节拿到的数填进来:

变量 从哪来
插槽数 ____ lscpu → Socket(s)
每插槽核数 ____ lscpu → Core(s) per socket
核心数 = 两者相乘 ____
主频 (GHz) ____ lscpu → CPU max MHz(见第 9 节"频率用哪个")
FMA 单元数 ____ 第 2.3 节微架构表
单元宽度 (位) ____ 第 2.3 节微架构表(512/256/128)
已插通道数 ____ dmidecode:数 Size 有值的条子
实际内存速率 (MT/s) ____ dmidecode → Configured Memory Speed

然后:

1
2
3
4
5
6
7
8
FP64/核/周期 = FMA单元数 × (单元宽度/64) × 2
FP32/核/周期 = FMA单元数 × (单元宽度/32) × 2

FP64峰值(FLOP/s) = 核心数 × FP64/核/周期 × 主频(GHz) × 1e9
FP32峰值(FLOP/s) = 核心数 × FP32/核/周期 × 主频(GHz) × 1e9

内存带宽(B/s) = 已插通道数 × 速率(MT/s) × 1e6 × 8
机器平衡(Byte/FLOP, 按FP64) = 内存带宽(B/s) ÷ FP64峰值(FLOP/s)

4. 参考示例一:双路 Xeon Platinum 8480+(假想配置)

指标 计算过程 结果
核心数 2 插槽 × 56 核 112
每核/周期 FP64 2 FMA × 512位/64 × 2 32
FP64 峰值 112 × 32 × 2.0e9 7.17 TFLOP/s
FP32 峰值 112 × 64 × 2.0e9 14.34 TFLOP/s
内存带宽(满配 16 通道) 16 通道 × 4800e6 × 8 614 GB/s
机器平衡(FP64) 614.4e9 ÷ 7.168e12 0.086 Byte/FLOP(≈ 11.7 FLOP/Byte)

一份常见讲义里这段算错过:把 2 个插槽漏算成 1 个(结果 3.58 TFLOP/s),且机器平衡单位差 1000 倍(写 0.17,实为把 TFLOP 当 GFLOP 所致,正确 0.086)。本文已订正。


5. 参考示例二:单路 i7-14700KF(他人机器,P 核 + E 核混合)

这个例子教你混合核心怎么算:P 核和 E 核的频率、每周期 FLOP 都不同,要分开乘再加

项目 P 核 (Raptor Cove) E 核 (Gracemont)
数量 8 12
峰值睿频 5.6 GHz 4.3 GHz
FMA 单元 2 × 256 位 1 × 256 位
FP64/核/周期 16 8
FP32/核/周期 32 16
1
2
3
4
5
SP 峰值 = 8×5.6×32 + 12×4.3×16 = 1433.6 + 825.6 = 2259.2 GFLOPS ≈ 2.26 TFLOP/s
DP 峰值 = 8×5.6×16 + 12×4.3×8 = 716.8 + 412.8 = 1129.6 GFLOPS ≈ 1.13 TFLOP/s
内存带宽 = 2 通道 × 5600e6 × 8 = 89.6 GB/s
机器平衡(DP) = 89.6e9 ÷ 1.13e12 = 0.079 Byte/FLOP(≈ 12.6 FLOP/Byte
机器平衡(SP) = 89.6e9 ÷ 2.26e12 = 0.040 Byte/FLOP(≈ 25.2 FLOP/Byte

6. 实战:你的 Hygon C86 5380(用真实命令输出走一遍)

下面把你贴的两段输出完整过一遍,展示怎么一步步读数。这台机器比上面两个假想例子更值得做,因为它同时踩中两个大坑:老 Zen 架构的向量单元、通道数虚高。

6.1 第一步:lscpu → 核心数与频率

你的输出里取这几行:

1
2
3
4
5
Thread(s) per core:    2          # 有超线程 → CPU(s)=64 是逻辑数,不能直接用
Core(s) per socket: 16 # 物理核 ← 用这个
Socket(s): 2
CPU max MHz: 2500.0000 # Frequency boost: disabled → 就用 2.5 GHz
Flags: ... fma ... avx ... avx2 ... # 无 avx512f → 指令最宽 256 位
  • 核心数 = 2 × 16 = 32(不是 64)。
  • 频率 = 2.5 GHz(boost 已关闭,CPU MHz: 2477 表明现在就跑在 ~2.48 GHz,用 2.5 做持续值很贴切)。

6.2 第二步:识别微架构 → 每核每周期 FLOP

Model name: Hygon C86 5380,海光(Hygon Dhyana),底层是 AMD Zen1 架构(lscpu 的 CPU family: 24 即海光的厂商号 0x18)。

按第 2.3 节表,Zen1/Hygon 每核只有 2 个 128 位 FMA 单元,虽然指令是 256 位(avx2)但要拆成两半执行:

1
2
FP64/核/周期 = 2 × (128/64) × 2 = 8
FP32/核/周期 = 2 × (128/32) × 2 = 16

⚠️ 若照着"Intel 式"误填成 2×256 位,会高估一倍。这正是 2.2 节警告的"avx2 不等于 2×256 位"。海光/EPYC Zen1 与同代 Intel 单核浮点能力差一半,这是架构事实,不是频率造成的。

6.3 第三步:dmidecode → 数条子、定速率

你的输出里 Size: 32 GB(装了)的只有 4 根,分别插在:

内存条 位置 通道
DIMM16 P0 CHANNEL D
DIMM3 P0 CHANNEL G
DIMM32 P1 CHANNEL D
DIMM19 P1 CHANNEL G

其余全是 Size: No Module Installed。再看速率:

1
2
Type: DDR4              Speed: 3200 MT/s          # DIMM 标称
Configured Memory Speed: 2933 MT/s # 实际跑的 ← 用这个
  • 已插通道数 = 4(别填 16。CPU 每插槽确实支持 8 通道 × 2 插槽 = 16,但那是满配,现在只有 4 个通道有内存)。
  • 速率 = 2933 MT/s(控制器实际跑 2933,不是条子标称的 3200)。
  • 单通道带宽 = 2933e6 × 8 ≈ 23.5 GB/s

6.4 第四步:套公式出结果

1
2
3
4
5
FP64 峰值  = 32 × 8  × 2.5e9 = 640 GFLOP/s ≈ 0.64 TFLOP/s
FP32 峰值 = 32 × 16 × 2.5e9 = 1280 GFLOP/s ≈ 1.28 TFLOP/s
内存带宽 = 4 × 2933e6 × 893.9 GB/s
机器平衡(FP64) = 93.9e9 ÷ 640e9 ≈ 0.15 Byte/FLOP(≈ 6.8 FLOP/Byte
机器平衡(FP32) = 93.9e9 ÷ 1280e9 ≈ 0.073 Byte/FLOP(≈ 13.6 FLOP/Byte

6.5 一个反直觉但重要的结论

这台"双路 32 核服务器"其实是内存受限机器:浮点峰值 0.64 TFLOP/s 还算常见,但内存带宽只有 ~94 GB/s(4 通道),因为 16 个通道只插了 4 根条子

对比:若把 16 通道插满(每通道 1 根 DDR4-2933),带宽可达 16 × 2933e6 × 8 ≈ 375 GB/s,机器平衡(FP64) 会变成 0.59 Byte/FLOP。

含义(结合 Roofline):

  • 现在跑带宽敏感的程序(STREAM 测速、稀疏矩阵/图计算、大数据量拷贝),就算 CPU 空转也只会拿到约 1/4 的内存带宽;
  • 想让这类负载提速,加内存条(让通道跑满)是最直接的,比换更强 CPU 有用得多;
  • 附带提醒:4 个 NUMA 节点各只有 1 个有内存的通道,写多线程程序要注意 NUMA 亲和(numactl/绑定到本地节点),否则跨节点访问还要绕 Infinity Fabric。

上面是"理论峰值"。想验证:装 STREAM 跑 Triad 看实测带宽是否接近 ~94×0.8≈75 GB/s;浮点可跑 HPL/LINPACK 看是否接近 ~0.64×0.7≈0.45 TFLOP/s。


7. 一键脚本(把第 3 节的值填进顶部 6 个变量即可)

保存为 compute_peak.shchmod +x,改前 6 行后运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env bash
# compute_peak.sh —— 估算单节点理论峰值
set -u

# ====== 按你的机器填这 6 个变量(对应第 3 节汇总表)======
SOCKETS=2 # lscpu -> Socket(s)
CORES_PER_SOCKET=16 # lscpu -> Core(s) per socket
FREQ_GHZ=2.5 # 主频 GHz(见第 9 节"频率用哪个")
FMA_UNITS=2 # 每核 FMA 单元数(第 2.3 节微架构表)
VEC_BITS=128 # 单个 FMA 单元宽度:512/256/128(注意海光=128!)
CHANNELS=4 # 已插内存条数量(dmidecode 数 Size 有值的,别填满配 16)
MT_PER_S=2933 # 实际内存速率 MT/s(dmidecode -> Configured Memory Speed)
# ====== 下面不用改 ======

CORES=$(( SOCKETS * CORES_PER_SOCKET ))
FP64_CYCLE=$(awk "BEGIN{print $FMA_UNITS * ($VEC_BITS/64) * 2}")
FP32_CYCLE=$(awk "BEGIN{print $FMA_UNITS * ($VEC_BITS/32) * 2}")

FP64_TF=$(awk "BEGIN{printf \"%.2f\", $CORES * $FP64_CYCLE * $FREQ_GHZ / 1000}")
FP32_TF=$(awk "BEGIN{printf \"%.2f\", $CORES * $FP32_CYCLE * $FREQ_GHZ / 1000}")
BW_GBPS=$(awk "BEGIN{printf \"%.1f\", $CHANNELS * $MT_PER_S * 8 / 1000}")
BAL_BPF=$(awk "BEGIN{printf \"%.4f\", $BW_GBPS*1e9 / ($FP64_TF*1e12)}")
BAL_FPB=$(awk "BEGIN{printf \"%.1f\", $FP64_TF*1e12 / ($BW_GBPS*1e9)}")

echo "核心数 : $CORES"
echo "FP64 峰值 : $FP64_TF TFLOP/s"
echo "FP32 峰值 : $FP32_TF TFLOP/s"
echo "内存带宽 : $BW_GBPS GB/s"
echo "机器平衡(FP64) : $BAL_BPF Byte/FLOP (= $BAL_FPB FLOP/Byte)"

脚本里我已按你的机器预填好。在服务器上执行预期输出:

1
2
3
4
5
核心数          : 32
FP64 峰值 : 0.64 TFLOP/s
FP32 峰值 : 1.28 TFLOP/s
内存带宽 : 93.9 GB/s
机器平衡(FP64) : 0.1467 Byte/FLOP (= 6.8 FLOP/Byte)

8. 常见坑(务必读)

  1. 频率用哪个? CPU max MHz 是单核睿频,多核全载达不到;AVX-512 重载还会进一步降频(AVX offset)。
    • 要"理想峰值"→ 用最大睿频(偏乐观);
    • 要"可达到的持续值"→ 用基础频率全核睿频(更接近真实)。若 BIOS 关了 boost(如你的机器 Frequency boost: disabled),max MHz 本身就是持续值,直接用。
  2. 超线程不算核。 核心数 = 插槽数 × 每插槽核数,别用 CPU(s)
  3. "支持 AVX-512/AVX2" ≠ 固定的每周期 FLOP。 一定按微架构定"FMA 单元数 × 单元宽度"(第 2.3 节)。Zen1/海光是 2×128 位,别当成 2×256。
  4. 内存带宽按"已插的条子"算。 CPU 支持 16 通道 ≠ 现在有 16 通道带宽;数 dmidecodeSize 有值的 DIMM。速率用 Configured Memory Speed(实际跑的),不是条子标称。
  5. 单位别混。 带宽 GB/s = ×10⁹ B/s,峰值 TFLOP/s = ×10¹² FLOP/s。机器平衡 = 带宽/峰值,得到的是个很小的数(Byte/FLOP);若算出 0.6 以上的"Byte/FLOP",多半把 TFLOP 当 GFLOP 了,或把满配通道当已插通道了。
  6. 这是理论值,不是实测。 实际持续性能还要看功耗墙、缓存、访存模式、NUMA。ERT 实测值通常只有理论值的 30%~60%,这正是"Roofline 模型"存在的意义——先画天花板,再实测能摸到哪。

第二部分:ERT 实测与 Roofline 分析

练习 1 算的是"理论天花板",ERT 做的是"实际能摸到多高"。两者结合,就是完整的 Roofline 分析。

你已经在服务器上跑完了 ./ert Config/config.mycpu.01,结果在 Results.MyCPU/Run.001/ 下。下面教你怎么读、怎么看图。


9. ERT 输出文件总览

执行 ./ert Config/config.mycpu.01 后,在结果目录下会生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Results.MyCPU/
└── Run.001/
├── roofline.json ← 原始数据(JSON)→ 第 10
├── roofline.gnu ← gnuplot 脚本
├── roofline.ps ← 主图表(PostScript)→ 第 11
├── config.ert ← 本次运行使用的配置快照
├── FLOPS.001/ ← FLOP强度=1 的详细数据 → 第 13
│ ├── OpenMP.0001/1 线程,3 次试验
│ │ ├── pre ← 试验配置
│ │ ├── try.001/002/003 ← 每次运行的原始数据
│ │ ├── max ← 该配置下所有工作集的最大值
│ │ ├── sum ← 汇总:各级缓存/DRAM 峰值
│ │ └── run.done ← 标记已完成
│ ├── OpenMP.0002/2 线程
│ └── ...
├── FLOPS.002/ ← FLOP强度=2
├── FLOPS.004/ ← FLOP强度=4
├── FLOPS.008/ ← FLOP强度=8
└── FLOPS.016/ ← FLOP强度=16
文件/目录 用途
roofline.json 原始数据(JSON):实测 GFLOP/s、各层级带宽、kernel 信息
roofline.ps 主图表(PostScript 格式):Roofline 图
roofline.gnu gnuplot 脚本:可重新生成或转换图表
FLOPS.001/ ~ FLOPS.016/ 每个 FLOP 强度的详细测试数据

10. 读 roofline.json——最快的数据概览

roofline.json 是 ERT 的核心输出,所有数据都在里面。用任意文本编辑器或 cat 打开,关注两个关键段。

10.1 实测浮点峰值

1
2
3
4
5
"gflops": {
"data": [
["FP64 GFLOPs", 195.0] ← 你海光机器的实测值
]
}

195 GFLOP/s = ERT 实测的 FP64 浮点峰值(所有 FLOP 强度下跑到的最高 GFLOP/s)。

10.2 实测各级带宽

1
2
3
4
5
6
7
8
"gbytes": {
"data": [
["L1", 1559.98], ← L1 缓存带宽,GB/s
["L2", 859.35], ← L2 缓存带宽
["L3", 52.01], ← L3 缓存带宽
["DRAM", 36.27] ← 实际内存带宽 ← 与第 3 节理论值对比
]
}
层级 你的实测 说明
L1 1560 GB/s 工作集 < 32 KB 时,数据在 L1 内循环
L2 859 GB/s 工作集 32 KB ~ 256 KB
L3 52 GB/s 工作集 256 KB ~ 16 MB
DRAM 36.27 GB/s 工作集 > 64 MB,必须走内存

10.3 配置元数据

1
2
3
4
5
"metadata": {
"CONFIG": "{...}",
"HOSTNAME": "('controller01', ...)",
"TIMESTAMP_DATA": "1788621874.0372012"
}

可以看到 ERT 的完整编译参数、线程配置、时间戳等。


11. 看 Roofline 图表——三种方法

ERT 生成的是 roofline.ps(PostScript 格式)。在 macOS 上查看:

方法一:安装Ghostscript转换PostScript(最稳)

1
brew install ghostscript

装完转换成 PDF:

1
2
3
cd /Users/jsy/Downloads/Results.MyCPU/Run.001
gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=roofline.pdf roofline.ps
open roofline.pdf

这是最稳的路,Ghostscript 就是 PostScript/PDF 的行业标准解释器。

方法二:用 gnuplot 重新生成 PNG

ERT 自带的 roofline.gnu 是为 PostScript 终端写的。生成高清 PNG:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cd /Users/jsy/Downloads/Results.MyCPU/Run.001
gnuplot -persist -e "
set term pngcairo size 1400,900 enhanced font 'Helvetica,14';
set output 'roofline.png';
set logscale x 10;
set logscale y 10;
set xlabel 'Arithmetic Intensity (FLOPs / Byte)';
set ylabel 'Performance (GFLOPs / sec)';
set title 'Empirical Roofline: controller01 (Hygon C86 5380, FP64)';
set grid xtics ytics;
set key top left;
set xrange [0.01:100];
set yrange [10:*];
# 实测数据点(从 roofline.json 读出)
plot \
1560*x w lines lw 2 lc rgb '#CC0000' title 'L1 (1560 GB/s)', \
859*x w lines lw 2 lc rgb '#DD5500' title 'L2 (859 GB/s)', \
52*x w lines lw 2 lc rgb '#FF6600' title 'L3 (52 GB/s)', \
36.27*x w lines lw 2 lc rgb '#0066CC' title 'DRAM (36.3 GB/s)', \
195 w lines lw 3 lc rgb '#000000' title 'FP64 Peak (195 GFLOP/s)'
"
open roofline.png

方法三:用 Python 读 JSON 自动画图(最灵活,可叠加你的应用数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import json
import matplotlib.pyplot as plt
import numpy as np

# 1. 读 ERT 数据
with open('/Users/jsy/Downloads/Results.MyCPU/Run.001/roofline.json') as f:
d = json.load(f)

gflops_peak = d['empirical']['gflops']['data'][0][1] # 195.0
mem_levels = d['empirical']['gbytes']['data'] # [L1, L2, L3, DRAM]

# 2. 画 Roofline 图
plt.figure(figsize=(12, 8))

x = np.logspace(-2, 2, 500)

# 画各内存层级的带宽斜线
colors = ['#CC0000', '#DD5500', '#FF6600', '#0066CC']
labels = ['L1', 'L2', 'L3', 'DRAM']
for (name, bw), color in zip(mem_levels, colors):
y = np.minimum(bw * x, gflops_peak)
plt.loglog(x, y, color=color, linewidth=2, label=f'{name} ({bw} GB/s)')
# 标注转折点
ridge = gflops_peak / bw
plt.axvline(ridge, color=color, linestyle=':', alpha=0.4)

# 画计算峰值水平线
plt.axhline(gflops_peak, color='black', linewidth=2.5,
label=f'FP64 Peak ({gflops_peak} GFLOP/s)')

# 3. 标注你的机器信息
plt.xlabel('Arithmetic Intensity (FLOPs / Byte)', fontsize=13)
plt.ylabel('Performance (GFLOPs / sec)', fontsize=13)
plt.title('Empirical Roofline: controller01\nHygon C86 5380 × 2, 32 cores, DDR4-2933 × 4ch', fontsize=14)
plt.legend(loc='upper left', fontsize=11)
plt.grid(True, which='both', ls='--', alpha=0.4)
plt.xlim(0.01, 100)
plt.ylim(10, 2000)

# 4. 标注 DRAM 转折点
dram_bw = mem_levels[3][1] # 36.27
ridge_dram = gflops_peak / dram_bw
plt.annotate(f'DRAM turning point\n{ridge_dram:.1f} FLOP/Byte',
xy=(ridge_dram, gflops_peak),
xytext=(ridge_dram * 3, gflops_peak * 0.6),
arrowprops=dict(arrowstyle='->', color='blue'),
fontsize=10, color='blue')

plt.tight_layout()
plt.savefig('/Users/jsy/Downloads/Results.MyCPU/Run.001/roofline_matplotlib.png', dpi=150)
plt.show()

12. 理解 Roofline 模型——看图说话

Roofline 图只有两种线,全貌就清楚了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Performance (GFLOP/s)

│ ╱ ← 计算受限区(Arithmetic Intensity > turning point
195 ├────── 水平天花板 = FP64 Peak
│ ╲
│ ╲
│ ╲ ← 内存受限区(Arithmetic Intensity < turning point
│ ╲
│ ╲___ DRAM 斜线 (36.27 GB/s × AI)

└────────────────────────→
0.01 1 10 100
Arithmetic Intensity (FLOPs / Byte)
(对数坐标)

12.1 三条线 / 一个点的含义

元素 是什么 你的海光机器的值
水平天花板 CPU 纯浮点计算上限 195 GFLOP/s
DRAM 斜线 内存带宽能支撑的最大性能 36.27 × AI GFLOP/s
转折点 斜线和天花板的交点,AI 多大时从"内存瓶颈"切换为"计算瓶颈" 195 ÷ 36.27 ≈ 5.4 FLOP/Byte
数据点 你的应用实测的性能 你叠加上去的

12.2 怎么判断你的应用是内存瓶颈还是计算瓶颈

算法公式:你的 kernel 的"算术强度" = FLOPs 数 ÷ 搬运的字节数

场景 判断 优化方向
算术强度 < 5.4 FLOP/Byte 内存受限:性能被 DRAM 带宽卡住 优化访存模式、数据布局、利用缓存/寄存器;加内存通道
算术强度 > 5.4 FLOP/Byte 计算受限:性能被浮点峰值卡住 优化向量化(确保 AVX2 编译生效)、减少分支、用 FMA

12.3 以你的机器为例,几个典型 kernel 的位置

Kernel 类型 典型算术强度 在 Roofline 上的位置 瓶颈
向量加法 C[i]=A[i]+B[i] ~0.67 FLOP/Byte(3 数组各读一次,算 1 次) 斜线下方,很低的位置 内存
矩阵乘法(朴素三循环) ~2 FLOP/Byte 斜线下方 内存
矩阵乘法(分块缓存优化) 610 FLOP/Byte 刚过转折点,靠近天花板 轻内存,主要是计算
SAXPY(向量内积) ~1.5 FLOP/Byte 斜线下方 内存
密集线性代数(L3/L2 内完成) 501560 FLOP/Byte 天花板附近 计算

13. 逐线程数分析

FLOPS.001/ ~ FLOPS.016/ 分别对应 FLOP 强度 1 到 16 的测试。每个目录下有 OpenMP.0001/ ~ OpenMP.0016/(对应 1 到 16 线程,ERT 在你的配置里实际跑了 1,2,4,8,16 线程)。

13.1 max 文件格式详解

max 文件是该 FLOP 强度下,所有线程数、所有工作集大小中跑出的最大性能。每列含义:

内容 示例(FLOPS=1, 工作集=1MB)
1 FLOP 强度 1
2 工作集大小(字节) 1048576 (= 1 MB)
3-5 三次试验的 GFLOP/s 20648.323 20782.992 20814.359
6-8 三次试验的 FLOP/Byte(算术强度) 98.347 98.496 99.138
9-11 三次试验的内存带宽(GB/s) 6.147 6.156 6.197

关键理解:ERT 用一个"stream kernel"(类似 STREAM triad)来测带宽,同时改变 FLOP 强度来改变算术强度。同一个 kernel,FLOP 强度越高 → 每次迭代算的 FLOP 越多 → 算术强度越高 → 在 Roofline 图上越靠右上方。

13.2 sum 文件格式

sum 文件是该 FLOP 强度 + 线程数组合下,各级存储层级的峰值

1
2
3
4
5
6
7
8
   6.25 FP64 GFLOPs         ← 该配置下的峰值性能

1261.16 Weight ← 权重因子

99.86 L1 ← L1 缓存实测带宽
29.71 L2 ← L2 缓存实测带宽
22.91 L3 ← L3 缓存实测带宽
17.07 DRAM ← DRAM 实测带宽

Weight 是 ERT 内部的归一化因子,一般不用管。DRAM 行就是该线程数下的内存带宽——16 线程时约 17 GB/s,与 roofline.json 里汇总的 36.27 GB/s 的差异来自:sum 是单线程数配置的测量,roofline.json 取了最优线程配置。

13.3 快速对比各线程数

1
2
3
4
5
6
7
8
# 看 FLOP强度=1 时,1~16 线程的实测 GFLOP/s 和带宽
for t in 1 2 4 8 16; do
f="/Users/jsy/Downloads/Results.MyCPU/Run.001/FLOPS.001/OpenMP.$(printf '%04d' $t)/sum"
if [ -f "$f" ]; then
echo "=== 线程数=$t ==="
head -9 "$f"
fi
done

13.4 你的数据中观察到的现象

FLOPS.001/max 可以看到:

工作集大小 算术强度 (FLOP/Byte) 性能 (GFLOP/s) 所在的存储层级
1 KB ~ 256 KB ~17 ~123K L1/L2 内循环
1 MB ~ 64 MB 617 95K100K L3 缓存
128 MB ~ 1 GB 16 20K100K DRAM 访问
  • 小工作集(< 256 KB):数据全在 L1/L2,算术强度 ~17 FLOP/Byte,性能 ~123K GFLOP/s(计算受限)
  • 大工作集(> 128 MB):数据在 DRAM,算术强度降到 12 FLOP/Byte,性能跌到 ~20K GFLOP/s(内存受限)
  • 这就是 Roofline 图的完整曲线——左边低性能(带宽受限),右边高性能(计算受限),中间斜率就是内存带宽

14. 环境准备与工具获取

14.1 环境准备

ERT 需要编译和运行 C++ 微内核,确保 Ubuntu 20.04 上有:

1
2
3
4
5
6
7
8
9
10
11
12
# 基础编译工具
sudo apt-get update
sudo apt-get install -y build-essential gcc g++

# OpenMP(ERT 用多线程测并行扩展性)
sudo apt-get install -y libgomp1

# gnuplot(ERT 自动用它画 PostScript 图)
sudo apt-get install -y gnuplot

# 可选:Python 环境(如果你要用第 11 节方法三自己画图)
pip3 install matplotlib numpy

验证:

1
2
g++ --version
gnuplot --version

14.2 获取 ERT

ERT 官方仓库是 https://github.com/opencollab/ert,也可以用你课程提供的 fork:

1
2
3
4
5
6
7
# 方式一:课程仓库
git clone https://github.com/NewbotEra-baoyue/HighPerformanceComputing.git
cd HighPerformanceComputing/tool/roofline

# 方式二:官方仓库
git clone https://github.com/opencollab/ert.git
cd ert

目录结构预览:

1
2
3
4
5
6
7
Empirical_Roofline_Tool-1.1.0/
├── Config/
│ ├── config.mycpu.01 ← 你的配置文件(见下一节)
│ └── config.* ← 其他预设配置
├── src/ ← 微内核源码
├── ert ← 主执行脚本
└── README.md

15. 配置你的机器

ERT 通过配置文件告诉它:测几个线程、几种 FLOP 强度、内存多大等。

1
2
cd Empirical_Roofline_Tool-1.1.0/Config
ls config*

打开 config.mycpu.01,你会看到类似这样的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
ERT_RESULTS     Results.MyCPU
ERT_DRIVER driver1
ERT_KERNEL kernel1
ERT_MPI False
ERT_OPENMP True
ERT_FLOPS 1,2,4,8,16 ← 测 5 种 FLOP 强度(算术强度)
ERT_ALIGN 64
ERT_CC g++
ERT_CFLAGS -O3 -march=native -fstrict-aliasing -ftree-vectorize -funroll-loops
ERT_PROCS_THREADS 1,2,4,8,16,32,64 ← 测这些线程数
ERT_NUM_EXPERIMENTS 3 ← 每个配置跑 3 次取平均
ERT_MEMORY_MAX 1073741824 ← 最大工作集 1GB
ERT_PRECISION FP64 ← FP64(双精度),可改 FP32

关键参数说明

参数 含义 你的机器建议值
ERT_FLOPS 测几种 FLOP/迭代(即算术强度):1=纯内存拷贝, 16=重计算 保持 1,2,4,8,16
ERT_PROCS_THREADS 测几个线程/进程 保持 1,2,4,8,16,32,64
ERT_NUM_EXPERIMENTS 每个配置跑几次 保持 3(取中位数,剔除异常)
ERT_MEMORY_MAX 最大工作集(字节) 保持 1GB(足够覆盖 L3)
ERT_PRECISION 浮点精度 FP64(双精度)或 FP32(单精度)

如果你的机器是海光 C86 5380(32 核),ERT_PROCS_THREADS 设为 1,2,4,8,16,32 就够了(超线程的两个线程共享浮点单元,测到 64 线程意义不大)。


16. 运行 ERT

1
2
cd Empirical_Roofline_Tool-1.1.0
./ert Config/config.mycpu.01

ERT 会自动完成以下步骤:

  1. 编译:用 -O3 -march=native 编译出 5 个 FLOP 强度的微内核
  2. 逐个配置运行:5 种 FLOP 强度 × 7 种线程数 × 3 次重复 = 105 次运行
  3. 收集数据:每次运行记录 GFLOP/s、带宽、算术强度
  4. 汇总:取三次运行的中位数,写入 roofline.json
  5. 画图:调用 gnuplot 生成 roofline.ps

运行时间取决于机器性能,32 核的海光大约 5-15 分钟。


17. 与练习1 理论值对照

指标 第 3 节理论值 ERT 实测 利用率 差距原因
FP64 峰值 640 GFLOP/s 195 GFLOP/s ~30% 全核持续跑达不到睿频;Zen1 AVX2 重载降频;NUMA 跨节点损耗;功耗墙
DRAM 带宽 93.9 GB/s(4 通道 × 2933) 36.27 GB/s ~39% ERT 用流式访存模式(非最优);只测了 1 个 socket 的 16 线程;跨 NUMA 访问有损耗

30%~40% 的理论利用率对这台机器是正常的。海光 C86 5380(Zen1 架构)+ DDR4-2933 + 只插 4 根内存条,持续浮点/内存性能本来就到不了理论峰值。这正是 ERT 存在的意义——理论值画天花板,ERT 实测告诉你"实际上限"。

17.1 Roofline 图的四个关键数据

结合第 3 节(第 6.4-6.5 步)和 ERT 结果,你机器的 Roofline 参数:

参数 来源
计算峰值(水平线) 195 GFLOP/s ERT 实测
DRAM 带宽(最陡斜线) 36.27 GB/s ERT 实测
转折点 195 ÷ 36.27 ≈ 5.4 FLOP/Byte 计算得出
你的机器是 内存受限型(转折点低、带宽窄) 判断

17.2 如果插满 16 通道内存呢?

如果加内存到 16 通道 DDR4-2933:

指标 当前(4 通道) 插满(16 通道)
理论带宽 93.9 GB/s 375 GB/s
转折点 5.4 FLOP/Byte 21.6 FLOP/Byte
影响 大部分应用受 DRAM 限制 更多应用进入计算受限区

18. 常见问题

Q1:ERT 输出的 roofline.ps 在 Windows 上怎么打开?

PostScript 在 Windows 上不原生支持。方法:

  • 转 PDF:用 Ghostscript gswin64c -sDEVICE=pdfwrite -sOutputFile=roofline.pdf roofline.ps
  • 或直接看 roofline.json 里的数值
  • 或用第 11 节方法三(Python)自己画 PNG

Q2:ERT 测出来的峰值比 HPL/LINPACK 高/低,正常吗?

正常。ERT 用的是人工微内核,访存模式最优;HPL 是实际 Linpack,访存模式更复杂。ERT 峰值 ≈ HPL 的 1.3~2 倍是正常范围。

Q3:我的 roofline.json 里 FP64 峰值只有理论值的 30%,是不是有问题?

没问题。理论峰值是单核单线程的理想值(睿频、无开销、无 NUMA),ERT 测的是多核全载、持续运行的实测值。30-50% 是正常范围。

Q4:ERT 只测了 FP64,想看 FP32 怎么办?

改配置文件里的 ERT_PRECISION

1
ERT_PRECISION   FP32

然后重新运行 ./ert Config/config.mycpu.01

Q5:数据点为什么有的不在 Roofline 线上?

Roofline 线是"上限"。实测数据点理论上不该超过线。如果超过,可能是:

  • ERT 运行时间太短,数据还在缓存内,没到 DRAM
  • 编译器做了优于预期的优化
  • 数据点恰好落在 L2/L3 斜线区间,被更快的缓存层级支撑

Q6:海光机器的 ERT 结果和其他 Intel 机器差很多,正常吗?

正常。海光 C86 5380(Zen1 内核):

  • 每核只有 2 个 128 位 FMA(Intel 同代是 2 个 256 位),FP64 峰值只有一半
  • DDR4-2933(Intel 新机器用 DDR5-4800+),内存带宽也更低
  • 只插 4 根内存(16 通道只用了 4 个),带宽大打折扣

附录:你的海光 C86 5380 的完整结果速查

练习 1 理论值

指标 来源
核心数 32(2 插槽 × 16 核) lscpu
主频 2.5 GHz(boost disabled) lscpu
每核 FP64/周期 8(2×128 位 FMA) 微架构表
FP64 理论峰值 0.64 TFLOP/s 32×8×2.5e9
FP32 理论峰值 1.28 TFLOP/s 32×16×2.5e9
已插通道数 4 dmidecode
内存速率 2933 MT/s dmidecode
理论内存带宽 93.9 GB/s 4×2933e6×8
机器平衡(FP64) 0.15 Byte/FLOP 93.9e9÷640e9

练习 2 ERT 实测值

指标 来源
FP64 实测峰值 195 GFLOP/s roofline.json
L1 带宽 1560 GB/s roofline.json
L2 带宽 859 GB/s roofline.json
L3 带宽 52 GB/s roofline.json
DRAM 带宽 36.27 GB/s roofline.json
DRAM 转折点 5.4 FLOP/Byte 计算:195/36.27
机器类型 内存受限 判断
实测/理论峰值比 ~30% 195/640
实测/理论带宽比 ~39% 36.27/93.9

高性能计算训练营
https://jiangsanyin.github.io/2026/09/05/高性能计算训练营/
作者
sanyinjiang
发布于
2026年9月5日
许可协议