多核与 NUMA 架构 —— 从核到多路服务器

第七篇讲清了一个核怎么变快,本篇讲多个核怎么协作。从单芯片多核(CMP)到多路服务器(多 socket),从 Ring Bus 到 Mesh,从 SMP 到 NUMA——多核架构的演进是过去二十年服务器 CPU 最重要的变化。

为什么要多核

2004 年前后 CPU 频率撞上功耗墙

  • 频率每翻倍,动态功耗约翻 8 倍(与频率立方近似成正比)
  • Pentium 4 在 3.8 GHz 之后就推不动了

往多核走是必然选择:

1
2
3
1 核 4 GHz   功耗 100W   性能基准 1.0
2 核 3 GHz 功耗 100W 并行性能 1.5
4 核 2.5 GHz 功耗 100W 并行性能 2.5

核数翻倍但每核降频降压,整体功耗持平,并行性能更好。这是 Intel Core 2 Duo(2006)开启多核时代的物理原因

核间互联拓扑

多核要协作,就需要”芯片内的网络”。主流拓扑三种:

1. 总线 / Crossbar(早期)

graph TB
  C1[Core 1] --- BUS
  C2[Core 2] --- BUS
  C3[Core 3] --- BUS
  C4[Core 4] --- BUS
  BUS[共享总线] --- L3
  BUS --- MC[内存控制器]

简单,但核数越多冲突越严重。只适合 2-4 核

2. Ring Bus(环形总线)

Intel 在 Sandy Bridge 到 Broadwell(2011-2015)的 server CPU 用环形总线:

graph LR
  C1[Core 1] --> C2[Core 2] --> C3[Core 3] --> C4[Core 4]
  C4 --> L3a[L3 slice]
  L3a --> MC[MC]
  MC --> C1

每个核挂一个”站点”,数据在环上传递。两个方向都跑,平均跳数是 N/4。适合 8-16 核,再多就跳数过大、延迟高。

3. Mesh(网格)

Intel 从 Skylake-SP(2017)切换到二维 Mesh:

graph TB
  C00[Core] --- C01[Core] --- C02[Core] --- C03[Core]
  C00 --- C10[Core] --- C20[Core] --- C30[Core]
  C01 --- C11[Core] --- C21[Core] --- C31[Core]
  C02 --- C12[Core] --- C22[Core] --- C32[Core]
  C03 --- C13[Core] --- C23[Core] --- C33[Core]

每个 tile 上挂一个核 + 一个 L3 slice + 部分 IO,可双向传递。适合 28-128 核——AMD Chiplet 内 CCD 也是类似思路。

Chiplet 时代的”两级网络”

AMD Zen 2 起把网络切成两级:

graph TB
  subgraph CCD0["CCD 0 (8 核)"]
    direction LR
    C00[Core] --- L30[L3 slice]
  end
  subgraph CCD1["CCD 1 (8 核)"]
    direction LR
    C10[Core] --- L31[L3 slice]
  end
  IOD[IO Die
内存控制器 + Infinity Fabric Switch] CCD0 -- IF Link --> IOD CCD1 -- IF Link --> IOD
  • CCD 内:核 + L3 共享,延迟很低
  • CCD 间:必须经过 IO Die 和 Infinity Fabric

这导致 AMD CPU 内部存在”伪 NUMA“——同一物理 socket 内不同 CCD 之间访问 L3 也有延迟差异。BIOS 里的 NPS 设置(NUMA Per Socket)就是为此而来。

SMP vs NUMA

SMP:对称多处理

graph TB
  C1[CPU] --- BUS[共享总线]
  C2[CPU] --- BUS
  BUS --- MEM[(单一共享内存)]

所有 CPU 访问内存的延迟一致。简单优雅,但扩展性差——总线带宽很快被多核压死。

NUMA:非一致访存

每个 CPU 有自己的”本地内存”,访问本地快,跨节点慢:

graph TB
  subgraph N0["NUMA Node 0"]
    C0[CPU 0]
    M0[(内存 0)]
    C0 -- 本地, ~80ns --> M0
  end
  subgraph N1["NUMA Node 1"]
    C1[CPU 1]
    M1[(内存 1)]
    C1 -- 本地, ~80ns --> M1
  end
  C0 <-- UPI/IF
~140ns --> C1
访问类型 典型延迟(DDR5 系统)
本地 NUMA ~80 ns
跨 NUMA ~140-200 ns
跨 socket(UPI) ~140-200 ns
跨 CCD(同 socket) ~100-130 ns

NUMA 是”牺牲一致性换扩展性“——多 socket 服务器的必然选择。

NUMA 在多 socket 服务器里的样子

一台双路 EPYC 9754(Bergamo)服务器:

graph TB
  subgraph SK0["Socket 0"]
    direction TB
    M0L[(本地内存 768GB)]
    CPU0[128 核]
    M0L --- CPU0
  end
  subgraph SK1["Socket 1"]
    direction TB
    M1L[(本地内存 768GB)]
    CPU1[128 核]
    M1L --- CPU1
  end
  CPU0 <-- "Infinity Fabric
4 链路 / 76.8 GB/s 单向" --> CPU1

如果 BIOS 设 NPS=4(每 socket 切 4 个 NUMA 节点),整机就有 8 个 NUMA 节点。

查看 NUMA 拓扑

Linux 上用 numactllscpu

1
2
3
4
numactl --hardware           # 列出所有 NUMA 节点和距离矩阵
lscpu | grep NUMA # 简要 NUMA 信息
numastat # NUMA 内存分配统计
hwloc-ls # 详细拓扑(核 / 缓存 / 内存)

numactl --hardware 的关键输出之一是距离矩阵

1
2
3
4
node distances:
node 0 1
0: 10 21
1: 21 10

10 = 本地访问基准,21 = 跨 NUMA 大约慢 2.1 倍。具体数字硬件平台不同。

NUMA 优化:软件视角

应用要适应 NUMA,否则可能跨 NUMA 频繁访问导致严重性能损失。

内存绑定

1
2
3
4
5
# 把进程绑到 NUMA node 0,并只用 node 0 的内存
numactl --cpunodebind=0 --membind=0 ./app

# 跨 NUMA 平均分配内存
numactl --interleave=all ./app

数据库调优

PostgreSQL、MySQL、Redis 都建议:

  • 关掉 NUMA balancing,避免频繁迁移内存页
  • numactl --interleave=all 启动,避免单 NUMA 内存爆炸
  • Linux 5.x 起的 vm.zone_reclaim_mode 默认值已较合理

大内存 NUMA 大坑

跑过这种代码的人都知道这个坑:

1
2
3
4
5
# 单线程申请 200GB 内存,全部分配到 node 0
big_array = malloc(200 * GB)
# 然后在 64 核上并行处理它
parallel_for(big_array, 64_threads)
# → 32 个跨 NUMA 的核访问 node 0,性能折半

正确做法是在每个 NUMA 节点本地申请自己处理的那部分内存——这就是 NUMA-aware 编程。

多 socket 互联:UPI 与 Infinity Fabric

厂商 互联协议 当前规格 链路数(双路)
Intel Skylake-Cascade UPI 1.0 10.4 GT/s 2-3
Intel Ice Lake UPI 1.0+ 11.2 GT/s 2-3
Intel SPR/EMR UPI 2.0 16 GT/s 4
Intel Granite Rapids UPI 2.0+ 24 GT/s 6
AMD Naples Infinity Fabric ~37 GB/s 4
AMD Rome/Milan IF ~64 GB/s 4
AMD Genoa/Turin IF Gen 4 ~76.8 GB/s 单向 4

待补充:Genoa/Turin 的精确 IF 速率以及 8 路场景的 UPI 拓扑(Sapphire Rapids 8P)。

UPI/IF 链路数越多,跨 socket 的总带宽越高、性能损失越小。双路场景下,4 链路是当前主流

4 路 / 8 路服务器的 UPI 拓扑

多路服务器的 UPI 拓扑有讲究——不是所有 socket 都直连。

4 路全互联(典型 SPR/EMR)

graph TB
  CPU0 --- CPU1
  CPU0 --- CPU2
  CPU0 --- CPU3
  CPU1 --- CPU2
  CPU1 --- CPU3
  CPU2 --- CPU3

每两颗 CPU 都直连,跨 socket 一跳到位。

8 路 Twisted Hypercube(Cooper Lake/Cascade)

1
2
8 颗 CPU,每颗仅 3 个 UPI 链路。
最远跨 2 跳。

这种拓扑下”距离矩阵“会出现 10/16/24 三档——跨节点延迟差别可达 2.4 倍。BIOS 里的”snoop modes”调优就是为这个场景准备的。

“巨型机柜”也是 NUMA

NVIDIA GB200 NVL72 整机柜把 72 颗 GPU 放在同一个 NVLink Domain 里,可以”像一颗大 GPU”地编程。但每颗 GPU 之间的 NVLink 距离/延迟仍有差异——这本质上也是 NUMA,只是把概念扩展到了 GPU 之间。

未来 CXL 3.x 把”内存池”概念引入数据中心,会带来”机柜级 NUMA“——一颗 CPU 既能访问本地内存,也能访问机柜里某台机器的远端内存,延迟分级。

一张总结

graph TB
  L1[单核:流水线/超标量]
  L2[多核共享 L3:Ring/Mesh]
  L3[Chiplet 内的"伪 NUMA"]
  L4[多 socket NUMA:UPI/IF]
  L5[整机柜 GPU 域:NVLink]
  L6[CXL 内存池:机柜级 NUMA]
  L1 --> L2 --> L3 --> L4 --> L5 --> L6

核间通信延迟随层级递增几乎是数量级

1
2
3
4
5
6
7
8
L1 → L2:  3-5 周期
L2 → L3: 10-15 周期
L3 → DRAM (本地): ~200 周期
DRAM (跨 NUMA): ~400 周期
跨 socket UPI: ~500 周期
NVLink GPU 间: ~1000 周期
CXL 内存池: ~2000 周期
跨主机 RDMA: ~5000 周期

每跨一层,应用都需要新的”NUMA-aware”编程方式。这是分布式系统设计的根本约束。

小结

  • 多核是 2004 年功耗墙之后的必然选择
  • 核间互联从 Ring → Mesh,Chiplet 时代演化为两级网络
  • NUMA = 牺牲一致性换扩展性,多 socket 必然 NUMA
  • UPI 链路数和 IF 链路数决定了跨 socket 性能
  • “NUMA”概念正在从 socket 间扩展到 GPU 间、CXL 池间——本质上是同一个问题

下一篇是本章倒数第二篇——CPU 的命名规则和选型方法。