Ollama: パフォーマンスチューニング
パフォーマンスチューニングはローカルAIエンジンのギアシフト——1速の這い進みから5速の疾走へ。
💡 ヒント:
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キャッシュ
- バッチリクエスト処理とキュースケジューリング
- ベンチマークツールとパフォーマンスベースラインの確立
2. SaaS起業家のリアルな事例
💡 ヒント:
OLLAMA_KEEP_ALIVEはモデルがメモリに留まる時間を設定。高同時実行シナリオでは30m以上に設定し、頻繁なモデルのアンロード/再ロードによるコールドスタート遅延(5〜30秒)を回避。使用頻度が低い場合は5mに設定してVRAMを解放。
ℹ️ 情報:
num_ctx(コンテキストウィンドウサイズ)はKVキャッシュのメモリ使用量に直接影響する。8Bモデルでnum_ctx=2048は約4GB VRAM、num_ctx=8192は約6GB、num_ctx=32768は約12GBが必要。実際のニーズに基づいて設定し、むやみに増やさないこと。
(1) ペインポイント:高同時実行時の応答タイムアウト
AliceのSupportBotが本番稼働し、ピーク時には10の同時リクエストで応答時間が2秒から30秒に急増。顧客から「長すぎる」と苦情が来たが、Ollamaはデフォルトで1つの同時リクエストしか処理しない。
(2) ソリューション:同時実行とコンテキストの最適化
OLLAMA_NUM_PARALLELとOLLAMA_MAX_LOADED_MODELSを調整し、応答時間は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) 3つのコア指標
| 指標 | フルネーム | 意味 | 目標値 |
|---|---|---|---|
| Tokens/s | Tokens per second | 生成速度 | GPU: 30以上、CPU: 5以上 |
| TTFT | Time to First Token | 最初のトークンレイテンシ | 500ms未満(GPU) |
| Memory | VRAM/RAM使用量 | リソース消費 | 容量の90%未満 |
(2) Ollamaパフォーマンス統計
--verboseモードまたは非ストリーミングレスポンスに含まれるパフォーマンスデータ:
| フィールド | 意味 |
|---|---|
| total_duration | 合計時間(ナノ秒) |
| load_duration | モデルロード時間 |
| prompt_eval_count | 入力トークン数 |
| prompt_eval_duration | 入力処理時間 |
| eval_count | 出力トークン数 |
| eval_duration | 出力生成時間 |
(3) ▶ サンプル:パフォーマンス指標収集
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[同時リクエスト?] --> B{ピークQPS?}
B -->|1〜2| C[デフォルト: NUM_PARALLEL=1]
B -->|3〜5| D[NUM_PARALLEL=4<br/>8GB VRAM最低]
B -->|6〜10| E[NUM_PARALLEL=8<br/>16GB以上 VRAM]
B -->|10以上| F[マルチノード<br/>ロードバランサー]
| 設定 | VRAM要件 | スループット | リクエストごとのレイテンシ |
|---|---|---|---|
| NUM_PARALLEL=1 | 最低 | 1 req/s | 最短 |
| NUM_PARALLEL=4 | 中程度 | 3〜4 req/s | やや増加 |
| NUM_PARALLEL=8 | 高 | 6〜8 req/s | 顕著に増加 |
(3) ▶ サンプル:同時実行設定とテスト
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 command executed successfully
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キャッシュ (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キャッシュはnum_ctxに比例して増大。8Bモデルでnum_ctx=32768は8GB VRAMでは動作せず、21GB以上のVRAMが必要。
(2) num_ctx最適化戦略
| 戦略 | 方法 | 効果 |
|---|---|---|
| オンデマンド設定 | チャットは2048、RAGは8192 | メモリ無駄を削減 |
| スライディングウィンドウ | 直近N往復の会話のみ保持 | 入力長を制御 |
| 要約圧縮 | 早期の会話を定期的に圧縮 | コンテキスト空間を節約 |
| RAGキュレーション | top-kを5から3に削減 | 入力トークンを削減 |
(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
# Function defined successfully
6. バッチ処理とベンチマーク
(1) バッチ処理戦略
| 戦略 | 説明 | ユースケース |
|---|---|---|
| シリアルバッチ | リクエストを順次処理 | シンプルで信頼性が高い |
| パラレルバッチ | asyncio同時実行 | 高スループット |
| キュースケジューリング | FastAPI + キュー | 本番環境 |
(2) ▶ サンプル:バッチベンチマークスクリプト
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 ===
=== Concurrent Benchmark ===
(3) ▶ サンプル:パフォーマンスベースライン確立
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)ランダムシードを固定。
Q 本番環境のパフォーマンスをモニタリングするには?
A レッスン21でPrometheus + Grafanaモニタリングスタックを詳述。今は
--verboseとカスタムスクリプトで指標を収集。📖 まとめ
- 3つのコア指標:Tokens/s(速度)、TTFT(レイテンシ)、Memory(リソース)
- OLLAMA_NUM_PARALLELが同時実行を制御;8GB VRAMでは4が開始点
- num_ctxはKVキャッシュメモリに比例影響;オンデマンド設定:チャットは2048、RAGは8192
- ベンチマークにはウォームアップ+複数回実行の平均が必要;ベースライン確立で変化を追跡
- OLLAMA_KEEP_ALIVEでコールドスタート回避;高頻度サービスは30mに設定
- 本番チューニング:単一リクエストテスト → 同時実行 → 長コンテキストの順
📝 練習問題
- 基本(難易度 ⭐):
--verboseモードを実行し、3回の推論でTokens/s、TTFTなどの指標を記録し、パフォーマンスベースラインを確立する。 - 中級(難易度 ⭐⭐):OLLAMA_NUM_PARALLEL=4を設定し、同時テストを実行し、同時実行前後のスループットとレイテンシの変化を比較する。
- 上級(難易度 ⭐⭐⭐):完全なパフォーマンスチューニングレポートを作成——単一リクエストベンチマーク、同時テスト、num_ctx比較、最適化推奨を含め——Markdown形式で出力する。