本地模型冷启动后首 token 极慢

本地 LLM 第一次请求时等待 10-60 秒才开始输出,后续请求正常。定位模型加载、KV cache 预热、GPU 初始化等冷启动瓶颈并给出可执行的优化方案。

Ollama 0.4 服务在请求到达后才开始加载模型(懒加载),RTX 3090 / 24 GB 显存下加载 Llama-3-8B Q4_K_M 需要 15-25 秒,之后同样的请求只需要 0.3 秒(Time to First Token,TTFT)。用户体验上,第一次问问题需要等半分钟才有响应,而后续对话流畅。这是典型的冷启动问题:模型文件从磁盘读入显存的过程决定了首 token 延迟。

常见原因

1. Ollama 的模型懒加载 + 自动 unload 策略

Ollama 默认在收到请求时才加载模型,并在 5 分钟(可配置)无请求后自动卸载模型释放显存。下一次请求到来时需要重新加载,触发冷启动。对于 70B 模型,冷启动可能长达 60-90 秒。

怎么判断:请求间隔超过 5 分钟后再次请求,TTFT 是否明显变长;ollama ps 查看模型是否还在内存中(若无输出说明已卸载)。

2. 模型文件在 HDD 而非 SSD,读取速度成为瓶颈

从 HDD 读取 40 GB 的 Q4_K_M 文件(顺序读取约 150 MB/s),需要约 267 秒;NVMe SSD(顺序读取 3000-5000 MB/s)只需要 8-13 秒。若模型存储在机械硬盘上,冷启动时间可能长达几分钟。

怎么判断df -h ~/.ollama/models 确认所在分区;hdparm -Tt <device>dd if=/dev/zero of=/tmp/test bs=1M count=1000 oflag=direct 测试磁盘顺序读写速度。

3. CUDA 初始化需要编译 JIT kernel

NVIDIA GPU 在某些驱动版本下,首次运行特定 CUDA kernel 时会触发 JIT(Just-In-Time)编译,时间可能长达 10-30 秒。这发生在 GPU 计算开始前,表现为模型文件已加载但 TTFT 仍然很长。

怎么判断nvidia-smi 观察加载阶段 GPU 利用率是否有一段时间保持在 10-30%(JIT 编译阶段),而非 0%(等待)或 90%以上(推理阶段)。

4. mmap 懒加载导致首批 token 时有大量缺页中断

llama.cpp 默认用 mmap 加载模型文件,启动很快,但实际权重数据直到被访问时才从磁盘读取。首次推理时大量缺页中断(page fault)会导致前几个 token 的生成速度极慢,后续 token 才恢复正常速度。

怎么判断perf stat -e page-faults ./llama-cli -m model.gguf -p "hello" -n 10 2>&1 | grep page-fault;若 page-fault 数量超过 10 万次,就是缺页中断问题。

5. Apple Silicon 上 Metal shader 首次编译

macOS Metal 后端在首次运行时需要编译 Metal shader,在 M2/M3 上可能需要 5-15 秒。编译完成后结果会被缓存(~/Library/Caches/),后续启动不再重复编译,但清除缓存或更新 Ollama 版本后需要重新编译。

怎么判断:查看 Ollama 日志中是否有 compiling shader 相关信息;在 macOS Activity Monitor 中观察首次加载时 GPU 是否有一段高 CPU 占用(shader 编译在 CPU 上进行)。

6. 多模型竞争显存导致频繁 swap

若 Ollama 配置了同时保持多个模型在显存中(OLLAMA_MAX_LOADED_MODELS=2),模型间频繁换入换出,每次都触发冷启动延迟。

怎么判断ollama ps 查看当前加载的模型列表;若有 2 个以上模型在内存中,检查显存是否不够。

最短修复路径

Step 1:延长 Ollama 的模型保活时间

# 方法 1:设置环境变量(在服务启动前设置)
export OLLAMA_KEEP_ALIVE=30m  # 保持 30 分钟,默认 5 分钟

# 设置为 -1 表示永不卸载
export OLLAMA_KEEP_ALIVE=-1

# macOS Launch Agent 配置
# 编辑 ~/Library/LaunchAgents/com.ollama.ollama.plist
# 在 EnvironmentVariables 节点下添加 OLLAMA_KEEP_ALIVE

# 方法 2:在每次 API 请求中指定 keep_alive
curl http://localhost:11434/api/generate -d '{
  "model": "llama3:8b",
  "prompt": "hello",
  "keep_alive": "30m"
}'

Step 2:预加载模型(发送空请求)

# 在服务启动后立即发送一个预热请求,让模型提前加载
# 可以加到服务启动脚本的最后
curl -s http://localhost:11434/api/generate -d '{
  "model": "llama3:8b",
  "prompt": "",
  "keep_alive": "1h"
}' > /dev/null

echo "模型预热完成"

Step 3:使用 —mlock 锁定模型到内存

# llama.cpp 启动时锁定内存,防止 swap 和被 OS 回收
./llama-server \
  -m model-Q4_K_M.gguf \
  --mlock \
  -ngl 35 \
  --ctx-size 4096 \
  --host 0.0.0.0 \
  --port 8080

# 验证 mlock 效果
# 1. 加载完成后 nvidia-smi 查看显存已满
# 2. 长时间不用后 ollama ps 仍显示模型在内存中

Step 4:将模型文件迁移到 NVMe SSD

# 查看当前 ollama 模型目录
ls -lh ~/.ollama/models/blobs/ | head -5

# 若在 HDD 上,迁移到 SSD
# 方法:使用 OLLAMA_MODELS 环境变量指向 SSD 路径
export OLLAMA_MODELS=/path/to/ssd/ollama_models

# 将现有模型拷贝过去
rsync -avh ~/.ollama/models/ /path/to/ssd/ollama_models/

# 验证加载速度(计时)
time ollama run llama3:8b "hello" --verbose 2>&1 | grep "load duration"

Step 5:用 --numa--no-mmap 消除缺页中断

# 对于首 token 延迟敏感的场景,启动时完全预读模型到内存
./llama-server \
  -m model-Q4_K_M.gguf \
  --no-mmap \
  --mlock \
  -ngl 35 \
  --ctx-size 4096

# --no-mmap:启动时完整读取模型(启动慢但推理快)
# --mlock:锁定内存防止换出

预防建议

  • 将常用模型的保活时间设为至少 30 分钟,覆盖典型用户对话的间隔。
  • 应用启动时发送一个预热请求,避免第一个真实用户遇到冷启动。
  • 将模型文件存储在 NVMe SSD 上,机械硬盘的加载速度会让冷启动不可接受。
  • 对于 7B 以下模型,使用 Q8_0 量化提升推理质量;对于 70B 模型,使用 Q4_K_M 控制加载时间。
  • 在生产环境中监控 TTFT(Time to First Token)指标,设置告警阈值(如超过 5 秒),及时发现模型被卸载导致的冷启动。
  • 若有多个模型需要热加载,配置足够大的 OLLAMA_MAX_LOADED_MODELS 同时保持多个模型在显存中,避免频繁换入换出。

常见问答 (FAQ)

Q: ollama run--verbose 参数里的 “load duration” 是什么意思? A: “load duration” 是模型从磁盘加载到 GPU 显存的时间。它与 TTFT(首 token 时间)的差值就是 prompt 评估时间(prompt eval duration)。load duration 大说明冷启动慢;prompt eval duration 大说明 prompt 太长。

Q: 能不能用 Docker 持久化模型加载状态,跨容器重用? A: 不能直接持久化 GPU 显存状态。但可以用 --keep-alive -1 + Docker restart policy 让容器和 Ollama 服务保持长期运行,模型不被卸载,效果相当于持久化加载。

Q: 为什么 LM Studio 的冷启动比 Ollama 快? A: LM Studio 默认不会自动卸载模型,加载一次后保持在显存中直到手动卸载。Ollama 的懒加载和自动卸载策略更适合服务器场景(多用户共享),但对单用户场景会带来冷启动问题。设置 OLLAMA_KEEP_ALIVE=-1 可以让 Ollama 的行为接近 LM Studio。

Q: 首次请求后的第二次请求也慢,只有第三次才正常,为什么? A: 可能是 KV cache 预热问题。首次请求评估 prompt 时填充了 KV cache,若 KV cache 还在”冷”状态(GPU L2 cache 未命中),第二次请求可能也偏慢。第三次请求时 GPU cache 已经热了,延迟才完全正常。这种情况通常在 KV cache 较大时更明显。

相关阅读

标签: #local-llm #ollama #排查