はじめに
「8Bクラスのモデルをローカルで動かすと遅い」——その遅さは、実は設定次第で3倍以上変わります。本記事では /api/generate のレスポンスに含まれる計測値で生成速度(tok/s)を正しく測り、同じ qwen3:8b が 9.1 tok/s → 33.3 tok/s に改善した実測を紹介します。
環境
- Windows 11(ビルド 10.0.26200)
- GPU: RTX 4060(VRAM 8GB)
- Ollama 0.40.4(既定の
http://127.0.0.1:11434) - モデル:
qwen2.5:0.5b/qwen3:8b - Python 3.11(標準ライブラリのみ。OllamaのPythonライブラリ不要)
やったこと
1. 計測スクリプト
/api/generate のJSONレスポンスには計測値が含まれている。ここから eval_count(生成トークン数)と eval_duration(生成にかかった時間)を取り出して tok/s を計算する。
import json
import time
import urllib.request
def gen(model, num_ctx=None, think=None):
options = {"num_predict": 32, "temperature": 0}
if num_ctx:
options["num_ctx"] = num_ctx
payload = {
"model": model,
"prompt": "1から10まで数えて。数字だけをスペース区切りで。",
"stream": False,
"options": options,
}
if think is not None:
payload["think"] = think
req = urllib.request.Request(
"http://127.0.0.1:11434/api/generate",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=600) as res:
d = json.loads(res.read())
ev, ev_s = d["eval_count"], d["eval_duration"] / 1e9
print(f"{model} ctx={num_ctx}: load {d['load_duration'] / 1e9:.1f}s / "
f"{ev} tok を {ev_s:.2f}s → {ev / ev_s:.1f} tok/s")
gen("qwen2.5:0.5b") # 1回目: ロード込み
gen("qwen2.5:0.5b") # 2回目: ウォーム
gen("qwen3:8b") # 既定コンテキスト
gen("qwen3:8b", num_ctx=4096) # コンテキストを絞る
2. 計測結果
| 構成 |
ollama ps のサイズ |
PROCESSOR | 生成速度(2回目以降) |
|---|---|---|---|
| qwen2.5:0.5b(既定 ctx 32768) | 865 MB | 100% GPU | 193.6 tok/s |
| qwen3:8b(既定 ctx 40960) | 11 GB | 46%/54% CPU/GPU | 9.1 tok/s |
| qwen3:8b(num_ctx=4096) | 5.6 GB | 100% GPU | 33.3 tok/s |
コールドスタート(モデルのロード)は 0.5b で約3.3秒、8b(ctx 4096)で約16.6秒だった。
3. なぜ qwen3:8b が 9 tok/s だったのか
実行中に ollama ps を見ると、qwen3:8b は 46%/54% CPU/GPU と表示された。VRAM 8GBのGPUに対して、既定コンテキスト 40960 のKVキャッシュを含むサイズが 11GB あり、全部をGPUに載せられず一部がCPUに落ちていた。
同じモデルでも num_ctx を 4096 に絞ると 5.6GB に収まり、100% GPU で動くようになる。生成速度は 9.1 → 33.3 tok/s(約3.7倍)になった。
gen("qwen3:8b", num_ctx=4096)
長文コンテキストが必要なタスク以外は、num_ctx を必要な分だけ指定するのが効率的、という実感が得られた。
4. thinkingモデル(qwen3)の速度の見え方
qwen3系は既定で「思考トークン」を生成する。num_predict=32 のような短い上限だと、32トークン全部が思考に使われて response が空に見えることがある。思考を切って比べると動きが分かりやすい。
gen("qwen3:8b", num_ctx=4096, think=None) # 既定: 思考あり
gen("qwen3:8b", num_ctx=4096, think=False) # 思考なし: 直接回答
ハマった点
-
total_durationで測ると遅く見える。total_durationにはロード時間(load_duration)やプロンプト評価が含まれる。純粋な生成速度はeval_count ÷ eval_durationで出す。初回計測ほどこの差が大きい。 -
ollama psの PROCESSOR 列を見ないと気づけない。「GPUに載っているか」はタスクマネージャの使用量では判断しづらい。まずollama psで100% GPUか分割かを確認するのが早い。 - 計測と同じマシンで重い処理を並行させると数字が汚染される。筆者は計測中に別ウィンドウでビルド処理を走らせてしまい、初回計測が 2.3 tok/s と出た。条件を揃えて測り直したら 9.1 tok/s だった。ベンチは静かな状態で。
-
思考トークンも
eval_countに含まれる。think のオン/オフで「同じ土俵」にならないので、比較するなら条件を揃える。
まとめ
- 生成速度は
eval_count ÷ eval_durationで測る。total_durationはロード込みなので比較に使わない。 -
ollama psのPROCESSOR列で 100% GPU か CPU/GPU 分割かを必ず確認する。 - VRAMに収まらない場合は
num_ctxを絞る。実測では 9.1 → 33.3 tok/s(3.7倍)に改善した。 - ベンチマークは「ウォーム状態・同一条件・静かなマシン」で。
関連する本
速度計測の手順を含む実測ガイドを一冊にまとめました。第1章・第2章は無料で読めます。