大模型在多核CPU上的推理优化:线程亲和性与NUMA感知
一台 128 核的服务器,跑大模型推理的吞吐量却不如 32 核机器——这种情况在实际工程中并不罕见。根本原因往往不是核数不够,而是线程之间的"沟通成本"太高,以及内存访问路径不对。
本篇聚焦两个关键优化方向:线程亲和性(Thread Affinity) 和 NUMA 感知(NUMA-Aware),解释它们的原理,并提供可直接使用的配置方案。
1 多核 CPU 的内存访问模型
1.1 UMA vs NUMA
现代高核数服务器处理器几乎都是 NUMA 架构:
NUMA 节点 0 NUMA 节点 1
┌─────────────────────┐ ┌─────────────────────┐
│ CPU Core 0-31 │ │ CPU Core 32-63 │
│ L3 Cache (共享) │ │ L3 Cache (共享) │
│ 内存控制器 │◄──────►│ 内存控制器 │
│ 本地 DRAM │ │ 本地 DRAM │
└─────────────────────┘ └─────────────────────┘
- 本地内存访问:Core 0 访问 NUMA 节点 0 的内存 → 低延迟
- 远端内存访问:Core 0 访问 NUMA 节点 1 的内存 → 需跨 NUMA 互联,延迟高约 30-50%
1.2 大模型推理的内存访问特征
以 7B 模型(FP16,约 14GB)为例:
- 模型权重在内存中占据连续大块空间
- 每次推理都需要将权重从内存加载到 CPU 缓存
- 若线程分散在多个 NUMA 节点,权重可能被分散在不同 NUMA 节点的内存上,产生大量远端内存访问
2 查看系统 NUMA 拓扑
在优化之前,先了解机器的 NUMA 布局:
# 查看 NUMA 节点数量和每个节点的 CPU 分布
numactl --hardware
# 示例输出:
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 3 ... 31
# node 0 size: 128000 MB
# node 1 cpus: 32 33 34 ... 63
# node 1 size: 128000 MB
# node distances:
# node 0 1
# 0: 10 20
# 1: 20 10
# 查看详细 CPU 拓扑(核/线程/NUMA)
lscpu --extended
# 查看当前进程的 NUMA 绑定情况
numastat -p $(pgrep python)
3 NUMA 感知推理部署
3.1 numactl 绑定
最简单的方式:用 numactl 将推理进程绑定到单个 NUMA 节点:
# 将推理进程绑定到 NUMA 节点 0,内存也从节点 0 分配
numactl --cpunodebind=0 --membind=0 python inference_server.py
# 多实例部署:节点 0 和节点 1 各跑一个实例
numactl --cpunodebind=0 --membind=0 python inference_server.py --port 8080 &
numactl --cpunodebind=1 --membind=1 python inference_server.py --port 8081 &
3.2 Python 中的 NUMA 感知内存分配
import os
import ctypes
def set_numa_policy(node: int):
"""将当前进程的内存分配策略设置为绑定到指定 NUMA 节点"""
# MPOL_BIND = 2,表示严格绑定到指定节点
libnuma = ctypes.CDLL("libnuma.so.1")
nodemask = ctypes.c_ulong(1 << node) # 绑定到 node 节点
libnuma.set_mempolicy(2, ctypes.byref(nodemask), 64)
# 在模型加载前调用
set_numa_policy(node=0)
import torch
model = torch.load("model.pt") # 权重将分配在 NUMA 节点 0 的内存上
4 线程亲和性优化
4.1 什么是线程亲和性
线程亲和性(Thread Affinity / CPU Pinning)是指将线程固定到特定的 CPU 核心上运行,避免操作系统调度器在核心间迁移线程。
为什么重要:线程迁移会导致该线程的 L1/L2 缓存数据失效,迁移后需要重新预热缓存,这对推理性能影响可达 10-20%。
4.2 OpenMP 线程亲和性配置
llama.cpp、PyTorch 等框架使用 OpenMP 进行多线程并行,通过环境变量控制线程亲和性:
# OMP_PROC_BIND: 线程绑定策略
# close - 线程尽量靠近主线程(适合数据局部性优化)
# spread - 线程均匀分散(适合独立并行任务)
# master - 所有线程绑定到主线程所在核心
export OMP_PROC_BIND=close
export OMP_PLACES=cores # 以物理核为单位(不包含超线程)
export OMP_NUM_THREADS=32 # 线程数 = NUMA 节点 0 的物理核数
# 验证绑定效果
OMP_DISPLAY_ENV=TRUE python -c "import torch; print(torch.get_num_threads())"
4.3 taskset 手动绑定
# 将 Python 进程绑定到 CPU 0-31(NUMA 节点 0 的物理核)
taskset -c 0-31 python inference_server.py
# 查看进程的 CPU 亲和性掩码
taskset -p $(pgrep python)
# 组合 NUMA 绑定和 CPU 亲和性绑定
numactl --membind=0 taskset -c 0-31 python inference_server.py
4.4 Python 代码中设置亲和性
import os
import psutil
def pin_to_cores(core_list: list[int]):
"""将当前进程绑定到指定 CPU 核心"""
p = psutil.Process(os.getpid())
p.cpu_affinity(core_list)
print(f"Process {os.getpid()} pinned to cores: {p.cpu_affinity()}")
# 推理进程绑定到 NUMA 节点 0 的核心(0-31)
pin_to_cores(list(range(32)))
5 超线程(HyperThreading)对推理的影响
超线程允许一个物理核运行两个逻辑线程,但二者共享计算单元和缓存。
在大模型推理中,超线程的影响存在争议:
| 场景 | 超线程收益 |
|---|---|
| 纯矩阵乘法(计算密集) | 通常无益,甚至降低性能(竞争 FPU 单元) |
| IO 密集型预处理 | 有一定收益 |
| 混合批处理(多请求并行) | 略有帮助 |
建议:对推理服务进行 A/B 测试,分别在 OMP_NUM_THREADS=物理核数 和 OMP_NUM_THREADS=逻辑核数 下测试,取性能更好的配置。
# 查看物理核数 vs 逻辑核数
lscpu | grep -E "^CPU\(s\)|Thread\(s\) per core|Core\(s\) per socket"
# CPU(s): 128
# Thread(s) per core: 2 <- 超线程开启,逻辑核=物理核×2
# Core(s) per socket: 32
6 llama.cpp 的多核配置示例
llama.cpp 是目前 CPU 推理最高效的开源实现,其线程配置:
# -t: 线程数(建议等于物理核数,而非逻辑核数)
# -ngl: 将多少层卸载到 GPU(纯 CPU 推理设为 0)
# NUMA 节点 0,32 物理核,INT4 量化模型
numactl --cpunodebind=0 --membind=0 \
./llama-cli \
-m ./models/llama-3-8b.Q4_K_M.gguf \
-t 32 \
-ngl 0 \
-p "你好,请介绍一下自己"
7 总结
多核 CPU 推理优化的核心是减少跨 NUMA 访问和保持缓存热数据:
- 单 NUMA 节点部署:模型权重与计算线程在同一 NUMA 节点,消除远端内存延迟
- 线程亲和性:用
OMP_PROC_BIND=close或taskset固定线程,保持缓存热度 - 线程数 = 物理核数:超线程对矩阵运算通常无益,不要盲目开满逻辑核数
- 多实例 > 单实例:高并发场景下,每个 NUMA 节点跑一个独立实例,比单实例多线程更高效
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)