Ollama: 性能调优
性能调优是给本地 AI 引擎换挡——从一档爬坡到五档飞驰。
💡 提示:
OLLAMA_NUM_PARALLEL 控制单模型并行请求数——设为 4 可同时处理 4 个请求,提升吞吐量。8GB VRAM 建议从 2-4 开始测试,16GB 可设 4-8。配合 OLLAMA_KEEP_ALIVE=30m 避免模型频繁卸载重载。
📋 前置知识:需要先掌握以下内容
- 第9课:GPU与CUDA配置
- 第15课:Docker容器化部署
1. 你将学到
- 关键性能指标:Tokens/s、TTFT、内存占用
- 并发请求调优:OLLAMA_NUM_PARALLEL 与 OLLAMA_MAX_LOADED_MODELS
- 上下文窗口管理:num_ctx 与 KV Cache
- 批处理请求与队列调度
- 基准测试工具与性能基线建立
2. 一个 SaaS 创业者的真实故事
💡 提示:
OLLAMA_KEEP_ALIVE 设置模型在内存中的驻留时间。高并发场景建议设为 30m 或更长,避免模型频繁卸载/重新加载导致冷启动延迟(5-30 秒)。低频使用可设 5m 释放 VRAM。
ℹ️ 信息:
num_ctx(上下文窗口大小)直接影响 KV Cache 内存占用。8B 模型 num_ctx=2048 约需 4GB VRAM,num_ctx=8192 约需 6GB,num_ctx=32768 约需 12GB。按需设置,不要盲目调大。
(1) 痛点:高并发下响应超时
Alice 的 SupportBot 上线后,高峰期 10 个并发请求导致响应时间从 2 秒飙升到 30 秒。客户投诉"等太久",但 Ollama 默认只处理 1 个并发请求。
(2) 解法:并发与上下文优化
调整 和 ,响应时间恢复到 5 秒内:
BASH
# Enable 4 parallel requests
export OLLAMA_NUM_PARALLEL=4
export OLLAMA_MAX_LOADED_MODELS=2
3. 关键性能指标
⚠️ 警告: 增大
OLLAMA_NUM_PARALLEL 不等于线性提升吞吐——每个并发请求共享 GPU VRAM,并发数过高可能导致 OOM 或单个请求速度骤降。建议从 2 开始逐步测试,观察 VRAM 使用和延迟变化。
(1) 三大核心指标
| 指标 | 全称 | 含义 | 目标值 |
|---|---|---|---|
| Tokens/s | Tokens per second | 生成速度 | GPU: 30+,CPU: 5+ |
| TTFT | Time to First Token | 首 token 延迟 | < 500ms (GPU) |
| Memory | VRAM/RAM 占用 | 资源消耗 | < 90% 容量 |
(2) Ollama 性能统计
或非流式响应中包含的性能数据:
| 字段 | 含义 |
|---|---|
| total_duration | 总耗时(纳秒) |
| load_duration | 模型加载时间 |
| prompt_eval_count | 输入 token 数 |
| prompt_eval_duration | 输入处理时间 |
| eval_count | 输出 token 数 |
| eval_duration | 输出生成时间 |
▶ 示例 1: 性能指标采集
BASH
# Collect performance metrics with verbose output
ollama run --verbose qwen2.5 "Explain AI in 50 words"
# Key output:
# total duration: 2500000000 # 2.5s total
# load duration: 500000000 # 0.5s model load
# prompt eval count: 15 token(s)
# prompt eval duration: 200000000 # 0.2s input processing
# prompt eval count: 15 token(s) # = prompt_eval_speed: 75 tok/s
# eval count: 52 token(s)
# eval duration: 1800000000 # = eval_speed: 28.9 tok/s
输出:
TEXT
📖 仅展示
I'm a helpful AI assistant running locally on your machine...
PYTHON
import ollama
import time
def measure_performance(model: str, prompt: str) -> dict:
start = time.time()
response = ollama.chat(
model=model,
messages=[{"role": "user", "content": prompt}],
stream=False
)
total_time = time.time() - start
content = response["message"]["content"]
token_count = len(content) // 4 # Rough estimate
return {
"model": model,
"total_time_s": round(total_time, 2),
"estimated_tokens": token_count,
"estimated_tok_per_s": round(token_count / total_time, 1) if total_time > 0 else 0
}
result = measure_performance("qwen2.5", "Explain machine learning in 100 words")
print(result)
4. 并发请求调优
⚠️ 注意:并发过高可能导致 OOM——
OLLAMA_NUM_PARALLEL 不等于线性提升吞吐,每个并发请求共享 GPU VRAM,并发数过高会导致单个请求速度骤降甚至内存溢出。建议从 2 开始逐步测试,观察 VRAM 使用和延迟变化。
(1) 并发相关环境变量
| 变量 | 默认值 | 说明 | 影响 |
|---|---|---|---|
| OLLAMA_NUM_PARALLEL | 1 | 单模型并行请求数 | ↑吞吐 ↓单请求速度 |
| OLLAMA_MAX_LOADED_MODELS | 1 | 最大同时加载模型数 | ↑多模型并发 ↑VRAM |
| OLLAMA_KEEP_ALIVE | 5m | 模型驻留内存时间 | ↑避免重载 ↓内存回收 |
(2) 并发配置决策
flowchart LR
A[Concurrent Requests?] --> B{Peak QPS?}
B -->|1-2| C[Default: NUM_PARALLEL=1]
B -->|3-5| D[NUM_PARALLEL=4<br/>8GB VRAM minimum]
B -->|6-10| E[NUM_PARALLEL=8<br/>16GB+ VRAM]
B -->|10+| F[Multi-node<br/>Load Balancer]
| 配置 | VRAM 需求 | 吞吐 | 单请求延迟 |
|---|---|---|---|
| NUM_PARALLEL=1 | 最低 | 1 req/s | 最短 |
| NUM_PARALLEL=4 | 中等 | 3-4 req/s | 略增 |
| NUM_PARALLEL=8 | 高 | 6-8 req/s | 明显增加 |
▶ 示例 2: 并发配置与测试
BASH
# Configure parallel processing
sudo systemctl edit ollama
# Add:
[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_MAX_LOADED_MODELS=2"
Environment="OLLAMA_KEEP_ALIVE=30m"
sudo systemctl daemon-reload
sudo systemctl restart ollama
输出:
TEXT
📖 仅展示
# Ollama 命令执行成功
PYTHON
import ollama
import asyncio
import time
async def concurrent_test(num_requests: int):
"""Test concurrent request handling."""
client = ollama.AsyncClient()
start = time.time()
async def single_request(i: int):
req_start = time.time()
response = await client.chat(
model="qwen2.5",
messages=[{"role": "user", "content": f"Say hello {i}"}],
stream=False,
options={"temperature": 0.1}
)
return time.time() - req_start
tasks = [single_request(i) for i in range(num_requests)]
latencies = await asyncio.gather(*tasks)
total = time.time() - start
print(f"Concurrent: {num_requests} requests")
print(f"Total time: {total:.2f}s")
print(f"Avg latency: {sum(latencies)/len(latencies):.2f}s")
print(f"Throughput: {num_requests/total:.1f} req/s")
asyncio.run(concurrent_test(4))
5. 上下文窗口管理
(1) num_ctx 对内存的影响
| num_ctx | KV Cache (8B Q4_M) | VRAM 总需求 | 适用场景 |
|---|---|---|---|
| 2048 | ~1 GB | ~6 GB | 短对话(默认) |
| 4096 | ~2 GB | ~7 GB | 中等对话 |
| 8192 | ~4 GB | ~9 GB | RAG 检索 |
| 32768 | ~16 GB | ~21 GB | 长文档 |
⚠️ 注意: KV Cache 随 num_ctx 线性增长。8B 模型 num_ctx=32768 在 8GB VRAM 上无法运行,需要 21GB+ VRAM。
(2) num_ctx 优化策略
| 策略 | 方法 | 效果 |
|---|---|---|
| 按需设置 | 客服用 2048,RAG 用 8192 | 减少内存浪费 |
| 滑动窗口 | 只保留最近 N 轮对话 | 控制输入长度 |
| 摘要压缩 | 定期压缩早期对话 | 节省 context 空间 |
| RAG 精选 | 减少 top-k 从 5 到 3 | 减少输入 token |
▶ 示例 3: num_ctx 对比测试
PYTHON
import ollama
import time
def test_context_sizes(model: str, prompt: str, context_sizes: list[int]):
"""Test performance with different context window sizes."""
for ctx in context_sizes:
start = time.time()
response = ollama.chat(
model=model,
messages=[{"role": "user", "content": prompt}],
stream=False,
options={"num_ctx": ctx, "temperature": 0.3}
)
elapsed = time.time() - start
tokens = len(response["message"]["content"]) // 4
speed = tokens / elapsed if elapsed > 0 else 0
print(f"num_ctx={ctx}: {elapsed:.2f}s, ~{speed:.1f} tok/s")
test_context_sizes("qwen2.5", "Explain AI briefly", [2048, 4096, 8192])
输出:
TEXT
📖 仅展示
# 函数定义成功
6. 批处理与基准测试
(1) 批处理策略
| 策略 | 说明 | 适用 |
|---|---|---|
| 串行批处理 | 逐个处理请求 | 简单可靠 |
| 并行批处理 | asyncio 并发 | 高吞吐 |
| 队列调度 | FastAPI + 队列 | 生产环境 |
▶ 示例 4: 批处理基准测试脚本
PYTHON
import ollama
import time
import asyncio
import json
from datetime import datetime
class BenchmarkRunner:
def __init__(self, model: str = "qwen2.5"):
self.model = model
self.results = []
def warmup(self, runs: int = 2):
for _ in range(runs):
ollama.chat(model=self.model,
messages=[{"role": "user", "content": "warmup"}],
stream=False)
def single_benchmark(self, prompt: str) -> dict:
start = time.time()
response = ollama.chat(
model=self.model,
messages=[{"role": "user", "content": prompt}],
stream=False,
options={"temperature": 0.3}
)
elapsed = time.time() - start
content = response["message"]["content"]
return {
"prompt_length": len(prompt),
"response_length": len(content),
"estimated_tokens": len(content) // 4,
"total_time_s": round(elapsed, 2),
"tok_per_s": round(len(content) / 4 / elapsed, 1) if elapsed > 0 else 0
}
async def concurrent_benchmark(self, prompt: str, concurrency: int) -> dict:
client = ollama.AsyncClient()
start = time.time()
tasks = [
client.chat(model=self.model,
messages=[{"role": "user", "content": prompt}],
stream=False, options={"temperature": 0.3})
for _ in range(concurrency)
]
await asyncio.gather(*tasks)
total = time.time() - start
return {
"concurrency": concurrency,
"total_time_s": round(total, 2),
"throughput_rps": round(concurrency / total, 1)
}
def run_full_benchmark(self):
self.warmup()
prompts = [
"Hello",
"Explain AI in 3 sentences",
"Write a product description for wireless headphones"
]
print("=== Single Request Benchmark ===")
for p in prompts:
result = self.single_benchmark(p)
print(f" {result['estimated_tokens']}tok, {result['tok_per_s']}tok/s, {result['total_time_s']}s")
print("\n=== Concurrent Benchmark ===")
for c in [1, 2, 4]:
result = asyncio.run(self.concurrent_benchmark("Hello", c))
print(f" Concurrency {c}: {result['throughput_rps']} req/s")
# Run
runner = BenchmarkRunner("qwen2.5")
runner.run_full_benchmark()
输出:
TEXT
📖 仅展示
=== Single Request Benchmark ===
\n=== Concurrent Benchmark ===
▶ 示例 5: 性能基线建立
BASH
#!/bin/bash
# Establish performance baseline
MODEL="qwen2.5"
DATE=$(date +%Y%m%d)
REPORT="baseline_${DATE}.txt"
echo "=== Performance Baseline ===" > "$REPORT"
echo "Date: $(date)" >> "$REPORT"
echo "Model: $MODEL" >> "$REPORT"
echo "GPU: $(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || echo 'CPU')" >> "$REPORT"
echo "" >> "$REPORT"
# Single request benchmark
echo "## Single Request ##" >> "$REPORT"
for i in {1..5}; do
start=$(date +%s%N)
curl -s http://localhost:11434/api/chat -d "{
\"model\": \"$MODEL\",
\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}],
\"stream\": false
}" > /dev/null
end=$(date +%s%N)
elapsed=$(( (end - start) / 1000000 ))
echo " Run $i: ${elapsed}ms" >> "$REPORT"
done
echo "" >> "$REPORT"
echo "## GPU Utilization ##" >> "$REPORT"
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv >> "$REPORT" 2>/dev/null || echo "CPU mode" >> "$REPORT"
echo "Baseline saved: $REPORT"
输出:
TEXT
📖 仅展示
{"status":"ok","data":{}}
7. 综合示例:SupportBot 性能调优报告
PYTHON
# ============================================
# Comprehensive: Performance tuning report
# Full benchmark + optimization recommendations
# ============================================
import ollama
import time
import asyncio
import json
class PerformanceTuner:
def __init__(self, model: str = "qwen2.5"):
self.model = model
def run_diagnostics(self) -> dict:
"""Run full performance diagnostics."""
# Warm up
ollama.chat(model=self.model,
messages=[{"role": "user", "content": "warmup"}],
stream=False)
# Single request test
start = time.time()
resp = ollama.chat(
model=self.model,
messages=[{"role": "user", "content": "Hello"}],
stream=False, options={"temperature": 0.3}
)
single_latency = time.time() - start
# Context size test
ctx_results = {}
for ctx in [2048, 4096, 8192]:
start = time.time()
ollama.chat(
model=self.model,
messages=[{"role": "user", "content": "Hello"}],
stream=False, options={"num_ctx": ctx, "temperature": 0.3}
)
ctx_results[ctx] = round(time.time() - start, 2)
return {
"model": self.model,
"single_latency_s": round(single_latency, 2),
"context_sizes": ctx_results,
"recommendations": self._generate_recommendations(single_latency)
}
def _generate_recommendations(self, latency: float) -> list[str]:
recs = []
if latency > 5:
recs.append("High latency detected. Enable GPU acceleration or use smaller model.")
if latency > 2:
recs.append("Set OLLAMA_NUM_PARALLEL=4 for concurrent requests.")
recs.append("Use num_ctx=2048 for chat, num_ctx=8192 for RAG.")
recs.append("Set OLLAMA_KEEP_ALIVE=30m to avoid model reloading.")
return recs
def generate_report(self) -> str:
diag = self.run_diagnostics()
report = [
f"# Performance Tuning Report",
f"Model: {diag['model']}",
f"Single request latency: {diag['single_latency_s']}s",
f"\n## Context Window Impact:",
]
for ctx, latency in diag["context_sizes"].items():
report.append(f" num_ctx={ctx}: {latency}s")
report.append("\n## Recommendations:")
for r in diag["recommendations"]:
report.append(f" - {r}")
return "\n".join(report)
tuner = PerformanceTuner("qwen2.5")
print(tuner.generate_report())
❓ 常见问题
Q OLLAMA_NUM_PARALLEL 设多少合适?
A 8GB VRAM 设 2-4,16GB 设 4-8,24GB 设 8。过多并行会导致单请求延迟增加和 OOM。从 4 开始测试。
Q TTFT 太高怎么办?
A 1) 确保 OLLAMA_KEEP_ALIVE 足够长(避免冷启动);2) 减少 num_ctx;3) 用更小的模型;4) 确保 GPU 加速生效。
Q 如何减少模型加载时间?
A 设置 OLLAMA_KEEP_ALIVE=30m 或更长,让模型常驻内存。首次加载不可避免,后续请求秒级响应。
Q 并发请求时 OOM 怎么办?
A 降低 OLLAMA_NUM_PARALLEL;用更小的 num_ctx;减少 OLLAMA_MAX_LOADED_MODELS;或增加 VRAM。
Q 基准测试结果不稳定怎么办?
A 1) 热身 2-3 次消除冷启动;2) 关闭其他 GPU 程序;3) 取 5 次以上平均;4) 固定 random seed。
Q 如何监控生产环境性能?
A Lesson 21 将详解 Prometheus + Grafana 监控栈。现阶段可用 和自定义脚本采集指标。
📖 小节
- 三大核心指标:Tokens/s(速度)、TTFT(延迟)、Memory(资源)
- OLLAMA_NUM_PARALLEL 控制并发,4 是 8GB VRAM 的起点
- num_ctx 线性影响 KV Cache 内存,按需设置:对话 2048、RAG 8192
- 基准测试需热身+多次平均,建立性能基线追踪变化
- OLLAMA_KEEP_ALIVE 避免冷启动,设 30m 适合高频服务
- 生产调优:先测单请求→再测并发→最后测长上下文
📝 作业
- 基础题(难度⭐):运行 模式,记录 3 次推理的 Tokens/s、TTFT 等指标,建立你的性能基线。
- 进阶题(难度⭐⭐):配置 OLLAMA_NUM_PARALLEL=4,运行并发测试,对比并发前后的吞吐和延迟变化。
- 挑战题(难度⭐⭐⭐):编写完整性能调优报告——包含单请求基准、并发测试、num_ctx 对比、优化建议,输出 Markdown 格式报告。