文章

NUMA 架构与亲和性调优

NUMA 架构与亲和性调优

概述

NUMA(Non-Uniform Memory Access)是多路 CPU 服务器的内存架构。每个 CPU Socket 拥有本地内存控制器和本地内存,访问本地内存快、远程内存慢。这个「快慢差异」对延迟敏感和带宽密集的工作负载(数据库、GPU 训练、DPDK)影响巨大。

一句话:NUMA 的本地/远程延迟差 = CPU 本地内存约 100ns,远程约 150-300ns。看起来不多,在 GPU 训练中每步卡 10μs 级联放大后就是 5-10% 吞吐差距。

拓扑分析

读取 NUMA 拓扑

# 方法 1:numactl
numactl --hardware
# available: 4 nodes (0-3)
# node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23    ← 物理核 + HT 兄弟
# node 0 size: 128000 MB      ← 本地 128GB
# node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
# node 1 size: 128000 MB
# ...
# node distances:
# node   0   1   2   3
#   0:  10  12  21  21    ← 10=本地, 12=同Socket不同die, 21=跨Socket
#   1:  12  10  21  21
#   2:  21  21  10  12
#   3:  21  21  12  10

# 方法 2:lstopo(更直观的拓扑图)
lstopo --no-io --no-legend
# 可输出 PNG 可视化拓扑

distance 矩阵解读

distance 值含义实例
10同 NUMA 节点(本地)基准延迟
12同一 Socket 的不同 die/CCD约 1.2x 延迟(AMD EPYC)
21跨 Socket约 2.1x 延迟
31-51跨机箱(4S/8S 服务器)不常见

distance 是 ACPI SLIT 表的标准化值,不是绝对纳秒,但比例关系准确。

内存分配分析

# 查看进程的内存分布在哪些 NUMA 节点
numastat -p $(pgrep -f kubelet)
#                            Node 0  Node 1  Node 2  Node 3  Total
#                   -------- ------- ------- ------- ------- -------
# Numa_Hit              1234567  987654  654321  321098 3197640
# Numa_Miss              12345  543210   98765   43210  697530  ← 20% 远程访问!
# Numa_Foreign            87321   65432   54321   43210  250284
# ...

# 查看每个 NUMA 节点的内存使用
numastat -m
# 显示 MemTotal, MemFree, MemUsed per node

如果 Numa_Miss 比例高(> 10%),说明进程频繁跨 NUMA 访问,这就是性能问题的信号

四种内存策略

NUMA 内存策略控制内核如何分配内存。通过 set_mempolicy() 系统调用或 numactl 设置。

策略numactl 参数行为适用场景
Default(默认)优先本地分配,失败后跨节点通用
Bind--membind=N只从指定节点分配,失败则 OOMGPU 训练、DPDK
Preferred--preferred=N优先从指定节点分配,失败后跨节点数据库、JVM
Interleave--interleave=0,N轮询分配,所有节点均匀分布大页面共享内存、tmpfs
# Bind:GPU0 训练进程只使用 NUMA0 内存
numactl --cpunodebind=0 --membind=0 python train.py

# Preferred:优先 NUMA0,但不拒绝远程
numactl --preferred=0 java -jar app.jar

# Interleave:所有 4 个 NUMA 节点均匀分布(适合被多 NUMA 进程共享的内存)
numactl --interleave=0,1,2,3 python train.py --use-shared-memory

zone_reclaim_mode 的陷阱

vm.zone_reclaim_mode 是 NUMA 调优中最容易被误解的参数:

行为结果
0禁用 zone reclaim。本地内存不够时,允许从远程 NUMA 分配可能远程访问多,但不会无故 reclaim
1启用。本地内存不够时,先回收本地缓存页,实在不够才跨 NUMA减少了远程访问,但 CPU 花费在 reclaim 上
2回收 + 回写脏页(更激进)
4回收 + 交换匿名页(最激进)

K8s 节点强烈建议设为 0。设为非 0 会导致:

  • 频繁 page reclaim → CPU sys 升高
  • 跨 NUMA 时本可用的远程内存被闲置
  • etcd、Redis 等延迟敏感应用受 reclaim 影响抖动
# 检查当前值
sysctl vm.zone_reclaim_mode
# 正确值:0

# 持久化
echo "vm.zone_reclaim_mode = 0" >> /etc/sysctl.d/99-numa.conf
sysctl -p /etc/sysctl.d/99-numa.conf

NUMA 自动平衡(auto-numa-balancing)

Linux 3.13+ 内核支持自动 NUMA 平衡:检测进程频繁访问远程内存时,自动将内存迁移到本地或进程迁移到数据所在 NUMA 节点。

参数说明
kernel.numa_balancing = 1启用自动平衡
kernel.numa_balancing_scan_delay_ms进程创建后多久开始扫描(默认 1000)
kernel.numa_balancing_scan_period_min_ms最小扫描间隔
kernel.numa_balancing_scan_period_max_ms最大扫描间隔
kernel.numa_balancing_scan_size_mb每次扫描的内存大小

K8s 场景建议

  • 多租户通用 K8s 节点:numa_balancing = 1(内核自动优化)
  • GPU 训练节点:numa_balancing = 0(由 numactl --membind 手动控制,避免自动迁移干扰 NCCL 通信)

GPU 训练场景的 NUMA 调优

为什么 GPU 训练对 NUMA 敏感

NUMA0                          NUMA1
  GPU0  GPU1                     GPU2  GPU3
  mlx5_0 (IB 网卡)               mlx5_1 (IB 网卡)
  CPU 0-7                        CPU 8-15
  RAM 128GB                      RAM 128GB

正确配置:GPU0 → mlx5_0(同 NUMA0),GPU2 → mlx5_1(同 NUMA1)
错误配置:GPU0 → mlx5_1(跨 NUMA),每次 NCCL AllReduce 都有 ~50% 远程访问

实操步骤

# 1. 查看 GPU-NUMA-网卡拓扑
nvidia-smi topo -m
#         GPU0    GPU1    GPU2    GPU3    mlx5_0  mlx5_1  CPU Affinity    NUMA Affinity
# GPU0     X      NV18    NV18    NV18    PXB     SYS     0-7,16-23       0
# GPU1    NV18     X      NV18    NV18    PXB     SYS     8-15,24-31      0
# GPU2    NV18    NV18     X      NV18    SYS     PXB     32-39,48-55     1
# GPU3    NV18    NV18    NV18     X      SYS     PXB     40-47,56-63     1
# mlx5_0  PXB     PXB     SYS     SYS      X      SYS
# mlx5_1  SYS     SYS     PXB     PXB     SYS      X
# 解释:PXB = 同 PCIe Switch(最优),SYS = 跨 NUMA(差)

# 2. GPU0 训练绑定同一 NUMA 节点
numactl --cpunodebind=0 --membind=0 \
  python -u train.py

# 3. 多卡训练:每个 rank 绑定自己的 NUMA
# rank 0 (GPU0, GPU1)
numactl --cpunodebind=0 --membind=0 \
  python -u -m torch.distributed.run --nproc_per_node=2 --master_addr=... train.py &

# rank 1 (GPU2, GPU3)
numactl --cpunodebind=1 --membind=1 \
  python -u -m torch.distributed.run --nproc_per_node=2 --master_addr=... train.py &

# 4. 设置 NCCL 使用本地 IB 设备
export NCCL_IB_HCA=mlx5_0,mlx5_1      # 指定 IB 设备
export NCCL_SOCKET_IFNAME=eth0
export NCCL_NET_GDR_LEVEL=5            # GPU Direct RDMA(需要 PXB 拓扑)

性能对比(典型场景)

配置MLPerf ResNet-50 吞吐NCCL AllReduce 带宽
GPU 和网卡跨 NUMA基准(3800 img/s)基准(18 GB/s)
GPU 和网卡同 NUMA+8% ~ +12%+25% ~ +40%
GPU 和网卡同 NUMA + mem bind+10% ~ +15%+30% ~ +45%

NUMA 故障排查

检测远程内存访问比例

# per-node 内存访问计数
perf stat -e 'node-loads,node-load-misses,node-stores,node-store-misses' \
  -p $(pgrep -f kubelet) -- sleep 10

# 计算远程访问比例
# node-load-misses / node-loads × 100%
# > 10% 说明存在问题

常见 NUMA 问题排查

症状可能原因排查方法
应用性能波动大,有时快有时慢某些批次的内存分配跨 NUMAnumastat -p <pid> 观察 miss 趋势
GPU 训练 NCCL 带宽不稳IB 网卡和 GPU 不在同一 NUMAnvidia-smi topo -m 检查 PXB/SYS
节点内存使用不平衡(一个节点满,另一个空)zone_reclaim_mode ≠ 0sysctl vm.zone_reclaim_mode
特定 Pod OOM 但节点整体内存充足membind 策略锁定在内存不足的 NUMA 节点检查 Pod 是否有 NUMA 亲和性注解

关联知识

参考资源

学习时间

阶段时间备注
架构理解2026-06-30完成:拓扑分析、四种策略、GPU 亲和性实战、故障排查

状态: 🌱 学习中 下次复习日期: 2026-07-07