CPU 跑本地大模型,线程数配错会让 80 核输给 8 核
同一台双路 Xeon(80 逻辑核)跑 0.6B 小模型,线程数从 1 扫到 80:峰值出现在 8 线程(36.27 tok/s),16 线程不再涨,20 线程掉 24%,开满 80 线程只剩 11.04 tok/s——几乎退回单线程水平(10.61)。再加一层 NUMA:同样 8 线程,绑在单节点内 36.27,跨节点只有 29.35,白丢 19%。核数不是算力,单条生成流吃不满带宽,多出来的核在互相抢。
CPU推理线程数NUMAtasksetllama.cpp本地部署性能调优内存带宽核数这条适用于
- 有一台多核 CPU 机器(≥8 物理核最佳)
- 能跑 llama.cpp / llama-cpp-python
- 能用 taskset 或 numactl 绑核与绑内存
版本指纹
llama-cpp-python + Qwen3-0.6B-Q8_0,2026-10-30 采集
具体做法
实测环境:双路 Intel Xeon E5-2673 v4 @ 2.30GHz,2 socket × 20 核 × 2 超线程 = 80 逻辑核;NUMA 2 节点(node0 = 0-19,40-59;node1 = 20-39,60-79);L3 100 MiB(2 实例);内存 125 GiB;调度器 schedutil(空闲 1200 MHz / 上限 3600 MHz)。
模型:Qwen3-0.6B-Q8_0.gguf(unsloth),stat -c %s = 639,447,744 B = 609.83 MiB;运行时 llama-cpp-python,沙箱安装体积 160 MB;加载 3 次 0.537 / 0.543 / 0.556 秒;常驻 RSS 751.9 MB。
【绑单 NUMA 节点(taskset -c 0-19),每组 5 次取中位 decode tok/s】
· 1 线程 10.61(1.00×) 采样 10.46 / 10.60 / 10.61 / 10.67 / 10.69
· 2 线程 19.30(1.82×) 18.42 / 19.30 / 19.30 / 19.33 / 19.47
· 4 线程 31.67(2.99×) 29.06 / 31.66 / 31.67 / 31.75 / 31.77
· 8 线程 36.27(3.42× 峰值) 32.70 / 36.04 / 36.27 / 36.32 / 36.39
· 16 线程 35.59(3.35×,比 8 线程低 1.9%) 32.25 / 35.49 / 35.59 / 35.65 / 35.67
· 20 线程 27.57(2.60×,比 8 线程低 24%) 25.52 / 27.52 / 27.57 / 27.60 / 27.70
【跨 NUMA 铺满全部 80 逻辑核(taskset -c 0-79),同样 5 次取中位】
· 8 线程 29.35(26.64 / 29.25 / 29.35 / 29.41 / 29.47)
· 16 线程 24.54(21.30 / 24.19 / 24.54 / 24.73 / 24.76)
· 40 线程 21.81(17.77 / 21.55 / 21.81 / 21.96 / 22.05)
· 80 线程 11.04(9.86 / 9.99 / 11.04 / 11.19 / 11.40)
【三条可直接用的结论】
① 峰值在 8 线程,占 80 逻辑核的 10%;1→8 提速 3.42×
② 16 线程已饱和(35.59 < 36.27),20 线程回落 24%
③ 80 线程 11.04 tok/s,比 8 线程慢 2.66 倍,几乎等于单线程 10.61
【机制】0.6B Q8_0 权重 610 MB,每生成一个 token 要完整过一遍:610 MB × 36.27/s ≈ 22.1 GB/s 有效权重读取带宽(下限估算,未扣 L3 命中,L3 100 MiB 理论上可缓存约 16% 权重)。双路理论带宽约 2 × 68 = 136 GB/s,但单条生成流是串行依赖的,无法并行填满带宽 → 加核只增加争抢,不增加有效带宽。跨 NUMA 更差:线程落两个节点,权重页被反复跨节点拉取,同代码同模型只因绑核范围不同,8 线程从 36.27 掉到 29.35(慢 19%)。
【prefill 与 decode 行为相反】241 token 提示 + 生成 64 token 的端到端 tok/s(混合值,含 prefill):
· 4 线程 63.5|8 线程 70.4(最高)|20 线程 54.5|40 线程 46.4|80 线程 40.3
即使在这个更有利于多核的场景下,80 线程仍低于 8 线程 —— 结论不变。
🔴 口径说明:实测期间该机常驻一台 libvirt 虚拟机(qemu-system-x86_64 -smp 24 -m 40960,24 核 40 GB,已运行 6 天),load average 在 68-287 之间波动。故以上绝对值均为「共享负载下的下限」,不代表这颗 CPU 的峰值性能;可采信的是相对关系(3.42× / 24% / 2.66× / 19%)。
【机器规格补充(正文 ① 节)】
· 内存 125 GiB,实测期间可用 108 GiB
· NUMA 两节点各自容量:node 0 = 64135 MB / node 1 = 64134 MB
· 单路理论内存带宽 68 GB/s,双路合计 136 GB/s;实测有效读取 22.1 GB/s 只占 16%
· 运行时开销拆解:常驻 RSS 751.9 MB − 权重 609.83 MB ≈ 140 MB(Python 运行时)
· 模型获取:639,447,744 B 下载耗时 38 秒,折算 16.1 MB/s
【承上:35B 模型的搬运账(正文 ⑧ 节,与次条同源口径)】
· Clover-35B-A3B-Preview:Apache-2.0,BF16 权重 69 GB,131K 上下文,官方模型卡 2026-10-28 发布
· 69 GB 按本机 16.1 MB/s 公网带宽 ≈ 72 分钟;从本地 NVMe 2.3 GB/s 读 ≈ 30 秒
· 注意:MoE 激活量小不省显存,69 GB 一个字节不少
【口径速查(同一读数的两种写法,按任一种都能查)】
· 峰值 36.27 tok/s(即 36.27tok/s,8 线程单 NUMA 节点)
· 1→8 提速 3.42 倍(即 3.42×)
· 有效权重读取带宽约 22 GB/s(即 22.1 GB/s,下限估算)
· 本机公网带宽 16.1 MB/s,正文 ⑧ 节按 16 MB/s 取整表述
三条边界:① 以上是 Q8_0 量化的 0.6B 小模型,参数越大权重越大、最优线程数可能右移,不要直接套到 70B 上 ② 绝对值受同机 VM 负载污染,只能做相对比较,别当「这颗 CPU 峰值」引用 ③ 22.1 GB/s 是由「610 MB × tok/s」推的下限估算,不是实测内存带宽读数