通过 llama.cpp 的 -t 参数控制 CPU 线程数对 QPS 影响的实际测量

文章导读
用 llama.cpp 的 -t 参数调整 CPU 推理线程数,确实会影响 QPS,但这个影响不是简单的线性关系。线程数太低,CPU 算力吃不饱;线程数太高,线程切换、内存带宽争抢和缓存竞争反而会让 QPS 下降。实际测量时,需要把模型、上下文长度、请求并发、批次大小都固定住,只让 -t 变化,才能得到相对可信的对比结果。
📋 目录
  1. A 先明确测量对象:单请求时延,还是并发吞吐
  2. B 推荐的测量流程
  3. C 除了 QPS,还要看什么
  4. D 常见的测量陷阱
  5. E 验证清单与最小实验
  6. F 常见问题
A A

用 llama.cpp 的 -t 参数调整 CPU 推理线程数,确实会影响 QPS,但这个影响不是简单的线性关系。线程数太低,CPU 算力吃不饱;线程数太高,线程切换、内存带宽争抢和缓存竞争反而会让 QPS 下降。实际测量时,需要把模型、上下文长度、请求并发、批次大小都固定住,只让 -t 变化,才能得到相对可信的对比结果。

测量 llama.cpp 的 -t 与 QPS 关系时,核心是控制变量:固定模型、上下文、并发和请求内容,只改变线程数。通常单次请求的吞吐会随线程数先升后平或略降,并发场景下表现更复杂。线程数不是越大越好,建议从 CPU 物理核心数的一半开始测,逐步加到两倍物理核数,再结合 CPU 占用和功耗选参数。任何结论都要基于本地实测,不能直接套用别人的数字。

先明确测量对象:单请求时延,还是并发吞吐

QPS(每秒查询数)在不同负载下对线程数的敏感度不同。单请求顺序测量时,-t 主要影响单次推理的 token 生成速度;并发请求时,-t 还影响多个请求之间对 CPU 资源的分配。建议先做两种测量:单一连接连续发请求的“单流 QPS”,以及多客户端同时请求的“并发 QPS”。两种结果不能混在一起对比,否则会误判最优线程数。

推荐的测量流程

  1. 用命令 lscpu 查看物理核数、逻辑核数和超线程状态。把物理核数记为 P,逻辑核数记为 L。测量范围建议从 P/2L,比如 4 物理核 8 逻辑核,就测 2、3、4、5、6、7、8 这组 -t 值。
  2. 固定模型文件、上下文长度、解码参数(温度、top_p 等)。上传同一个 prompt,保证每次生成的 token 数量相近。建议用相同 prompt 重复 20 次以上,取平均值。
  3. 先测单流 QPS:用类似 curl -s -w "%{time_total}\n" -o /dev/null http://127.0.0.1:8080/completion ... 循环请求,或者用简单的 bash 脚本记录耗时。再测并发:用 wrkhey 或写一个多线程 Python 脚本,固定并发数(比如 8、16)发请求,记录每秒完成数。
  4. 测试过程中用 mpstat -P ALL 1 观察每个核心的使用率,用 free -h 观察内存占用,用 perf stat 观察缓存命中率(可选)。如果 CPU 使用率已经接近 100% 而 QPS 不再上升,说明瓶颈在计算;如果 CPU 使用率只有几十而 QPS 下降,要检查内存带宽或 NUMA 拓扑。
# 单流测量示例:连续调用 20 次,统计平均耗时
for i in $(seq 1 20); do
  curl -s -o /dev/null -w "%{time_total}\n" \
    -H "Content-Type: application/json" \
    -d '{"prompt": "写一段 200 字的技术说明", "n_predict": 200}' \
    http://127.0.0.1:8080/completion
done | awk '{sum+=$1; count++} END {print "平均耗时(s):", sum/count}'

除了 QPS,还要看什么

只看 QPS 容易忽略两个问题:CPU 占用率是否打满、内存带宽是否饱和。llama.cpp 在 CPU 上的推理对内存带宽非常敏感,尤其是大模型或长上下文时,线程数增加会加剧内存控制器压力。建议用 topmpstat 记录测试期间的 CPU 平均占用;如果 QPS 相近,选择 CPU 占用更低或功耗更小的线程数。另外,如果服务器有多个 NUMA 节点,-t 只设置线程数,线程的分配可能跨 NUMA,导致性能波动。可以先观察 numactl `--hardware`,必要时用 numactl `--cpunodebind`=0 绑定到单一 NUMA 节点再测。

通过 llama.cpp 的 -t 参数控制 CPU 线程数对 QPS 影响的实际测量

常见的测量陷阱

  • 请求长度不固定:生成 token 数相差很大时,QPS 没有可比性。建议固定 n_predict,或者让每个请求都从同一个 prompt 生成到同一停止词。
  • 并发工具本身成为瓶颈:wrk 和 hey 适合短请求,llama.cpp 的单次请求可能超过几十秒,并发连接数和超时时间都要调大。建议先确认工具进程的 CPU 占用不持续偏高。
  • 没有预热:模型首次加载、页面缓存未命中时耗时会明显偏高。先发几个请求“热”起来,再开始计时。
  • -t-tb(batch 线程数)混在一起。llama.cpp 还有 -tb 参数控制 batch 处理阶段的线程数。如果只改 -t,不改 -tb,实际效果取决于哪部分操作更耗时。需要明确你测量的是整体 QPS,而不是某个解码阶段的速度。

验证清单与最小实验

  1. 确认服务器 CPU 型号、物理核数、超线程状态,记录环境信息。
  2. 选择一个 -t 值,启动 llama.cpp server,固定上下文长度和请求参数。
  3. 用一个固定 prompt 连续请求 20 次,记录平均单次耗时,计算 QPS。
  4. 保持其他条件不变,杀掉旧进程,换下一个 -t 值,重复同样请求。注意每次都要重新启动进程,避免前后端状态残留。
  5. 将不同 -t 的 QPS 列成表格,同时记录 CPU 占用和内存带宽(如果可用)。
  6. 观察趋势:QPS 是否在某个值后不再上升,或者出现下降。选择拐点附近的线程数做重复测量,确认不是随机波动。

如果你希望快速得到一个可用的初始值,可以先从物理核心数开始试,然后向上和向下各调一档,对比三组结果。不要直接照搬别人的“最优线程数”,因为模型大小、处理器微架构、内存频率都对结果有明显影响。测量过程中的原始记录和命令参数,建议保存为文本,方便之后复盘。

常见问题

-t 设成逻辑核心数就是最优吗?

不一定。超线程提供的逻辑核心对计算密集任务帮助有限,有时反而引起缓存争抢。通常最优值在物理核心数附近,但具体要通过测量确认。

通过 llama.cpp 的 -t 参数控制 CPU 线程数对 QPS 影响的实际测量

为什么 QPS 下降了但 CPU 还是没满?

可能是内存带宽或跨 NUMA 访问成为瓶颈。此时线程数增加只会增加等待时间,不会提高计算效率。可以尝试绑定 NUMA 节点,或者改用更大的 batch 提升单次计算效率。

单流 QPS 和并发 QPS 都要测吗?

建议都测。单流反映单次请求的响应速度,并发反映服务端对多个请求的调度能力。你的使用场景偏在线服务,就重点看并发;偏离线批量生成,就重点看单流。