TL;DR
- ThinkingCap-Qwen3.6-27B (NVFP4) の native MTP head を vLLM で有効化し、単一ストリームの decode 速度を実測した
- MTP の draft acceptance は英語コード生成で 71%、日本語カジュアル会話で 36%。速度向上は 1.85 倍 vs 1.12 倍と、ワークロードで天と地ほど違う
- 「アクティブパラメータ 9 倍差の MoE と dense を比べたら 9 倍遅くなる」は誤り。実測は 2.5 倍差で、その内訳は帯域の数学で説明できる(量子化 1 段で 9→5 倍、固定オーバーヘッドで 5→2.5 倍)
- 音声チャットのようなリアルタイム日本語対話では投機デコードを当てにしない設計が正しい。エージェント/コード用途ではほぼタダで 2 倍近く速くなる
背景: MoE から dense への乗り換えで速度が問題になった
運用中のローカル LLM を、35B-A3B の MoE(アクティブ 3B)から dense 27B に置き換える検討をしていた。指示追従やエージェント性能は dense 側が良い。問題は decode 速度で、リアルタイム音声チャットも同じエンジンに乗っているため「会話が遅くなるなら没」という制約があった。
そこで期待したのが、乗り換え候補モデルに同梱されている MTP (Multi-Token Prediction) head による投機デコードだった。「dense の速度ハンデを投機で補えるか?」が今回の問いになる。
環境と測定方法
- GPU: RTX PRO 6000 Blackwell (96GB)
- vLLM 0.24.0 / モデル: ThinkingCap-Qwen3.6-27B-NVFP4(重み ~19.7GB + MTP head bf16 0.85GB)
--speculative-config '{"method":"qwen3_5_mtp","num_speculative_tokens":3}'/--kv-cache-dtype fp8- 比較対象: Agents-A1(35B-A3B MoE、fp8、投機なし)
- 単一ストリーム、streaming で TTFT と decode tok/s を測定(completion_tokens / 初トークン以降の実時間)
- acceptance は vLLM の
/metricsの差分から算出:
acceptance = Δ vllm:spec_decode_num_accepted_tokens_total
/ Δ vllm:spec_decode_num_draft_tokens_total
シナリオは 2 つ。ja_chat(ペルソナ付きの日本語カジュアル会話、512 tok 上限)と code_agent(英語のコーディングタスク、2048 tok 上限)。
結果
| decode tok/s(中央値) | MoE 35B-A3B fp8 | dense 27B 素 | dense 27B + MTP | MTP acceptance |
|---|---|---|---|---|
| 日本語カジュアル会話 | 177 | 72.0 | 80.5 (×1.12) | 36.1% (92/255) |
| 英語コード生成 | 177 | 70.7 | 131.0 (×1.85) | 71.4% (2795/3912) |
TTFT はどの構成でも 50〜100ms で有意差なし。
素の decode がシナリオ不問でフラット(72 vs 70.7)なのは帯域律速の dense らしい挙動で、差がつくのは全部 MTP の acceptanceということになる。同じモデル・同じ draft head で、コードは 7 割当たり、日本語雑談は 3 割半しか当たらない。
なぜ日本語雑談で外れるのか
投機デコードの利得は「draft がどれだけ本体の出力を言い当てるか」で決まる。当たりやすいのは次トークンのエントロピーが低いテキストだ。
- コード: 構文の制約が強く、boilerplate・インデント・キーワードなど「ほぼ一本道」のトークンが大量にある。draft はそこを刈り取れる
- 日本語カジュアル会話: 語尾・言い回し・相槌のバリエーションが本質的に多い。「〜だね」「〜だよ」「〜かな」のどれも自然な地点では、draft は当てようがない
加えて、コミュニティの MTP/投機デコードの評判は英語+コードのベンチに偏っている。「投機で 2 倍速い」系の報告を日本語対話サービスにそのまま持ち込むと、期待の半分も出ないというのが今回の学びだ。逆に、翻訳やテンプレ性の高い文書生成なら日本語でも当たる可能性は残る(未計測)。
「アクティブ 9 倍差で速度 2.5 倍差」の内訳
もう一つ、直感に反した数字が「MoE(アクティブ 3B)と dense 27B の速度差」だった。パラメータ比 9 倍に対し、実測は 177 vs 71 の 2.5 倍しか違わない。
decode は帯域律速なので、効くのはパラメータ数ではなく 1 トークンあたりの重み読み出しバイト数:
- MoE 35B-A3B @ fp8: アクティブ ~3B × 1 byte + shared expert + attention ≈ ~4 GB/token
- dense 27B @ NVFP4: ~0.56 byte/param(+ bf16 の埋め込み等)≈ ~20 GB/token
つまり真の比は 9 倍ではなく ~5 倍(FP4 が fp8 の半分/param なので 9 → 5 に縮む)。さらに理論天井と実測を比べると:
| 帯域理論値 (~1.7TB/s) | 実測 | 天井到達率 | |
|---|---|---|---|
| MoE 35B-A3B | ~425 tok/s | 177 | ~42% |
| dense 27B FP4 | ~85 tok/s | 71 | ~84% |
dense は帯域天井近くまで回っているのに対し、MoE は kernel 起動やサンプラー等の固定費に食われて天井の半分も出ていない。高 tok/s 側ほど固定費で理論比が圧縮されるため、5 倍が 2.5 倍まで縮む。「MoE は速い」は事実だが、単一ストリームでは公称ほどの差にならない — これは逆に言えば、dense 側の見かけの絶望感も公称ほどではないということだ。
運用の結論
- リアルタイム日本語対話は投機デコードを当てにしない。 acceptance 36% では体感を変えるほどの利得がない。会話パスの速度が要件なら、素の decode 速度(= bytes/token)で選ぶ
- エージェント/コード用途では MTP はほぼタダ飯。 フラグ 1 つで 1.85 倍。しかも同梱 head なので追加 VRAM は ~1GB
- acceptance は自分のワークロードで測る。
/metricsの差分を取るだけで 5 分で分かる。英語コードのベンチ数字は日本語対話の参考にならない - 最終的に我々は指示追従を優先して dense 27B への全面移行を選び、会話パスの 0.45 倍は「実測した上で許容」した。この判断が精神論にならなかったのは、シナリオ別の実測が先にあったからだ
kotonia.ai の音声チャット/デスクトップエージェント基盤の刷新記録。関連記事: 「ローカルLLMが『Pythonで計算しました』と嘘をつくとき」(指示追従側の検証)。
