thinking-token reasoning-budget llama.cpp Gemma-4 Qwen3

🔍 発端 — 翻訳結果はなぜこうなったのか
Atelierブログの翻訳結果が継続して不良だった。文章がぎこちなく、途中で切れる現象が繰り返された。最初はモデル自体の品質問題と判断したが、追跡した結果、原因は別の場所にあった。
llama.cppはmax_tokens一つでthinkingトークンと回答(content)トークンを統合管理する。thinkingが有効な状態でモデルが些細な作業に過度にthinkingトークンを消費すると、実際の回答(content)生成に使う予算が残らない。つまり、翻訳が切れたのはモデルの品質問題ではなく、予算不足の問題だった。
max_tokens予算をthinkingトークンとcontentトークンが分け合う。モデルが些細な作業に過度に思考(thinking)すると、content生成前に予算が枯渇し、finish_reason=lengthが返される。
📊 測定 — 3スロットマトリクス
DGXの3スロット全てを対象に測定を実施した。各スロットに些細な作業(1行翻訳)と推論作業(数学問題)を投げて、thinkingトークン消費量を測定した。条件はmax_tokens=2048、finish_reason=stop。
| スロット | モデル | 些細な作業 (1行翻訳) | 推論作業 (数学) |
|---|---|---|---|
| model1 | Gemma-4-26B-A4B | think 1,601c / 5.4s | think 742c / 4.0s |
| model2 | Gemma-4-E4B | think 943c / 2.3s | think 253c / 1.4s |
| model3 | Qwen3.6-27B | think 4,498c / 63.5s | think 906c / 23.4s |
Qwen3.6(model3)は1行翻訳のために63.5秒を消費した。数学問題でもない単純な翻訳に1分以上かかるのだ。これはQwen3系の典型的な失敗モード「過思考(over-thinking)」現象だ。

🔬 コミュニティクロスチェック
測定データがモデルのスペックと一致するか検証した。直接測定した数値がモデルの設計意図と合っているか確認する段階だ。
Gemma 4 — ネイティブthinking、enable_thinkingフラグ
Gemma 4はネイティブthinkingモードをサポートし、enable_thinkingフラグで制御する。公式ドキュメントには別途のbudgetガイドが明記されていないが、コミュニティの慣例に従って運用される。マルチターン会話時に以前のthinkingブロックを除去することが公式要件だ。
enable_thinking=false設定でも空のthoughtブロックを出力する。E4Bモデルのみこのブロックが完全に省略される。
Qwen3 — ハイブリッドthinking、過思考が深刻
Qwen3系はハイブリッドthinking構造を持つ。enable_thinking=falseまたは/no_thinkシステムプロンプトで無効化できるが、完全に無効化するとtool-calling性能が低下する。Agenticパスでは完全無効化よりbudgetキャップを設定することが推奨されるというのが、公式thinking_budgetドキュメントの結論だ。
llama.cpp — –reasoning-budget スペック
公式README基準、--reasoning-budget値の範囲は以下の通り:-1(無制限) / 0(即時終了) / N > 0(トークンキャップ)。デフォルト値は-1だ。
重要な優先順位ルールがある。GH disc 21445によると、サーバー設定が--reasoning-budget=-1の場合のみ、per-requestのthinking_budget_tokensフィールドが適用される。サーバーフラグが0や正の値であれば、per-request設定は完全に無視される。
</think>を挿入して応答を続けるtransition messageを追加すると89%まで回復する。

🛠️ 実証 — per-requestコントロール検証 (build b9743)
理論を確認後、実際にper-requestコントロールが機能するか検証した。対象はmodel3(Qwen3.6-27B)とmodel2(Gemma-4-E4B)で、再起動なしで設定を適用した。
| 条件 | content | reasoning | 所要時間 | 短縮率 |
|---|---|---|---|---|
| model3 baseline | 106c | 7,262c | 102.7s | — |
model3 thinking_budget_tokens=256 |
106c | 827c | 16.1s | 6.4× |
model3 enable_thinking=false |
109c | 0c | 2.4s | 43× |
model2 enable_thinking=false |
110c | 0c | 0.7s | — |
per-requestコントロールが正常に機能することを確認した。model3 baseline 102.7秒がbudgetキャップ適用後16.1秒に短縮された。thinkingを完全に無効化すると2.4秒まで下がる。contentトークン数は3条件全て106〜109cで同一だった。これはthinkingトークン消費が問題だったのであり、モデル自体に問題はなかったことを証明する。
⚙️ 実装 — terry-llm v0.4.5 (コミット 220b85a1)
実証結果をもとにterry-llmゲートウェイに反映した。主な変更点は以下の通り。
1. providers/router.py — per-request budget パッシング
クライアントが送信したthinking_budget_tokensをextra_body経由でllama.cppサーバーに渡す。
# providers/router.py
if request.thinking_budget_tokens is not None:
extra_body["thinking_budget_tokens"] = request.thinking_budget_tokens
2. server.py — agenticパス budget キャップ
Agenticループでthinkingトークンが無制限に消費されることを防ぐため、設定値からキャップを読み込んで適用する。デフォルト値は4096トークンだ。
# server.py _do_agent()
budget = cfg.defaults.agent_thinking_budget_tokens
# config.yaml:
# agent_thinking_budget_tokens: 4096
3. agent/loop.py — 空contentの正直な処理
finish_reason=length(切断)とfinish_reason=stop(正常終了)を区別する。contentが空の場合はtruncatedフラグを付けて返す。以前は空文字列を正常な応答として返す問題があった。
4. run_http.py — セッション独立HTTP サービス (:8090)
既存のMCPセッションに依存せず:8090ポートで独立して動作するstandalone HTTPサービスだ。セッションが終了してもHTTPエンドポイントは安定して維持される。
✅ 3スロット最終状態
| スロット | モデル | 状態 | 処理方式 |
|---|---|---|---|
| model1 | Gemma-4-26B-A4B | ✅ 最適化完了 | ゲートウェイbudgetキャップ(4096) + 単発パスenable_thinking=false適用 |
| model2 | Gemma-4-E4B | ✅ 最適化済み | terryragsysがenable_thinking=falseおよびGBNF制約デコーディングを適用中 |
| model3 | Qwen3.6-27B | ✅ 意図的に保持 | コーディング専用モデルとして、thinking機能を維持 |
実質的な変更はmodel1のみに適用した。model2はすでに最適化済みで、model3はコーディング作業のためにthinking機能が必須なため保持した。
⚠️ 注意点
- DGXサーバーの
--reasoning-budget=-1維持必須:この設定が-1でなければ、per-requestのthinking_budget_tokensは無視される。サーバー設定を変更すると全てのper-requestコントロールが一括で無効化される。 - 正のbudgetキャップ使用時はtransition message推奨:thinkingを強制終了する際に
</think>を挿入しないと性能が急激に落ちる。Qwen3基準でHumanEvalスコアが約16%p低下するため、ライブラリが対応していない場合は自前実装が必要だ。 - Gemmaマルチターン — 以前のthinkingブロック除去必須:公式要件だ。マルチターンパイプライン構成時には以前の会話のthinkingブロックを必ず処理しなければならない。
- E4B以外のGemma — 空thought block出力:
enable_thinking=false設定でも<|channel>thoughtのような空ブロックが出力される場合がある。パースロジックでこれへの対処が必要だ。
📚 参考資料
- Gemma 4 thinking 公式ドキュメント — Google AI for Developers
- Gemma 4 モデルカード — E4B thinking無効化動作の公式スペック
- llama.cpp server README —
--reasoning-budget公式スペック - GH disc 21445 — per-request
thinking_budget_tokens優先順位ルール - GH disc 21338 — Gemma 4 E4B thinking無効化バグ報告
- r/LocalLLaMA — HumanEvalベンチマークとtransition messageの効果
- Qwen3 thinking_budget 公式ドキュメント — QwenLM
関連するDGXインフラ運用記録はインフラアーカイブで確認できる。以前のモデル混在事例とレイテンシ改善結果もまとめられている。
✅ まとめ
問題は単純だった。thinkingオン状態でmax_tokens予算をthinkingとcontentが共有するのだが、モデルが些細な作業に過度に思考するとcontentが切れる。Qwen3基準で1行翻訳に63.5秒を消費し、baseline reasoningトークンは7,262cに達した。
解決策も明確だった。DGXサーバーの--reasoning-budgetを-1に維持し、ゲートウェイからper-requestのthinking_budget_tokensを渡し、agenticパスに4096トークンキャップを適用した。再起動なしでmodel1パスのみに適用して問題を解決した。