Ollama CPU 推理大提示词优化实测报告(细致化数据版)

Ollama CPU 推理大提示词优化实测报告(细致化数据版)

测试日期:2026-08-08

关键词:Ollama / llama.cpp / CPU 推理 / 大上下文 / KV Cache 量化 / Prompt Cache

数据来源:真实服务器实测,全部原始测量数据附后


摘要(TL;DR)

结论一(反直觉) :CPU 推理下开启 KV Cache 量化(q8_0)是严重负优化------同一提示词预填充从 84 秒恶化到 208 秒,慢 2.5 倍。GPU 场景的优化手段不能照搬到 CPU。

结论二:手动设置线程数(32 / 64)与默认无差异------预填充瓶颈是内存带宽而非核心数,SMT 虚核收益互相抵消。

结论三(正解) :将 OLLAMA_KEEP_ALIVE 从默认 5 分钟拉长至 24 小时,让模型常驻内存,llama.cpp 的 Prompt Cache 对相同前缀提示词实现缓存命中 99.99%:11,055 token 的预填充从 84 秒降至 0.05 秒,请求总耗时 1.6 秒。

适用场景:固定系统提示词 + 固定文档上下文 + 尾部变化问题(RAG / 长文档问答 / 多轮对话的标准形态)。


一、测试环境

项目 参数
CPU AMD Ryzen Threadripper PRO 9975WX(Zen5)
核心 32 核 / 64 线程,单 NUMA 节点
内存 128 GB DDR5(实测 125 GiB),交换分区 8 GiB
GPU 96 GB 显存 NVIDIA 专业卡一块(未参与本模型推理,仅系统显示占用)
OS Linux(systemd 管理服务)
推理引擎 Ollama 0.30.8(内置 llama.cpp)
模型 MoE 架构 GGUF 模型:总参数 29.9B / 激活约 3B,Q4_K_M 量化,文件 31.16 GB
上下文窗口 202,752 tokens(启动参数 -c 202752
GPU offload 0 层(-ngl 0,纯 CPU 推理)
其他 llama.cpp 参数 --mlock --no-mmap --flash-attn auto -b 2048 -ub 2048 -np 1
服务监听 0.0.0.0:11434,OpenAI 兼容端点 /v1/chat/completions

二、问题现象

客户端通过 /v1/chat/completions 提交 18,730 token 的大提示词(系统提示 + 文档上下文 + 问题)后:

  • CPU 占用飙至 28 核满负荷(约 2800%),持续 3 分 14 秒;
  • 期间客户端一个 token 都不返回,表现与"死循环/卡死"无异;
  • 预填充完成后才开始生成,生成阶段 17.06 tok/s(CPU 水平)。

诊断结论 :不是死循环。Ollama 日志显示 prompt processing 进度持续推进(progress 0.11 → 0.99),说明引擎正在执行预填充(prefill) ------逐 token 消化提示词。预填充阶段本就不产生任何输出,大提示词 + CPU 推理 = 数分钟"假死"。

关键日志证据(基线,18,730 tokens)

复制代码
slot print_timing: id  0 | task 0 | prompt processing, n_tokens =  2048, progress = 0.11, t =   7.02 s / 291.56 tokens per second
slot print_timing: id  0 | task 0 | prompt processing, n_tokens =  4096, progress = 0.22, t =  16.56 s / 247.29 tokens per second
slot print_timing: id  0 | task 0 | prompt processing, n_tokens =  6144, progress = 0.33, t =  28.89 s / 212.67 tokens per second
slot print_timing: id  0 | task 0 | prompt processing, n_tokens =  8192, progress = 0.44, t =  44.14 s / 185.57 tokens per second
slot print_timing: id  0 | task 0 | prompt processing, n_tokens = 10240, progress = 0.55, t =  62.41 s / 164.08 tokens per second
slot print_timing: id  0 | task 0 | prompt processing, n_tokens = 12288, progress = 0.66, t =  83.51 s / 147.14 tokens per second
slot print_timing: id  0 | task 0 | prompt processing, n_tokens = 14336, progress = 0.77, t = 107.39 s / 133.49 tokens per second
slot print_timing: id  0 | task 0 | prompt processing, n_tokens = 16384, progress = 0.87, t = 134.02 s / 122.25 tokens per second
slot print_timing: id  0 | task 0 | prompt eval time = 10963 ms / 187 tokens (17.06 tok/s)   ← 预填充完成后进入生成
slot print_timing: id  0 | task 0 | stop processing: n_tokens = 18916, truncated = 0        ← 正常结束,未截断

三、实验设计

控制变量法 :固定提示词(19,125 字符中文 ≈ 11,055 tokens),固定生成上限(max_tokens=32,只测预填充阶段),每组只改一个变量;每次修改后 systemctl daemon-reload && systemctl restart ollama冷却 45 秒排除热降频干扰(Threadripper 满载后 boost 频率回落,已验证冷却后频率恢复 3.9~5.3 GHz)。

实验组 变更项 对照目的
基线(改前现场) 调用方真实请求 18,730 tokens,默认配置(f16 KV、自动线程) 建立参考曲线
A-1 OLLAMA_KV_CACHE_TYPE=q8_0 + OLLAMA_NUM_THREADS=64 KV 量化 + 全线程
A-2 OLLAMA_KV_CACHE_TYPE=q8_0 + OLLAMA_NUM_THREADS=32 KV 量化 + 物理核
A-3 OLLAMA_KV_CACHE_TYPE=q8_0 + 默认线程 KV 量化单独作用
B f16 KV(默认)+ 默认线程 回归基线,与 A 组对照
C f16 KV + OLLAMA_KEEP_ALIVE=24h,同提示词第二次请求 Prompt Cache 验证

四、完整测量数据

4.1 预填充速度轨迹对比表(tok/s,按已处理 token 数)

已处理 tokens 基线 18.7K (f16) A-1 q8+64线程 A-2 q8+32线程 A-3 q8+默认 B f16 默认
2,048 291.56 172.84 172.45 169.92 290.17
4,096 247.29 116.90 117.17 117.32 247.33
6,144 212.67 89.16 89.30 89.28 212.67
8,192 185.57 71.57 71.50 71.46 184.85
10,240 164.08 60.12 60.03 59.99 163.94
12,288 147.14 --- --- --- ---
14,336 133.49 --- --- --- ---
16,384 122.25 --- --- --- ---
总耗时(11,055 tokens) --- 197.4 s 207.8 s 207.8 s 84.1 s

注:A-1/A-2/A-3 三组轨迹逐项几乎一致(差异 <2%),说明线程设置对预填充无影响;A 组整体比 B 组慢约 2.5 倍,唯一变量是 KV 量化 → q8_0 是负优化元凶

4.2 关键单点对比(10,240 tokens 处)

复制代码
q8_0:  prompt eval = 170.57 s / 10240 tokens (60.03 tok/s)
f16 :  prompt eval =  62.46 s / 10240 tokens (163.94 tok/s)
                差 = 2.73 倍

4.3 Prompt Cache 验证(C 组,决定性数据)

复制代码
第一轮(模型冷加载后首个请求,全量预填充):
  prompt eval = 71451.11 ms / 11055 tokens (154.72 tok/s)   → 请求总耗时 84.1 s

第二轮(相同提示词,Keep-Alive 生效,缓存命中):
  cached n_tokens = 11054, memory_seq_rm [11054, end)        ← 跳过 11054/11055 个 token
  prompt eval = 47.99 ms / 1 tokens                          ← 仅计算尾部新增 1 token
                                                              → 请求总耗时 1.6 s
指标 第一轮 第二轮 提升
预填充耗时 71,451 ms 48 ms 99.93% ↓
请求总耗时(含 32 token 生成) 84.1 s 1.6 s 98.1% ↓
缓存命中率 0% 11,054 / 11,055 = 99.99% ---

4.4 生成阶段速度(供参考)

场景 生成速度
长文本生成(187 tokens 实测) 17.06 tok/s
短回复(32 tokens 实测) 22.67 tok/s

五、机理分析

5.1 为什么预填充随上下文衰减(291 → 122 tok/s)

  1. 注意力计算量平方级增长:每处理一个新 token 需与前面全部 KV 做注意力运算,上下文 2,048 → 16,384,计算量增 8 倍;
  2. 内存带宽是 CPU 推理的第一瓶颈:权重 31 GB + 持续增长的 KV 缓存全部走内存总线;
  3. 纯 CPU 推理(-ngl 0)时上述开销完全由内存子系统承担,核心数再多也救不了。

5.2 为什么 q8 KV 在 CPU 上是负优化

维度 GPU CPU(本实验)
内存带宽 极高(~1 TB/s 级) 有限(百 GB/s 级)
KV 量化收益 省带宽 → 明显加速 省下的带宽有限
dequant 开销 并行计算单元消化 每次注意力计算都要反量化,开销 ≥ 收益
实测结果 (未测) 慢 2.73 倍

结论 :KV Cache 量化(OLLAMA_KV_CACHE_TYPE=q8_0)是为 GPU 显存/带宽场景设计的手段,CPU 上反量化开销吃掉了全部带宽收益还倒贴。CPU 推理保持默认 f16 KV。

5.3 为什么线程数无效

预填充受内存带宽约束而非计算核心数约束;Threadripper 的 SMT 虚核(64 逻辑核)共享同一内存带宽,多线程争抢时收益互相抵消。Ollama/llama.cpp 自动检测的线程数已经是最优。

5.4 为什么 Keep-Alive + Prompt Cache 是质变

llama.cpp 会缓存已计算前缀的 KV 状态(cached n_tokens);请求前缀一致时直接复用,只对增量部分做预填充。默认 OLLAMA_KEEP_ALIVE=5min 时模型频繁卸载,缓存每次归零(基线日志 cached n_tokens = 0);拉长到 24h 后模型常驻,缓存跨请求存活------这正是 RAG 场景的标准形态:固定 system + 固定文档 + 尾部变化问题。

六、结论与最佳实践

优先级 手段 收益 备注
★★★ OLLAMA_KEEP_ALIVE=24h(模型常驻) 同前缀请求预填充提速 99.9% 前提:调用方提示词前缀稳定
★★★ 提示词结构化:前缀固定、变更放尾部 激活 Prompt Cache 的前提 调用方可改造时
★★☆ 保持 f16 KV(不开 OLLAMA_KV_CACHE_TYPE 避免 2.7 倍退化 CPU 推理
★★☆ 保持默认线程(不设 OLLAMA_NUM_THREADS 避免无效操作 自动检测即最优
☆☆☆ KV 量化 / 线程调优(CPU 场景) 负收益 / 零收益 实测

使用前提与边界

  • Prompt Cache 收益的前提是提示词前缀一致:若每次提示词完全随机(无公共前缀),缓存不命中,回到全量预填充(CPU 物理极限:11K token ≈ 84 s);
  • 模型常驻增加约 31 GB 内存占用(本机 128 GB 无压力;小内存机器需权衡);
  • 生成阶段(decoding)不受上述优化影响,CPU 上约 17~23 tok/s。

排查口诀

遇到"CPU 拉满但模型不输出":先看日志中 prompt processingprogress 是否推进------在推进 = 预填充(正常,等它完成);停滞 = 才是真卡死

附录 A:测试方法(可复现)

  1. 生成固定长度中文提示词(19,125 字符 ≈ 11,055 tokens);
  2. POST /v1/chat/completionsmax_tokens=32 隔离生成阶段,仅测预填充:
bash 复制代码
curl -s -X POST http://<host>:11434/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"<model>","messages":[{"role":"user","content":"<11K-token-text>"}],"max_tokens":32,"stream":false}'
  1. 每次修改配置:systemctl daemon-reload && systemctl restart ollama,冷却 45 s 消除热降频;
  2. journalctl -u ollama 提取 prompt processing 轨迹与 cached n_tokens
  3. 对照组每项只改一个变量。

附录 B:配置变更记录(systemd drop-in)

ini 复制代码
# /etc/systemd/system/ollama.service.d/override.conf(最终生效版)
[Service]
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"      # ❌ 已移除(实测负优化 2.7x)
Environment="OLLAMA_NUM_THREADS=64"          # ❌ 已移除(实测无差异)
Environment="OLLAMA_KEEP_ALIVE=24h"          # ✅ 保留(Prompt Cache 收益来源)

修改流程:先备份原 drop-in 目录 → 追加 Environment 行(勿覆盖原 OLLAMA_MODELS/OLLAMA_HOST/OLLAMA_ORIGINS)→ daemon-reloadrestart → 基准复测。


免责声明:本文数据为单机单次实测(每配置一组基准,轨迹逐点记录),个体差异以复测为准。测试期间系统负载 33 → 1.4(清理异常任务后),冷却 45 秒后频率恢复正常(3.9~5.3 GHz)再测量。

相关推荐
KobeSacre1 小时前
ForkJoinPool 源码
java
橘色的喵2 小时前
ARM-Linux 嵌入式库:内存池与信号量的无锁化改造
后端
SamDeepThinking2 小时前
写代码,到底应该参考优秀作品,还是坚持自己的想法?
后端
wear工程师2 小时前
Kafka 消费者心跳正常,为什么还会被踢出组?拆清 max.poll.interval.ms
java·kafka
cfm_29142 小时前
基于OAuth2.0实现微服务SSO单点登录
后端·spring·微服务·架构
橘色的喵2 小时前
MergeCell 合并机制:双平台事件流去重
后端
SemiTris2 小时前
为什么 C/C++ 不走 Java 式的虚拟机跨平台路线?
java·程序员
予怀9602 小时前
ClickHouse踩坑:一个sum()引发的"数字不一致"悬案
后端