はじめに
GMOコネクトの永田です。
ローカルLLMの推論をもっと速くしたい!
LLMを利用するタスクによっては1件あたり数十秒〜かかり、処理件数が多いとかなりの処理時間になります。
gpt-ossの場合、推論時間の8割はreasoning tokens、つまりモデルが内部で生成する思考過程です。これをOFFにすればタスクによっては12秒が2秒まで縮みます。
ただし、設定方法は推論エンジンごとに違ったり、Ollamaでは見かけだけOFFになる罠もあったりします。
今回、gpt-ossを例に、Think OFFの仕組み、推論エンジン別の設定手順、速度と精度のベンチマーク結果をまとめました。
前回の記事「LLM出力の精度90%→98%に。LLM-as-judgeとClaude Codeで自律チューニング」で予告していた、Think OFF設定手順の記事です。
(最初に)まとめ
chat_templateを1箇所書き換えるだけで、gpt-oss:20bの推論が12.4秒から2.0秒に縮みます。llama-serverとSGLangで有効で、Ollamaでは効きません。
| 推論エンジン | Think OFF | 方法 | Docker |
|---|---|---|---|
| llama-server | 可 |
--chat-template-file でカスタムテンプレート指定 |
不要 |
| SGLang | 可 |
--chat-template でカスタムテンプレート指定 |
必要 |
| Ollama | 不可 | チャネルトークンの独自パース層がテンプレート改変を無効化 | — |
| 設定 | 速度 | 高速化 | 精度影響 |
|---|---|---|---|
| Think ON(デフォルト) | 12.4s/件 | — | 基準 |
| Think OFF | 2.0s/件 | 6.2x | タスク次第で-20pt |
| reasoning_effort=low | 8.1s/件 | 1.5x | Think OFFと同水準 |
| Ollama nothink | 10.4s/件 | 1.2x | 変化なし(実質Think ON) |
ただし精度はタスク次第です。メタデータ抽出では精度を落とさず7.6倍速くなりましたが、レビューコメント分類では20pt落ちました。
reasoning tokenの仕組み
gpt-ossのチャネル構造
gpt-ossは出力をanalysisとfinalの2つのチャネルに分けています。analysisに思考過程、finalに最終回答が入ります。
<|start|>assistant<|channel|>analysis<|message|>
(思考過程: 500〜800 tokens)
<|end|>
<|start|>assistant<|channel|>final<|message|>
(最終回答: 70〜150 tokens)
<|end|>
Think ONではanalysisに思考を書き出してからfinalで回答します。Think OFFではanalysisごとスキップして、いきなりfinalに入ります。
なぜThink OFFが速いのか
推論時間の内訳を見ると、トークン生成のDecodeが全体の79%を占めています。Think OFFにしてreasoningの500〜800 tokensを丸ごと消せば、ここが一気に縮みます。
| フェーズ | Think ON | Think OFF |
|---|---|---|
| Prefill(入力処理) | ~0.5s (3%) | ~0.5s |
| Decode(トークン生成) | ~11.9s (79%) | ~1.2s |
| オーバーヘッド | ~2.6s (18%) | ~0.3s |
| 合計 | ~15s | ~2.0s |
DGX Sparkのメモリ帯域幅は273 GB/s、Decode速度は約59 tokens/秒。生成トークン数を減らすのが最も直接的な高速化です。Prefillは全体の3%なので、Flash AttentionやKVキャッシュ量子化を入れても体感は変わりません。
推論エンジン別の設定方法
llama-server(推奨・Docker不要)
GGUFモデルの準備
まずGGUF形式のモデルをダウンロードします。
# huggingface-hub でダウンロード
pip install huggingface-hub
huggingface-cli download \
ggml-org/gpt-oss-20b-GGUF \
gpt-oss-20b-mxfp4.gguf \
--local-dir ~/models
注意点として、OllamaのGGUFファイルはアーキテクチャ名がgptoss(ハイフンなし)になっています。llama.cppはgpt-oss(ハイフンあり)を期待するため、Ollama由来のGGUFは読めません。ggml-org/gpt-oss-20b-GGUFから取得してください。
chat_templateの仕組み
Think OFFの核心はchat_templateの書き換えです。generation promptに空のanalysisブロックを事前に入れておくと、モデルは「思考はもう終わった」と認識して、直接finalチャネルから回答を始めます。
# オリジナル(Think ON)
<|start|>assistant
# Think OFF版
<|start|>assistant<|channel|>analysis<|message|><|end|><|start|>assistant<|channel|>final<|message|>
テンプレートファイルで書き換えるのは末尾のgeneration prompt部分だけです。
{#- Generation prompt - MODIFIED: skip reasoning by pre-filling empty analysis block #}
{%- if add_generation_prompt -%}
<|start|>assistant<|channel|>analysis<|message|><|end|><|start|>assistant<|channel|>final<|message|>
{%- endif -%}
ベースはgpt-ossの公式chat_template(openai/gpt-oss-20bのchat_template.jinja)で、変更はこのgeneration prompt部分のみです。
起動コマンド
~/llama.cpp/build/bin/llama-server \
--model ~/models/gpt-oss-20b-mxfp4.gguf \
--jinja \
--chat-template-file ~/chat_template_no_reasoning.jinja \
--n-gpu-layers 99 --ctx-size 4096 \
--host 0.0.0.0 --port 8080
--jinjaと--chat-template-fileの両方を付けます。起動ログにthinking = 0と出ればテンプレートが効いています。
Pythonからの呼び出し
OpenAI互換APIなので、ふつうのchat completions呼び出しです。
import json, urllib.request
body = {
"model": "gpt-oss-20b",
"messages": [
{"role": "system", "content": "あなたはデータ分析アシスタントです。"},
{"role": "user", "content": "このテキストを分類してください。"}
],
"temperature": 0.3,
"max_tokens": 4096,
}
req = urllib.request.Request(
"http://localhost:8080/v1/chat/completions",
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=300) as resp:
result = json.loads(resp.read())
print(result["choices"][0]["message"]["content"])
print(f"completion_tokens: {result['usage']['completion_tokens']}")
Think OFFが効いていれば、completion_tokensは70〜150程度に収まります。Think ONだと500〜1000になるので、ここで判別できます。
SGLang(Docker環境)
SGLangはDockerが必要ですが、速度はllama-serverと同等です。
docker run --gpus all \
--shm-size 32g \
-p 30000:30000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-v ~/chat_template_no_reasoning.jinja:/opt/chat_template.jinja:ro \
--env "HF_TOKEN=$HF_TOKEN" \
--ipc=host \
lmsysorg/sglang:spark \
python3 -m sglang.launch_server \
--model-path openai/gpt-oss-20b \
--host 0.0.0.0 --port 30000 \
--chat-template /opt/chat_template.jinja
chat_templateファイルをコンテナにマウントして--chat-templateで指定します。
SGLangには--reasoning-parser gpt-ossというオプションもありますが、これはreasoning tokensをAPIレスポンス上で分離するだけで、生成自体は止まりません。Think OFFにするにはchat_templateの書き換えが要ります。
APIはllama-serverと同じOpenAI互換なので、クライアント側のコード変更は不要です。
Ollama(Think OFF不可)
Ollamaではchat_template改変によるThink OFFができません。4つの方法を試して、すべて失敗しています。
| 方法 | 結果 | 原因 |
|---|---|---|
| Modelfileテンプレートに空analysisブロック注入 | 出力が空 | チャネルトークンの独自パース層が先に消費 |
| Modelfileで直接finalチャネル指定 | 出力が空 | 同上 |
| システムメッセージに「Reasoning: none」 | 速度改善なし(14.8s) | reasoning生成自体は抑制されない |
Ollama API think: false
|
速度改善なし | 非表示にするだけ |
原因はOllamaのアーキテクチャだと思われます。
Ollamaはgpt-ossのチャネルトークンを処理する独自のパーサー(harmonyパッケージ)を持っていて、テンプレートに書いたチャネルトークンがモデルに届く前にOllama側で消費されるようです。Ollama Issueでもno thinkingが効かないという内容がありました。
なお、llama-serverやSGLangはJinjaテンプレートをそのままトークン列に変換するので、空analysisブロックの事前注入が効きます。
nothinkオプションの落とし穴
Ollamaのモデル定義ファイルにPARAMETER nothink trueを書くと、APIレスポンスからreasoningが消えます。一見Think OFFに見えますが、内部ではreasoningを生成してDecodeしています。
# Modelfile
FROM gpt-oss:20b
PARAMETER nothink true
completion_tokensと速度を並べると実態が見えます。
| 指標 | Think ON | Ollama nothink | 本当のThink OFF |
|---|---|---|---|
| completion_tokens | 524 | 503 | 73 |
| 速度 | 12.4s | 10.4s | 2.1s |
nothinkのcompletion_tokensは503トークンで、Think ONの524トークンとほぼ変わりません。本当のThink OFFなら73トークンまで下がります。速度もThink ONに近い10.4秒のままで、本当のThink OFFの2.1秒とは別物です。
nothinkを使う場合、completion_tokensがThink ONの1/5以下に減っているか、速度が3倍以上になっているかなどを確認してください。どちらも満たさなければ、裏でreasoningが動いています。
reasoning_effortパラメータ
Think OFFとは別に、reasoningの量を加減するパラメータもあります。chat_templateのJinja変数reasoning_effortがシステムプロンプトにReasoning: low|medium|highを埋め込み、モデルの思考量を変えます。
{%- if reasoning_effort is not defined %}
{%- set reasoning_effort = "medium" %}
{%- endif %}
{{- "Reasoning: " + reasoning_effort + "\n\n" }}
渡し方は推論エンジンごとに違います。
# SGLang: トップレベルパラメータ
body = {
"model": "gpt-oss-20b",
"messages": messages,
"reasoning_effort": "low", # SGLang はここを見る
}
# llama-server: chat_template_kwargs 経由
body = {
"model": "gpt-oss-20b",
"messages": messages,
"reasoning_effort": "low",
"chat_template_kwargs": {"reasoning_effort": "low"}, # llama-server はここを見る
}
両方入れておけばどちらのエンジンでも動きます。
ベンチマーク
速度比較
DGX Spark上での実測値です。
| モデル | エンジン | Think設定 | 速度 | completion_tokens (avg) | 高速化 |
|---|---|---|---|---|---|
| 120b | Ollama | ON | 18.5s/件 | 475 | 1x(ベースライン) |
| 20b | Ollama | ON | 12.4s/件 | 524 | 1.5x |
| 20b | Ollama | nothink(見かけだけ) | 10.4s/件 | 503 | 1.8x |
| 20b | SGLang | ON | 21.1s/件 | 1,014 | 0.9x |
| 20b | SGLang | effort=low | 8.1s/件 | 218 | 2.3x |
| 20b | llama-server | OFF | 2.1s/件 | 73 | 8.8x |
| 120b | llama-server | OFF | 4.2s/件 | 66 | 4.4x |
| 20b | SGLang | OFF | ~2.0s/件 | ~80 | ~7.6x |
SGLangのcompletion_tokensが1,014と多いのは、--reasoning-parserでreasoning tokensがAPI上分離されるもののカウントには含まれるためです。OllamaはAPI上からreasoningを隠すので、completion_tokensの直接比較には注意が要ります。
精度比較(タスク別)
精度への影響はタスクで大きく変わります。2つのタスクでの実測値です。
| タスク | Think ON | Think OFF | 差分 |
|---|---|---|---|
| メタデータ抽出(単純な構造化) | 8/8 | 8/8 | 精度維持 |
| レビューコメント分類(多段階推論) | 96.5% | 76.3% | -20pt |
メタデータ抽出は、研究データのタイトルやプロジェクト名を所定のフィールドに埋めるタスクです。入力と出力がほぼ1対1に対応していて、reasoningが要りませんでした。
レビューコメント分類は、Redmineチケットの本文やコメントからレビュー指摘を識別して4カテゴリに振り分けるタスクです。複数コメントを横断して文脈を読み、技術的指摘の有無を判断しなければなりません。Think OFFにすると、この多段階の推論が飛んで、複雑なチケットを安易にnoneと判定してしまいます。見落としは5件から18件に跳ね上がりました。
reasoning_effort=low
reasoning量を短くするreasoning_effort=lowも試しました。
| 構成 | completion_tokens | 速度 | 妥当率 |
|---|---|---|---|
| Think ON(medium、デフォルト) | 1,014 | 21.1s | 98.1% |
| reasoning_effort=low | 218 | 8.1s | 78.0% |
| Think OFF(完全省略) | 73 | 2.1s | 76.3% |
短くしても78.0%、完全に省略しても76.3%。精度低下の幅はほぼ同じです。このタスクでは十分な量のreasoningが必要で、中途半端に削っても意味がありませんでした。
explicit CoTによる精度回復の試み
Think OFFの精度低下を補おうと、プロンプト側にexplicit chain-of-thoughtを組み込みました。コメントを1件ずつスキャンしてから分類結果を出す2段階構成です。
| 構成 | completion_tokens | 速度 | 妥当率 |
|---|---|---|---|
| Think ON(モデル内部のreasoning) | 524 | 12.4s | 96.5% |
| Think OFF(reasoning省略) | 73 | 2.1s | 76.3% |
| Think OFF + explicit CoT | 380 | 13.4s | 87.5% |
精度は76.3%から87.5%に上がりましたが、出力トークンが73から380に膨らんだぶん、速度が2.1sから13.4sまで落ちました。Think ONの12.4sとほぼ同じです。精度を取り戻そうとすると速度が消える。しかも96.5%には届きません。
モデル内部のreasoningはanalysisチャネルで処理されるため、出力トークンとしてカウントされません。プロンプトでCoTを強制すると思考過程がfinalチャネルに出てしまい、そのぶんDecodeに時間がかかります。
まとめ
Think OFFの設定はchat_templateの書き換え1箇所です。速度は5〜8倍になりますが、精度はタスクの複雑さで決まります。reasoning_effort=lowで中間を取ろうとしても、精度低下はThink OFFと同じでした。Ollamaのnothinkは裏でreasoningが動いているので、completion_tokensと速度で検証してから使ってください。
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。