刚开机的工作站上运行 ollama run llama3.1:8b "say hello",光标闪了 45 秒第一个 token 才出来。之后 token 以 40+ t/s 流式输出,再问第二个问题几乎秒回。这不是 bug,而是冷启动开销,在 Ollama、llama-server(llama.cpp)和 LM Studio 上、在 NVIDIA / AMD / Apple Silicon 上表现都一样。
最快修复: 首 token 慢几乎总是两件事之一——模型还没进显存(磁盘读取 + 拷贝到 VRAM),或者它闲置后被卸载了。把它常驻内存,并在启动时预热一次:
# 把模型钉在内存里,永不卸载(Ollama)
export OLLAMA_KEEP_ALIVE=-1
# 然后用一个空请求预加载(只加载、不生成):
curl http://localhost:11434/api/generate -d '{"model": "llama3.1:8b"}'
这样就能彻底消除”被卸载导致的冷启动”。本文接下来诊断的是第一次冷启动(即便保活也躲不掉的那一次),并把它压缩到最短。
“冷启动”到底包含什么
冷模型的 Time to First Token(TTFT)是几个串行阶段之和,每个阶段都有各自的原因和修复方式:
| 阶段 | 发生了什么 | 典型耗时(8B Q4) | 典型耗时(70B Q4) |
|---|---|---|---|
| 磁盘读取 | GGUF 权重从 SSD/HDD 读入内存 | NVMe 2-8 秒 / HDD 30-90 秒 | NVMe 10-25 秒 / HDD 4-10 分钟 |
| 拷贝到 VRAM | 权重从内存拷贝到 GPU 显存 | 1-3 秒 | 5-15 秒 |
| Shader 编译 | CUDA/Metal kernel 首次使用时编译 | 首次 5-30 秒,之后命中缓存 | 首次 5-30 秒,之后命中缓存 |
| KV cache 分配 | 为上下文预留一整块连续显存 | 不到 1 秒 | 2-8 秒 |
| Prefill | 输出第一个 token 前先处理输入 prompt | 2k token 约 1-3 秒 | 2k token 约 15-30 秒 |
模型加载完、shader 缓存热了之后,下一次请求会跳过除 prefill 外的所有阶段——这就是第二个 prompt 飞快的原因。
常见原因
按对 TTFT 的影响从大到小排列。
1. 模型被卸载了,必须重新加载
这是最常见的”随机首 token 变慢”。两个运行时默认都会卸载空闲模型:
- Ollama 在最后一次请求后 5 分钟卸载模型(服务级变量
OLLAMA_KEEP_ALIVE,截至 2026 年 6 月默认5m)。隔了 10 分钟再请求,下一次就会触发完整重新加载。 - LM Studio 给 JIT 加载的模型默认 60 分钟空闲 TTL;而你用
lms load手动加载的模型没有 TTL,会一直常驻直到手动卸载。
怎么判断: 在发请求之前运行 ollama ps。如果列表里没有这个模型,说明它已被卸载,下一次请求要付完整的冷启动代价。LM Studio 则在 Developer 标签页查看已加载模型列表。
2. 权重不在系统文件缓存里(首次从磁盘读取)
刚开机时操作系统的页缓存是空的,所以 GGUF 要从磁盘读。一个 40 GB 的 Q4_K_M 文件,从 NVMe SSD(约 3-5 GB/s)读大约 8-15 秒;从 SATA SSD(约 500 MB/s)读要 80 秒以上;从机械硬盘(约 150 MB/s)读可能要好几分钟。这个磁盘读取阶段在第一次冷启动里占大头。
怎么判断: 连续运行两次 time ollama run llama3.1:8b "a"。如果第二次比第一次快 5-10 倍,说明文件缓存未命中(磁盘 I/O)是主要开销。用 df -h ~/.ollama/models 确认存储介质。
3. CUDA shader JIT 编译(NVIDIA)
NVIDIA 的预编译 CUDA 二进制(Ollama 内置了 llama.cpp)里带了常见架构的 SASS,但如果你的 GPU 架构不在其中精确匹配,驱动会在首次启动时即时(JIT)编译 PTX——耗时从几百毫秒到几秒不等。结果会缓存到 Linux 上的 ~/.cache/nvidia/ComputeCache/(由 CUDA_CACHE_PATH 控制,默认上限通过 CUDA_CACHE_MAXSIZE 设为 256 MiB)。之后再运行就跳过这一步。
怎么判断: 首次加载期间盯着 nvidia-smi——JIT 编译阶段 GPU 利用率会在 10-30% 停留几秒(既不是 0%,也不是 90% 以上)。看 ~/.cache/nvidia/ComputeCache/ 在首次启动时是否变大。设 CUDA_CACHE_DISABLE=1 会强制每次都重新编译,可以快速验证这个开销。
4. Metal shader 编译(Apple Silicon)
macOS 上 Metal 后端首次使用时编译 shader,在 M2/M3 上约需 5-15 秒。结果会被缓存,所以每个 Ollama/LM Studio 版本只付一次——更新运行时会让缓存失效,需要重新付一次。
怎么判断: 在 Ollama 日志里找 shader 编译相关的行,同时看 Activity Monitor——首次加载时 shader 编译表现为一段 CPU(而非 GPU)占用飙升。如果某次运行时升级后冷启动又变慢了,原因就是它。
5. KV cache 分配延迟
70B 模型在 8192 上下文下,需要一整块连续的显存(约 4-8 GB)作为 KV cache。在显存碎片化、或显存与显示输出共用的系统上,这次分配可能卡住几秒。
怎么判断: 在模型加载前后分别运行 nvidia-smi --query-gpu=memory.used,memory.free --format=csv,noheader。memory.used 在 5-10 秒内大幅跳升,就是 KV cache 分配阶段。
6. mmap 懒加载导致首次 prefill 大量缺页中断
llama.cpp 默认用 mmap 加载 GGUF(--mmap,默认开启),所以启动看起来很快,但权重要等被访问时才真正从磁盘读。首次推理会触发大量缺页中断(page fault),于是前几个 token极慢,后面的 token 才恢复正常速度。
怎么判断: perf stat -e page-faults ./llama-cli -m model.gguf -p "hello" -n 10。首次生成期间出现几万次 major fault,就是 mmap 懒加载。用 --no-mmap(加载时整体读入)配合 --mlock 解决。
7. system prompt 太长 = prefill 太长
如果第一个请求带了 2000 token 的 system prompt,prefill(在输出第一个 token 前处理全部输入)会随输入长度线性增长:8B 模型约 1-3 秒,70B 约 15-30 秒。即使模型完全热了,这也是真实的延迟。
怎么判断: 先发一个极短 prompt("a")测出基础 TTFT,再发完整 prompt,两者之差就是 prefill。在 llama-server 上 cache_prompt 默认为 true,重复的相同前缀会被复用而不是重新处理。
最短修复路径
Step 1:把模型钉在内存里(消除被卸载导致的冷启动)
# 让所有模型永久常驻(服务级默认值)
export OLLAMA_KEEP_ALIVE=-1
ollama serve &
# 或在单次请求里覆盖服务默认值:
curl http://localhost:11434/api/generate \
-d '{"model": "llama3.1:8b", "prompt": "hello", "keep_alive": -1}'
# 或设一个固定时长,比如 1 小时:
export OLLAMA_KEEP_ALIVE=1h
对于 systemd 持久化安装,在 /etc/systemd/system/ollama.service 的 [Service] 段加 Environment="OLLAMA_KEEP_ALIVE=-1",然后 sudo systemctl daemon-reload && sudo systemctl restart ollama。在 macOS 上,把 OLLAMA_KEEP_ALIVE 加到 ~/Library/LaunchAgents/com.ollama.ollama.plist 的 EnvironmentVariables 块里。
注意:keep_alive: -1 只是运行时状态——服务重启后模型又会被卸载,所以仍需要 Step 2。另外,如果模型被钉住时显存被占满,Ollama 会直接返回错误,而不会去驱逐被钉住的模型。
LM Studio 则在 Developer 标签页 > Server Settings 里设置 JIT TTL,或用 lms load <model> 手动加载(无 TTL),让模型一直常驻。
Step 2:在服务启动时预热模型
#!/bin/bash
# warmup.sh —— Ollama 启动后运行(比如放在 ExecStartPost 或应用启动流程里)
until curl -s http://localhost:11434/api/version > /dev/null; do sleep 1; done
# 空请求只加载模型、不生成任何内容:
curl -s http://localhost:11434/api/generate \
-d '{"model": "llama3.1:8b", "keep_alive": -1}' > /dev/null
echo "Model warmed up"
请求里不带 prompt 字段时,模型会被加载进显存(并编译 shader),但不会跑生成,这样第一个真实用户就不会遇到冷启动。
Step 3:把权重预读进操作系统页缓存
# 让磁盘读取阶段提前完成。先找到模型 blob:
ls -lh ~/.ollama/models/blobs/
# 然后预热它(用 vmtouch 最干净):
vmtouch -t ~/.ollama/models/blobs/sha256-<model-hash>
# 没装 vmtouch?普通读取也行:
dd if=~/.ollama/models/blobs/sha256-<model-hash> of=/dev/null bs=4M status=progress
# 验证是否已驻留缓存:
vmtouch ~/.ollama/models/blobs/sha256-<model-hash>
文件会一直留在缓存里,直到内存压力把它驱逐。配合 Step 1 一起用,模型也会同时常驻显存。
Step 4:用一次预热推理把 GPU shader 编译好
# 启动服务,再打一个极小的生成请求触发并缓存 shader JIT。
./llama-server -m model.gguf --n-gpu-layers all --port 8080 &
# 等服务起来,再发一个 1 token 的预热请求:
until curl -s http://localhost:8080/health > /dev/null; do sleep 1; done
curl -s http://localhost:8080/v1/chat/completions \
-d '{"messages": [{"role": "user", "content": "a"}], "max_tokens": 1}' > /dev/null
echo "Shaders compiled and cached"
NVIDIA 上这会填充 ~/.cache/nvidia/ComputeCache/;Apple Silicon 上会预热 Metal shader 缓存。每台机器做一次,每次升级运行时后再做一次。
Step 5:复用 system prompt 的 prefill,而不是重复处理
import requests
# llama-server 会自动复用相同前缀的 KV cache:cache_prompt 默认就是 true。
# 第一个请求为 system prompt 付一次 prefill 代价,之后相同前缀的请求直接跳过。
response = requests.post("http://localhost:8080/v1/chat/completions", json={
"messages": [
{"role": "system", "content": FIXED_SYSTEM_PROMPT},
{"role": "user", "content": "first question"},
],
"cache_prompt": True, # 当前版本已是默认值,这里写出来只为清晰
})
要让这份 KV cache 跨服务重启也保留,用 --slot-save-path /path/to/cache 启动 llama-server,再用 --cache-reuse N 开启跨请求复用(最小可复用块大小,默认 0 表示关闭)。基于主机内存的 prompt 缓存由 -cram, --cache-ram N 控制(默认 8192 MiB,-1 表示不限,0 表示禁用)——在 16 GB 的笔记本上设 --cache-ram 0,免得缓存挤占了实时 KV cache。
Step 6:用 —no-mmap 和 —mlock 消除 mmap 缺页中断
./llama-server \
-m model.gguf \
--no-mmap \ # 加载时整体读入模型(加载慢,但没有缺页中断卡顿)
--mlock \ # 把权重锁在内存里,防止换出/压缩
--n-gpu-layers all \
--port 8080
--no-mmap 会让加载本身变慢(整个文件一次性读完),但消除了首次 prefill 的缺页中断卡顿。--mlock 强制系统把模型保留在内存里,而不是换出或压缩它,这样短暂空闲也不会把页换走。对延迟敏感的单用户场景,两个一起用。
如何确认已修复
ollama ps(或 LM Studio 的已加载模型列表)在你发 prompt 之前就显示模型已常驻。- 冷的第一次请求和热的第二次请求 TTFT 几乎一样——原本 30-120 秒的差距消失了。
- 用
ollama run --verbose,热模型上的load duration接近 0(之前它占了 TTFT 的大头);如果剩下的prompt eval duration仍然很大,说明只是 prompt 太长(原因 7),而不是模型冷。
预防建议
- 在生产环境把
OLLAMA_KEEP_ALIVE设为-1(或大于你空闲间隔的时长),让模型不会在请求间被卸载。 - 在服务接收用户流量之前,加一个启动预热请求(空
prompt)。 - 把 GGUF 存在 NVMe 上,绝不要放 SATA SSD 或 HDD——在 40 GB 以上时,仅这一项就能把首次冷启动减少 60-80%。
- 在任何界面上,模型冷加载期间显示”正在加载模型”的提示,免得用户以为卡死了。
- 把 TTFT 和 token 间延迟分开监控,否则冷启动看起来就像一次普通的延迟尖刺。
- 在共享/多租户服务器上,跑一个保活心跳(定期发空请求)让模型保持热状态。
- 把 shader 预热推理写进部署/CI 流程,让
ComputeCache/Metal 缓存在第一波流量之前就准备好。
常见问答 (FAQ)
Q:为什么第一次很慢,第二次却总是飞快?
A:第一次请求之后,权重已在显存里,CUDA/Metal shader 已编译并缓存,GGUF 也在系统页缓存里。只要模型保持加载,这三项冷启动开销就都没了——而 OLLAMA_KEEP_ALIVE=-1 正是保证模型一直加载着。
Q:怎么把模型加载时间和 prefill 时间分开看?
A:用 ollama run --verbose:load duration 是模型加载(冷启动)开销,prompt eval duration 是 prefill 开销。load duration 大说明模型是冷的;prompt eval duration 大说明 prompt 太长。在 llama-server 上,先预热模型,再给第二次请求计时,那第二次的 TTFT 就是纯 prefill。
Q:--n-gpu-layers 会影响冷启动时间吗?
A:会。用 --n-gpu-layers all(或 auto 解析为全部)时,每一层都要从内存拷进显存,这是额外的一步冷启动开销。少卸载几层会缩小这次拷贝,但推理会变慢。截至 2026 年 6 月默认是 auto;只有在调某个特定显存预算时才需要写死一个数字。
Q:Apple Silicon 的冷启动比 NVIDIA 快吗?
A:通常更快。Apple 的统一内存意味着没有单独的”内存到显存”拷贝步骤。Metal shader 编译在首次运行时加 5-15 秒,但之后会缓存。13B Q4_K_M 在 M3 Max 上冷启动一般 3-8 秒,而 4090 上要 15-30 秒——但运行时升级会重新触发 Metal 编译。
Q:能不能跨服务或容器重启保留已加载状态?
A:显存状态本身不能。实用做法是让进程一直跑着(keep_alive: -1 加上 Docker restart: unless-stopped 策略),并在开机时重跑预热请求。单就 prefill 而言,llama-server --slot-save-path 能把 KV cache 持久化到磁盘,这样重启后相同的 system prompt 不必重新处理。
相关阅读
- Ollama 探测不到 GPU,全跑在 CPU
- LM Studio 加载模型时显存 OOM
- llama.cpp 在网络盘上 mmap 失败
- 多 GPU 没分配上,模型只跑在卡 0
- 本地 Embedding 服务在批量请求下崩溃
标签: #local-llm #ollama #排查