NUMA:你的内存不是一整块:看懂 NUMA 与机器拓扑

发布时间:2026/7/30 13:02:18
NUMA:你的内存不是一整块:看懂 NUMA 与机器拓扑 01 — 你的内存不是一整块看懂 NUMA 与机器拓扑你以为的内存是一大片连续、访问一样快的空间真相是它被切成了几座“内存孤岛”放错岛程序就慢一截。这一章带你把这些岛看清楚。1. 为什么会有 NUMA1.1 先厘清术语CPU、核、线程不是一回事聊 NUMA 之前必须先分清几个天天混用、其实不同的词。它们是层层包含的关系术语别名是什么类比物理 CPU / socketprocessor package、插槽主板上一个 CPU 插槽里插着的那一整颗芯片。lscpu里的Socket(s)就是数它。一栋楼核core物理核一颗 CPU 内部能独立执行指令的计算单元。一颗现代 CPU 里有很多核。楼里的一层硬件线程thread逻辑核、SMT、超线程一个核靠 SMTSimultaneous Multi-Threading同时多线程技术同时跑 2 条指令流对操作系统就显示成 2 个 “CPU”。一层里的两个工位用本机的lscpu输出对号入座Socket(s): 1 ← 1 颗物理 CPU1 个插槽 Core(s) per socket: 12 ← 这颗 CPU 里有 12 个物理核 Thread(s) per core: 2 ← 每个核开了 2 个硬件线程SMT CPU(s): 24 ← 12 核 × 2 操作系统看到的 24 个“逻辑 CPU”关键结论Linux以及top、/proc/cpuinfo、taskset里说的一个 “CPU”通常指的是最小的那个逻辑单元——硬件线程而不是整颗芯片。所以 “24 个 CPU” 其实是 “1 颗物理 CPU、12 个核、24 个硬件线程”。本系列后文为了顺口也会把逻辑单元叫 “CPU”和内核代码里的cpu一致但你心里要清楚它指的是哪一层。1.1.1 深入一点SMT超线程到底做了什么SMT 常被误解成 “一个核变成两个核”其实完全不是。一个物理核里真正做计算的部件ALU、FPU、乘加单元等统称执行单元只有一套SMT 复制的只是保存线程状态的那部分——寄存器、程序计数器、流水线的取指/发射入口等。于是一个物理核可以同时“记住”两条指令流的现场操作系统就把它看成 2 个逻辑 CPUIntel 叫 Hyper-Threading是 SMT 的商标名AMD 直接叫 SMT。为什么这样能提速因为单条指令流几乎不可能把执行单元喂满一旦遇到 cache miss要等内存返回数据动辄几百个周期、分支预测失败、或指令间存在依赖执行单元就只能空转。SMT 的思路是——当一条线程卡住时核立刻切去执行另一条线程的指令用第二条线程的工作填补这些“气泡”把原本浪费的执行单元利用起来。线程 A 的现场(寄存器/PC)发射 / 调度线程 B 的现场(寄存器/PC)共享的执行单元ALU / FPU (只有一套)共享 L1/L2 Cache由此得到几条对性能和 NUMA 都重要的结论两个硬件线程是“兄弟”不是“两个核”。它们共享同一套执行单元、同一份 L1/L2 cache。如果两条线程都是重计算比如都在跑满 FPU 的矩阵乘它们会互相抢执行单元SMT 带来的收益很小甚至因为 cache 被两份数据挤占而变慢。SMT 的收益高度依赖负载类型。访存密集、经常 stall 的负载等内存的时间多收益明显通常有 10%~30%而计算密集、本身就把执行单元喂满的 HPC / GPU 配套负载收益很小——所以很多 HPC 和数据库场景干脆在 BIOS 里关掉 SMT或用taskset把关键线程绑到各自独立的物理核上避免兄弟线程互相干扰。和 NUMA 的联系兄弟线程共享 cache 是“核内”的一层非均匀而 NUMA 的 node 间是“核外”的另一层非均匀。做绑核affinity时这两层要一起考虑既要让线程落在离数据近的 node也要决定同一物理核的两个兄弟线程是分开用还是合起来用。怎么认出兄弟线程/sys/devices/system/cpu/cpuN/topology/thread_siblings_list会列出与cpuN共享物理核的所有逻辑 CPU。例如0,12表示逻辑 CPU 0 和 12 是同一个物理核的两个 SMT 线程本机 12 核 24 线程典型编号就是k与k12配对。一句话记住核core决定“算得多快”SMT 线程决定“把算力空隙填多满”。加线程不等于加算力只是让已有算力少闲着。1.1.2 辨析硬件线程 ≠ 软件进程里的线程这里的“线程”和你写代码时pthread_create/ Java / Go 里的线程不是同一个东西只是撞了名字。区别一句话概括软件线程是操作系统的抽象硬件线程是芯片里的物理资源前者被调度到后者上运行。软件线程OS thread硬件线程SMT thread / 逻辑 CPU是什么一条可被内核调度的执行流寄存器现场 栈 调度状态物理核里那份被复制的寄存器/PC 现场入口数量几乎不限可开成百上千固定 物理核数 × 每核 SMT 宽度本机 12 × 2 24谁管理内核调度器硬件CPU 微架构类比排队等着干活的人很多工位本机 24 个两者是多对少的映射内核调度器把成百上千的软件线程分时复用到有限的硬件线程上。任一瞬间一个硬件线程最多跑一个软件线程SMT 的意义在于让一个物理核能同时真正并行跑 2 个软件线程它有 2 份现场再多就得靠内核做上下文切换轮流上。落到命令上就很清楚taskset -c 0,12 ./app里的0,12指的是硬件线程逻辑 CPU编号——你在决定“把软件线程绑到哪两个工位”而top里某进程显示的几百个 thread 是软件线程它们仍然在抢这 24 个硬件线程。后文以及内核代码、taskset、NUMA 绑核凡说 “CPU”几乎都指硬件线程这一层。1.2 从 UMA 说起有了上面的术语再看内存怎么接。早期多核系统是UMAUniform Memory Access统一内存访问不管有几个核、几个 socket所有核都通过同一条总线 / 同一个内存控制器访问同一片内存因此任意核访问任意地址的延迟都一样Uniform 统一。核 0共享内存控制器核 1核 2核 3同一片物理内存问题在于核越多或 socket 越多这条共享总线/控制器就越是瓶颈。所有核抢同一个内存控制器带宽被均摊、争用加剧加核也换不来性能扩展性很差。这就逼出了 NUMA。1.3 NUMA 的思路NUMANon-Uniform Memory Access非统一内存访问把 “CPU 内存控制器 一片内存” 打包成一个nodenode 之间用高速互连AMD 的 Infinity Fabric、Intel 的 UPI/QPI连起来。每个 CPU 都有一片 “离自己最近” 的本地内存。Infinity Fabricnode1CPU2内存控制器 MC1CPU3本地内存 1node0CPU0内存控制器 MC0CPU1本地内存 0代价是访问不再统一。本地访问localCPU 访问自己 node 的内存 —— 快。远程访问remoteCPU 访问别的 node 的内存 —— 要过互连慢。这就是名字里 “Non-Uniform” 的来源也是所有 NUMA 优化的核心矛盾尽量让计算访问本地内存避免跨 node。回到 1.1 的术语NUMA 分组分的是物理层级——一个 node 通常对应 “一颗或半颗物理 CPU 它的内存”。所以 “哪个 CPU 离哪块内存近” 这句话里的 CPU指的是物理核 / 物理 CPU而不是超线程。1.4 核心概念一览NUMA 的基本思路讲清楚了把前面散落的术语集中收成一张表后文会反复用到务必搞清楚这些概念。概念含义nodeNUMA 的基本单位一组 CPU 一个内存控制器 一片本地内存。内核用nidnode id标识从 0 开始。socket物理 CPU 插槽。一个 socket 可能是一个 node也可能被切成多个 node见 NPS。local memory当前 CPU 所属 node 的内存访问最快。remote memory其他 node 的内存访问要经过 node 间互连延迟更高、带宽更低。distance内核给出的 node 间 “相对距离”。本地固定为10远程通常20、32等数值越大越远。first-touchLinux 默认分配策略一个页在第一次被写入时才在 “执行这次写入的 CPU 所在 node” 上真正分配物理页。interleave把内存轮流分散到多个 node用于平摊带宽、避免单 node 热点。1.5 “多 CPU” 到底指哪些场景知道了 node 是什么再回头看 “多 CPU” 这个含糊的说法。它实际有三种完全不同的物理形态对 node 的划分影响也不同单 socket 多核最常见就是本机、绝大多数台式机/笔记本一个插槽、一颗芯片、内部很多核共享同一套内存控制器。通常只有 1 个 NUMA node不存在跨 node 问题。多 socket 服务器双路 / 四路 EPYC、Xeon主板上有 2 个甚至 4 个物理插槽每个插槽一颗 CPU、各自带一组内存条。每个 socket 至少是一个 node跨 socket 访问内存就是远程访问 —— 经典 NUMA 场景。单 socket 内部再切分AMD chiplet NPS一颗 EPYC 芯片内部由多个小芯片chiplet拼成BIOS 可以把这一颗 CPU 再切成 2/4 个 node见 3.1 节 NPS。这时一颗物理 CPU 里就有多个 NUMA node。2. NUMA 的性能特征与陷阱有了概念再看两个直接影响性能、也最容易踩坑的点node 间访问的非对称以及决定页落在哪个 node 的 first-touch。2.1 延迟/带宽的非对称直观数量级具体数字随平台不同但量级关系普遍成立访问类型相对延迟相对带宽本地 node1×基准高基准相邻 node1 跳~1.5×–2×明显下降远端 node多跳2×以上更低为什么会有这种非对称看一次访问在硬件里走的路本地访问只需 “核 → 本 node 内存控制器 → DRAM”而跨 node 访问要先 “核 → 本 node 内存控制器”发现目标不在本地再经 node 间互连Infinity Fabric / QPI-UPI绕到对端 node 的内存控制器最后才读到 DRAM然后数据再原路返回。多出来的这段互连往返就是延迟变大、带宽变低的根源。node1远端node0本地② 跨 node 互连Infinity Fabric / UPI① 本地访问一跳到底③ 数据再原路绕回 CPU0核 CPU0内存控制器0DRAM0内存控制器1DRAM1图里 CPU0 访问DRAM0路径①只在本 node 内一跳访问DRAM1路径②③则要跨过 node 间互连、绕到 node1 的内存控制器再回来——多的这一段就是 remote 比 local 慢的全部原因。对于内存带宽敏感的 GPU / HPC 负载页放错 node 可能直接让一段 kernel 慢 30%~50%所以 “迁到哪个 node” 不是学术问题而是实打实的性能问题。2.2 first-touch最容易踩的坑Linux 默认策略是 first-touch很多人以为malloc就分配了物理内存其实不然char*pmalloc(130);// 只是保留了虚拟地址还没有物理页memset(p,0,130);// 现在才真正分配 —— 分配在“执行 memset 的 CPU”的 node 上推论谁先写页就落在谁的 node 上。常见反模式是 “主线程初始化一大块内存然后分发给各 node 的工作线程去用” —— 结果所有页都落在主线程那个 node其他 node 全在做远程访问。正确做法是让每个工作线程各自 first-touch 自己负责的那部分。3. AMD 平台的 NUMA 拓扑3.1 chiplet 与 NPS概览现代 AMD CPUEPYC / 部分 Ryzen是chiplet架构装核的CCD 中央IOD集成多个内存控制器 UMC基本每个 DDR 通道一个两者靠片内Infinity Fabric相连。NPSNodes Per Socket是 BIOS 设置本质是把 IOD 上的内存通道UMC分成几组从而把一颗 socket 暴露成 1/2/4 个 NUMA node设置含义典型场景NPS1整个 socket 1 个 node内存跨所有通道 interleave通用、对 NUMA 不敏感的负载NPS2一个 socket 切成 2 个 node折中NPS4一个 socket 切成 4 个 node每个 node 绑定一部分内存通道带宽敏感的 HPC/GPU 负载本地性最好对我们的 SVM 迁移意义重大在 NPS4 多 GPU 的机器上每块 GPU 都有一个 “物理上最近” 的 CPU node。把 GPU 页迁回 CPU 时理想目标就是这个最近 node而不是随便一个。想深入了解 chiplet / UMC / Infinity Fabric 的物理结构、NPS1 vs NPS4 的示意图以及 “一个核访存到底怎么走、为什么每次访存都要过 Infinity Fabric” —— 见专题深入 · AMD 芯片是“拼”出来的chiplet、Infinity Fabric 与 NPS。3.2 GPU 也是拓扑里的一个 “node”从系统/内核视角看GPU尤其带自己显存的独显在拓扑里也被当作一种内存 nodeLinux 会为它建立到各 CPU node 的距离信息ROCm 的 topologyKFD里每块 GPU 记录了它 “最近的 CPU NUMA node”HMM/SVM 迁移本质就是在这些 node 之间搬页。所以后面章节里 “选目标 CPU node”其实就是 “给定发起迁移的 GPU找到与它 distance 最小的 CPU node”。4. 查看真实拓扑下面用本项目的一台开发机做示例。注意这台机器只有 1 个 NUMA node单路 Ryzen所以它本身不会暴露跨 node 问题——但正好用来认识各个工具的输出格式随后再给出多 node 机器上这些命令 “应该长什么样”。4.1 lscpu$ lscpu | grep -E Model name|Socket|Core|Thread|NUMA Model name: AMD Ryzen 9 9900X 12-Core Processor Thread(s) per core: 2 Core(s) per socket: 12 Socket(s): 1 NUMA node(s): 1 NUMA node0 CPU(s): 0-23解读1 个 socket、12 核 24 线程、1 个 NUMA nodeCPU 0–23 全属于 node0。多 node 机器例如双路 EPYCNPS1会是这样NUMA node(s): 2 NUMA node0 CPU(s): 0-63 NUMA node1 CPU(s): 64-1274.2 numactl --hardwarenumactl -H需安装numactl包Debian/Ubuntuapt install numactl是查看 NUMA 最直观的工具。本机没装多 node 机器上的典型输出如下$ numactl --hardware available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 ... 63 node 0 size: 257655 MB node 0 free: 200341 MB node 1 cpus: 64 65 66 ... 127 node 1 size: 258043 MB node 1 free: 210998 MB node distances: node 0 1 0: 10 32 -- node0→node010(本地), node0→node132(远程) 1: 32 10关键看两处每个 node 的cpus和size内存容量node distances矩阵对角线是本地10非对角线是跨 node 的相对代价。4.3 sysfs/sys/devices/system/node/不依赖任何工具直接读内核导出的信息脚本里最常用$ ls /sys/devices/system/node/ | grep node node0 $ cat /sys/devices/system/node/node0/distance 10 $ grep -E MemTotal|MemFree /sys/devices/system/node/node0/meminfo Node 0 MemTotal: 48445768 kB Node 0 MemFree: 2633528 kBnodeN/distance该 node 到所有 node 的距离向量。本机只有一行10只有自己。多 node 机器上node0/distance会是类似10 32。nodeN/cpulist属于该 node 的 CPU 列表。nodeN/meminfo该 node 的内存用量。常用换算每个 CPU 属于哪个 node可以从/sys/devices/system/cpu/cpuN/node*或numa_node_of_cpu()下一章得到。4.4 numastat按 node 的内存统计numastat在numactl包里看系统或某进程在各 node 上的内存分布$ numastat # 系统级 $ numastat -p pid # 某进程在各 node 上的页分布验证迁移落点时最常用多 node 机器上numastat -p能直接告诉你 “这个进程的内存有多少在 node0、多少在 node1”这正是第 08 章验证 SVM prefetch 落点要用的手段。4.5/proc/pid/numa_maps逐段查看页分布最细粒度的观察方式按 VMA 列出每段内存的页落在哪个 node$ cat /proc/self/numa_maps 00400000 default file/usr/bin/cat mapped6 N06 ... ^^^ 这段内存被映射到 node0 的 6 个页N06表示 node0 上有 6 个页多 node 时会看到N0.. N1..同时出现。这是确认 “页到底落在哪个 node” 的终极依据。5. 本章小结NUMA 把 “CPU 内存控制器 内存” 分成多个 node本地访问快、远程访问慢。distance矩阵量化了 node 间的远近本地恒为10。Linux 默认 first-touch页落在 “第一次写它的 CPU” 所在 node —— 谁写谁得。AMD 平台通过 NPS 决定一个 socket 暴露几个 nodeGPU 在拓扑里也有 “最近的 CPU node”。查看工具lscpu/numactl -H/numastat//sys/devices/system/node///proc/pid/numa_maps。

相关新闻

最新新闻

日新闻

周新闻

月新闻