ik_llama.cpp 的 NUMA 支持现状与多路 CPU 推理性能优化实战 ik_llama.cpp 的 NUMA 支持现状与多路 CPU 推理性能优化实战【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp本文基于 ik_llama.cpp 仓库中的社区讨论《What is the NUMA situation ?》github-data/discussions/201系统梳理该项目在非统一内存访问NUMA多路服务器上的支持现状、--numa参数的三种工作模式及其源码级实现原理并结合 Intel Xeon 6980P、AMD Epyc 等真实平台的基准数据给出 CPU-only 大模型推理的完整调优路径。读完本文你将掌握如何用numactl正确测量 NUMA 系统性能、如何组合--numa与 ik_llama.cpp 特有的-fa、-mla、-fmoe、-rtr等选项榨取多路 CPU 的推理吞吐以及 BIOS 层面SNC、snoop 模式对带宽的影响。一、背景为什么 LLM 推理绕不开 NUMA讨论发起人 bhugueney 提出的观察非常准确token 生成TG阶段是内存带宽受限的而大语言模型动辄需要数百 GB 内存。要同时扩大内存容量与带宽最便宜的方式就是走向 NUMA 多路服务器。例如一台双路 AMD Epyc 服务器每颗 CPU 可以拥有 16 或 24 条内存通道每颗 CPU 最多可划分为 4 个 NUMA 域以获得最好的理论性能在 Gen 2 Epyc 上L3 缓存仅在同一个 CCX 内的核心之间共享。但 NUMA 高效编程布满陷阱核心难点在于最小化跨 NUMA 域的内存访问与 PCIe 访问。社区当时的共识是llama.cpp及其 fork ik_llama.cpp只避免了最基础的问题比如把所有内存分配在同一个 NUMA 域距离高效利用多路 CPU 还有很大差距。相比之下KTransformers 采用在每个 NUMA 域复制矩阵的做法vLLM 则把每个 NUMA 节点当作一张 GPU 卡来做张量并行。二、ik_llama.cpp 的 NUMA 现状与主线 llama.cpp 一致面对ik_llama.cpp 是否 NUMA 感知的提问作者 ikawrakow 在 2025-02-11 给出了直接答复ik_llama.cpp 是 llama.cpp 的 fork其 NUMA 情况与 llama.cpp 相同。换句话说ik_llama.cpp继承并保留了 llama.cpp 的 NUMA 支持能力——即通过--numa参数启用三种线程亲和性策略但尚未实现每个 NUMA 域独立后端 张量并行这类更深层的架构。作者也坦言自己手头没有带宽足够大的双路系统可供测试而单纯优化 TG 阶段的访存模式与线程同步离不开真实硬件验证详见下文基准部分。三、实操--numa参数的三种策略无论是llama-climain 示例还是llama-benchik_llama.cpp 都支持统一的--numa TYPE命令行参数。参数解析位于 common/common.cpp帮助文本位于 common/common.cpp--numa TYPE attempt optimizations that help on some NUMA systems - distribute: spread execution evenly over all nodes - isolate: only spawn threads on CPUs on the node that execution started on - numactl: use the CPU map provided by numactl if run without this previously, it is recommended to drop the system page cache before using this三种策略的语义对比如下策略取值线程亲和性行为distributedistribute或空字符串按线程号取模节点数的方式把执行线程均匀散布到所有 NUMA 节点isolateisolate只在进程启动时所在的 NUMA 节点的 CPU 上派发线程numactlnumactl直接采用numactl提供的 CPU 掩码cpuset配合外部numactl命令使用禁用默认不传该参数不做任何 NUMA 相关线程亲和性设置llama-bench的解析逻辑examples/llama-bench/llama-bench.cpp与 common 完全一致默认值为GGML_NUMA_STRATEGY_DISABLEDexamples/llama-bench/llama-bench.cpp。3.1 与 numactl 配合的标准用法讨论中被反复引用的、Intel Xeon 6980P 上的实测命令即是最佳示范先用numactl把进程绑定到单个 NUMA 节点-N 0 -m 0表示在节点 0 上运行、内存也从节点 0 分配再让程序内部使用--numa numactl沿用该掩码numactl -N 0 -m 0 \ ./build/bin/llama-bench \ --model /mnt/ai/models/unsloth/DeepSeek-R1-GGUF/DeepSeek-R1-Q8_0/DeepSeek-R1.Q8_0-00001-of-00015.gguf \ --cache-type-k f16 \ --cache-type-v f16 \ --numa numactl \ --threads 64,43,64,86,128,172其中--threads传入多个候选值64、43、86、128、172llama-bench会逐一跑出 pp512 与 tg128 的吞吐。3.2 重要提示切换 NUMA 策略后建议清空系统页缓存帮助文本特别提醒如果在之前未使用--numa的情况下突然启用它建议先 drop 系统页缓存否则 mmap 的模型页可能已被内核按旧策略映射到远端节点抵消亲和性设置的效果。这解释了社区中换策略后性能反而变差的一类常见误报。四、源码级原理ggml 是如何实现 NUMA 感知的--numa参数最终通过llama_numa_init()进入 ggml 层。src/llama.cpp 中的实现非常薄void llama_numa_init(enum ggml_numa_strategy numa) { if (numa ! GGML_NUMA_STRATEGY_DISABLED) { ggml_numa_init(numa); } }真正的实现位于 ggml/src/ggml.c其工作流程是枚举节点与 CPU遍历/sys/devices/system/node/node%u统计 NUMA 节点数遍历/sys/devices/system/cpu/cpu%u统计 CPU 总数上限分别为GGML_NUMA_MAX_NODES与GGML_NUMA_MAX_CPUS确定当前节点通过getcpu()旧 glibc 回退到syscall(SYS_getcpu)获取进程当前所在 CPU 与 NUMA 节点构建节点 → CPU 映射对每个节点扫描/sys/devices/system/node/node%u/cpu%u得到该节点包含的 CPU 列表健康检查若节点数或 CPU 数为 0则整体禁用 NUMA 支持。ggml_is_numa()的定义是n_nodes 1ggml/src/ggml.c——只有检测到多于 1 个节点后续亲和性逻辑才会生效。线程亲和性的核心是set_numa_thread_affinity()ggml/src/ggml.c它对三种策略分别处理GGML_NUMA_STRATEGY_DISTRIBUTEnode_num thread_n % n_nodes按线程序号轮转分配节点保证负载均摊GGML_NUMA_STRATEGY_ISOLATE所有线程绑定到current_node进程启动时所在的节点GGML_NUMA_STRATEGY_NUMACTL直接把numactl注入的 cpuset 用pthread_setaffinity_np应用到当前线程后返回。随后用CPU_SET_S把目标节点的全部 CPU 装入掩码再调用pthread_setaffinity_np完成绑定。需要强调的是这是静态线程亲和性项目中没有动态线程调度、也没有线程池作者在讨论中明确说明线程一旦按策略绑到某个节点就不会在运行时跨节点迁移。此外ggml/src/ggml.c 还会读取/proc/sys/kernel/numa_balancing若内核自动 NUMA 平衡处于开启状态会打印警告——因为它会偷摸迁移线程与页面已被观察到会损害性能。4.1 从源码结构可以推断的局限从以上实现可以看出ik_llama.cpp 的 NUMA 支持停留在分配线程到节点这一层模型权重加载仍是单线程完成内存大概率落在启动节点推理时线程也只在分配层面感知 NUMA。它没有实现以下能力按 NUMA 域分块加载模型权重把每个 NUMA 域当作独立后端类似多 GPU 的张量并行动态负载均衡或 work stealing。这与讨论中单纯修补 CPU 后端恐怕会撞墙的担忧吻合——要彻底解决 NUMA 效率可能需要在架构上更像多 GPU 后端。五、真实基准Xeon 6980P 上的 ik_llama.cpp 表现讨论中引用了 Intel Xeon 6980PGranite Rapids双路平台的实测数据这是社区公开可查的、为数不多的 ik_llama.cpp NUMA 专项测试。5.1 单节点对比ik_llama.cpp 略胜主线在相同条件下单 NUMA 节点、单 CPU、DeepSeek-R1 671B Q8_0tg128 的对比量化Tokens/SecondNUMA 配置Q8_06.61x NUMA Node on 1x CPU ik_llamaQ8_06.21x NUMA Node on 1x CPU主线 llama.cpp5.2 线程数扫描ik_llama.cpp 峰值出现在 128 线程按numactl -N 0 -m 0--numa numactl命令跑出的完整数据DeepSeek-R1 671B Q8_0模型 664.29 GiB671.03B 参数CPU 后端modelsizeparamsbackendthreadstestt/sdeepseek2 671B Q8_0664.29 GiB671.03 BCPU64pp51256.86 ± 7.21deepseek2 671B Q8_0664.29 GiB671.03 BCPU64tg1284.86 ± 0.01deepseek2 671B Q8_0664.29 GiB671.03 BCPU43pp51240.62 ± 0.02deepseek2 671B Q8_0664.29 GiB671.03 BCPU43tg1283.69 ± 0.00deepseek2 671B Q8_0664.29 GiB671.03 BCPU64pp51257.67 ± 4.62deepseek2 671B Q8_0664.29 GiB671.03 BCPU64tg1284.89 ± 0.00deepseek2 671B Q8_0664.29 GiB671.03 BCPU86pp51262.21 ± 13.63deepseek2 671B Q8_0664.29 GiB671.03 BCPU86tg1285.69 ± 0.00deepseek2 671B Q8_0664.29 GiB671.03 BCPU128pp51278.89 ± 21.46deepseek2 671B Q8_0664.29 GiB671.03 BCPU128tg1286.60 ± 0.00deepseek2 671B Q8_0664.29 GiB671.03 BCPU172pp51270.63 ± 0.58deepseek2 671B Q8_0664.29 GiB671.03 BCPU172tg1285.05 ± 0.00值得注意的结论ik_llama.cpp 的 PP 与 TG 吞吐峰值都出现在 128 线程且在该平台上 pp512 峰值78.89 t/s远高于默认配置说明在多路 CPU 上正确设置--numa与线程数对 pp 的影响极其显著。原作者指出测试方使用的是最低性能配置即未启用下文第 6 节的 ik_llama.cpp 专有优化选项并据此推算在他的 Ryzen-7950X 上通过开启-fa -rtr -fmoe可将 DeepSeek-Lite 的 pp512 从 433 t/s 提升到 652 t/s提升约 50%因此同样的优化组合在 6980P 上冲击 100 t/s 并非不可能。5.3 峰值带宽参照TG 已接近单节点理论极限原作者在自己机器Ryzen-7950X单路上的数据可以作为参照系DeepSeek-Lite Q8_0 的tg128 22.3 t/s对应约57 GiB/s 的权重读取速率已非常接近该平台从未超过的 60 GiB/s 上限即便稠密模型亦然差距主要来自百分之几的同步开销。对比之下6980P 单节点理论带宽约 512 GiB/s经 Intel Memory Latency Checker 实测约 552~555 GiB/s而实测 6.6 t/s 与按参数规模推算的 ~11.9 t/s 存在近 2 倍差距——这说明多路系统上的 TG 性能远未达到理论峰值瓶颈主要在跨节点访存与线程同步而非算力。六、ik_llama.cpp 特有的加速选项CPU 场景同样适用讨论中多次强调ik_llama.cpp 相对主线的优势在于一系列独有的运行时选项而这些选项在此前 6980P 测试中全部未启用。作者在 Ryzen-7950X 上给出的 DeepSeek-Lite Q8_0pp512对照表threads16modelthreadsfartrfmoetestt/sdeepseek2 16B Q8_016000pp512433.04 ± 1.44deepseek2 16B Q8_016100pp512440.25 ± 2.54deepseek2 16B Q8_016001pp512441.58 ± 3.34deepseek2 16B Q8_016101pp512452.19 ± 1.21deepseek2 16B Q8_016010pp512607.32 ± 5.09deepseek2 16B Q8_016110pp512625.10 ± 7.66deepseek2 16B Q8_016011pp512627.87 ± 4.54deepseek2 16B Q8_016111pp512652.81 ± 3.52这些选项在 common/common.cpp 中均有对应解析-fa, --flash-attn (auto|on|off|0|1)Flash Attention 开关common/common.cpp-mla, --mla-use启用 MLAMulti-head Latent Attention优化对 DeepSeek 系模型的长上下文 TG 提升显著但 MLA 本身计算更重、会牺牲部分 PP因此社区推荐-mla 2 -fa组合common/common.cpp-no-fmoe, --no-fused-moe默认开启的 fused MoE融合专家并行开关-fmoe即其开启态common/common.cpp-rtr, --run-time-repack运行时重新打包权重common/common.cpp。注意社区提醒-rtr会禁用 mmap可能在 NUMA 场景下导致内存分配位置不理想测试者个人倾向先把量化重新打包成仓库支持的格式再加载而非使用-rtr-ctk, --cache-type-k TYPEK cache 数据类型配合-ctk-first/-ctk-last可对不同层设置不同 cache 类型common/common.cpp。将这些选项与--numa、--threads组合就是当前 CPU-only 多路服务器上跑 DeepSeek 类 MoE 模型的最佳实践起点。七、系统与 BIOS 层面的调优实测数据说话7.1 用 Intel MLC 验证节点数 vs 带宽的取舍讨论参与者 ubergarm 在一台双路 Xeon 6980P 上用 Intel Memory Latency Checkermlc做了对照实验结论对实际部署有直接指导意义**BIOS 设置SNCDisable每颗 CPU 合并为 1 个 NUMA 节点全机 2 节点**时节点内带宽约554843 MB/s≈555 GiB/s跨节点带宽约 247 MB/s数据地址 homed 在 writer socket 时空闲延迟本节点 130 ns、远端约 410~449 ns。**BIOS 设置SNCAuto/Enabled每颗 CPU 3 个 NUMA 节点全机 6 节点**时本地节点带宽约 188 GB/s、跨节点低至 27~94 GB/s本节点延迟 102~138 ns、跨节点高达 392~513 ns。结论节点划分越多、本地带宽与延迟越优但单个节点内存容量越小、跨节点通信越昂贵。对需要容纳数百 GB 大模型的双路服务器而言社区共识是优先保证每路一个 NUMA 节点AMD Epyc 用 BIOSNPS1Intel Xeon 用SNCDisable把权重分布在两个大节点上——单路 512 GiB/s 的带宽已足够接近最优同时能装下更大的模型。7.2 snoop 模式的意外收获另一位参与者 VinnyG9 分享在 X99 双路主板上尝试所有 snoop 模式后相比默认 BIOS 设置获得 200~300% 的性能提升该设置同样存在于 Xeon Scalable 平台。在其 qwen3moe 30BQ4_KCUDA 后端ngl0 即纯 CPU 权重 GPU 计算场景的对比中默认 BIOSpp512 123.10 ± 1.64tg128 12.28 ± 0.03home snoop w/ dir OSBpp512 263.82 ± 6.02tg128 34.76 ± 1.54tg256 达 35.70。同一作者还观察到snoop 模式作为 NUMA 相关设置对单路 CPU 推理也有 10~30% 的提升Intel MLC 测得的带宽从约 116 GB/s 提升到 140 GB/s混合CPUGPU 部分 offload推理下 TG 只提升约 10%而 qwen3 稠密模型则获得了 90% 的提升。7.3 运行时实测PP 与 TG 的访存特征差异saood06 在其双路机器上用 Intel PCM 实测了 DeepSeek 系模型的访存行为直接印证了 NUMA 优化的两个不同战场阶段Socket-0 READ (GB)Socket-1 READ (GB)LOCALLLCRDMISSLAT (ns)PP7.932.5648%365.82 / 436.65TG16.2214.7492%219.40 / 214.65解读PP 阶段大量访存跨节点LOCAL 仅 48%两路负载不均衡说明预填充阶段的计算调度与数据布局没有对齐 NUMA 拓扑TG 阶段 LOCAL 高达 92%两路读取量相当16.22 vs 14.74 GB说明 TG 对带宽的利用更好剩下的提升空间主要在消除剩余 8% 的跨节点访问与线程同步开销。这正好解释了为什么PP 性能可以靠-fa/-rtr/-fmoe等计算侧优化大幅拉高而 TG 的瓶颈是内存访问模式与同步。八、社区路线图通往高效 NUMA 的三条候选路径讨论末尾集中探讨了未来方向均处于构想/外部验证阶段尚未进入 ik_llama.cpp 主分支整理如下供读者参考模型复制data parallel把权重复制到每个 NUMA 节点各节点线程只读本地内存。社区 forkvproxy-tools/llama.cpp实测 QwQ-32B FP16 从 ~6.6 提到 ~10.7 token/s、DeepSeek R1 671B Q8 从 ~7.2 提到 ~9.7 token/s代价是内存占用翻倍且当时仅支持双路。原作者评价这能展示消除全部非本地访存的性能上限但复制模型的成本过高双线程加载 线程分半不复制模型而是加载权重时用两个线程各自绑到对应 NUMA 节点各加载一半张量推理时把线程一半绑节点 0、一半绑节点 1。原作者认为这应能显著提速且实现简单是第一步最该做的方案但被指出真正的难点在于确保每个线程处理的数据恰好存储在其本地节点专家并行expert parallelism针对 DeepSeek V3/R1 这类 MoE 模型把专家张量拆到不同 NUMA 节点每个节点用独立 RPC 后端——即把每个 NUMA 节点当作一张 GPU。这是 vLLM 的 NUMA 思路但改造成本最高。此外讨论中还提到了与 NUMA 高度相关的两个硬件事实Intel Granite Rapids6980P支持 AVX512 全系含AVX512_fp16与amx_bf16/amx_int8/amx_tile而主线 llama.cpp 的 AMX 支持是在 ik_llama.cpp 最后一次合并主线之后才加入的由于 ik_llama.cpp 大幅重构了后端移植并非易事截至讨论时尚未移植。九、总结与建议清单回到最初的问题ik_llama.cpp 是 NUMA 感知的吗——是的但仅限于线程亲和性层面它继承了 llama.cpp 的--numa distribute|isolate|numactl三种策略实现于 ggml/src/ggml.c 的节点枚举与 ggml/src/ggml.c 的线程绑定但在权重按节点分载、节点级并行等深层能力上仍属空白且缺少动态线程调度。给多路 CPU 用户的落地建议测量先行用numactl -N X -m X--numa numactlllama-bench的--threads多值扫描先画出本机的吞吐-线程曲线不要沿用默认线程数BIOS 归一双路 AMD Epyc 设NPS1、Intel Xeon 设SNCDisable把节点数收敛为 2 个兼顾带宽与模型容量尝试 snoop 模式Intel 平台可在 BIOS 中切换 snoop 模式如home snoop w/ dir OSB可能带来倍数级收益关闭内核自动 NUMA 平衡确认/proc/sys/kernel/numa_balancing为 0避免内核页面迁移干扰亲和性组合 ik_llama.cpp 专有选项DeepSeek 系模型优先-mla 2 -fa配合默认开启的 fused MoE-rtr需注意其禁用 mmap 的副作用必要时改为预重打包权重对 TG 预期管理多路系统的 TG 目前距理论带宽峰值仍有近 2 倍差距这是访存模式与同步开销所致也是该项目后续优化的重点方向。【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考