はじめに
ローカル LLM を触っていて毎回気になるのは、やっぱり decode の遅さです。1 token ずつ生成する以上、どうしてもじりじりした体感になります。
ここを速くするアプローチは大きく2つあると思っていて、
- モデル自体を軽くする (量子化、蒸留、パラメータ削減、より小さなモデルに乗り換える)
- モデルはそのままで decode を効率化する (投機的デコーディング、KV cache 最適化など)
このシリーズで追いかけたいのは 2 のほうです。「いま使っているモデルをそのまま、もっと速く動かしたい」というやつ。モデルを小さくすれば確かに速くはなりますが、それは「別のモデルにした」だけで、頭の中のモデル選定が振り出しに戻ります。そうではなくて、今あるモデルの decode をどう速くするか、という話を扱いたいです。
そもそも LLM の decode が遅いのは、処理がメモリ律速 だからです。GPU は 1 step ごとにモデルの重みを VRAM から全部読み出していますが、ボトルネックになっているのは演算より重み転送のほうです。重みを1回読むコストに対して 1 token しか進まないので、演算ユニットはわりと暇しています。
ここに「複数 token を一気に予測して、外したら戻す」という 投機的デコーディング (Speculative Decoding) を差し込むと、同じ重み読み出しコストで複数 token 進められる可能性が出てきます。これが今回の主題です。
Qwen3.5 はモデル本体に Multi-Token Prediction (MTP) ヘッド が組み込まれていて、別の draft モデルを用意せず投機ができるらしい。それなら手元の RTX 4070 12GB で素直に試せそうです。
先に結論を書くと、Qwen3.5-2B FP16 で baseline 115.4 tok/s → MTP-FP16-n1 で 133.5 tok/s (+15%)。効果は出ています。ただし先読み数を増やすほど逆効果になる、というのが今回いちばん面白かった観察です。
シリーズは3本に分けます。長くなったので。
- ① 本記事: 投機的デコーディングと MTP の基礎 + FP16 ベースライン
- ② 量子化 (FP8 / AWQ4) × MTP の比較 + Ollama 参考値
- ③ 番外編: Qwen3.5-4B + D-Flash
投機的デコーディングとは何か
通常のテキスト生成は1 token ずつです。
LLM は前までの token 列を入力として次 token を予測し、それを入力に足してまた次を予測します。1 step ごとに forward を1回回すので、生成 token 数 = forward 回数 です。
これは GPU からするともったいない計算です。重みを VRAM から読み出すコストに対して、1回あたりの計算量が少ないからです。
投機的デコーディングは、ここに draft (下書き) と 検証 (verify) の2段階を入れます。
- 安いモデル、もしくは専用ヘッドで、次の k token を まとめて先読み する (draft)
- target モデルが、その k token を 1回の forward で並列に検証 する (verify)
- 連続して正しかった分は採用、間違えたところで打ち切って次へ
連続で受理 (accept) された token が多いほど、forward 1回で複数 token 進めるので速くなります。逆に当たらないと、draft 側の計算が無駄になります。
このため、投機的デコーディングには2軸の評価が要ります。
- 受理率 (acceptance rate): draft のうち受理された割合
- 1 step で進める token 数: 受理率 × 先読み数 (おおよそ)
「受理率が高いほど速い」は単純すぎる話で、先読み数を増やせば受理率は下がります。このトレードオフの最適点を探すのが、num_speculative_tokens のチューニングです。
Qwen3.5 と内蔵 MTP の話
Qwen3.5 は 2026 年に Alibaba がリリースしたシリーズで、いくつか面白い特徴があります。
- Gated DeltaNet + Mamba のハイブリッドアーキ: 通常の softmax attention に加えて、SSM 系の線形 attention レイヤを混ぜています
- ネイティブマルチモーダル: 0.8B / 2B / 4B / 9B / ... の全サイズに Vision Encoder が同梱
- MTP ヘッド内蔵: 投機的デコーディング用の Multi-Token Prediction ヘッドが target モデル自体に組み込まれている
最後の MTP が今回の主役です。
通常の投機的デコーディング (たとえば EAGLE や draft-model 方式) は、target とは別に小さな draft モデルを動かします。VRAM もモデル管理コストも増えます。
MTP では、target モデル自体が複数 token を先読みする能力を持っています。専用の小さなヘッドが target の特徴量から複数 token を一気に提案し、target が同じ forward でその検証も行います。
vLLM では qwen3_next_mtp という method 名で実装されています。Qwen3-Next 系列で導入された機構が Qwen3.5 にも引き継がれている形です。同じ MTP 系には deepseek_mtp / mimo_mtp / glm4_moe_mtp などモデル別の実装があります。
ちなみに Mamba ハイブリッドの方は、今回は VRAM 制約 という形で効いてきます。Mamba の SSM state は max-model-len に関係なく固定サイズで常に確保されるため、コンテキスト長を下げても VRAM 消費がほとんど減りません。これが起動設定の細部に響きます。
実行環境
| 項目 | 内容 |
|---|---|
| GPU | RTX 4070 12GB |
| 実 VRAM | 11.59 GiB |
| OS | Ubuntu |
| vLLM |
vllm/vllm-openai:nightly (v0.21.1rc1.dev243) |
| API | OpenAI 互換 /v1/chat/completions
|
| temperature | 0 |
| thinking | enable_thinking: false |
| concurrency | 1 |
| max_model_len | 2048 |
vLLM は Docker で起動しました。起動コマンドは MTP なしならこんな感じです。
vllm serve Qwen/Qwen3.5-2B \
--dtype auto \
--attention-backend flash_attn \
--max-num-batched-tokens 4096 \
--max-model-len 2048 \
--gpu-memory-utilization 0.93
MTP を有効にするには --speculative-config を足します。
vllm serve Qwen/Qwen3.5-2B \
--dtype auto \
--speculative-config '{"method": "qwen3_next_mtp", "num_speculative_tokens": 1}' \
--attention-backend flash_attn \
--max-num-batched-tokens 4096 \
--max-model-len 2048 \
--gpu-memory-utilization 0.93
num_speculative_tokens は今回 1 / 3 / 5 の3パターン測ります。
計測方法
プロンプトは2条件にしました。
| ケース | 入力 | 出力 | 目的 |
|---|---|---|---|
| Medium | 約100 tokens | 256 tokens | 通常利用 |
| Long | 約200 tokens | 512 tokens | decode 区間を伸ばして MTP 効果を見る |
各構成で warmup 1 回、本計測 5 回。tok/s と elapsed_ms の中央値を採用しました。
Qwen3.5 は thinking モードがデフォルト有効ですが、今回は enable_thinking: false を指定して 通常の日本語回答だけを生成 する設定で計測しています。
MTP 構成は /metrics の前後差分から受理率を計算しています。
curl -s http://localhost:8000/metrics | grep spec_decode | grep -v "^#"
spec_decode_num_draft_tokens_total と spec_decode_num_accepted_tokens_total の差分で、acceptance rate = accepted / draft を出します。
FP16 ベースライン結果
まずは MTP の効果が一番素直に見える FP16 から。
構成名は Base-{量子化} を baseline、MTP-{量子化}-n{先読み数} を MTP ありで統一します (シリーズ通して同じ命名)。
Medium (出力 256 tokens)
| 構成 | tok/s | baseline 比 | acceptance |
|---|---|---|---|
| Base-FP16 | 115.4 | 1.00 | - |
| MTP-FP16-n1 | 126.4 | 1.10 | 0.52 |
| MTP-FP16-n3 | 119.7 | 1.04 | 0.36 |
| MTP-FP16-n5 | 94.3 | 0.82 | 0.22 |
Long (出力 512 tokens)
| 構成 | tok/s | baseline 比 | acceptance |
|---|---|---|---|
| Base-FP16 | 116.0 | 1.00 | - |
| MTP-FP16-n1 | 133.5 | 1.15 | 0.60 |
| MTP-FP16-n3 | 120.6 | 1.04 | 0.36 |
| MTP-FP16-n5 | 91.1 | 0.79 | 0.21 |
MTP の効果はちゃんと出ていますが、面白いのは num_speculative_tokens を増やすほど逆効果になる という傾向です。
n=1 → n=3 で受理率が 0.60 → 0.36 に落ちます。先読みを3つ連続で当て続けるのはかなり難しい、ということです。結果として n=3 では1 step あたりの実効 token 数が n=1 より増えず、オーバーヘッドだけが積み上がります。
n=5 になると受理率が 0.21 まで下がり、ベースラインより遅くなります。先読み5 token の draft コストが利益を上回った結果です。
vLLM 公式ドキュメントは「とりあえず n=1」と書いていますが、少なくとも Qwen3.5-2B FP16 + RTX 4070 + 日本語回答 (thinking off) の組み合わせでは、公式推奨の n=1 がそのまま最適 という結果になりました。
まとめと次回予告
今回分かったこと。
- 投機的デコーディングは受理率と先読み数の両方で決まる
- Qwen3.5 は target 内蔵 MTP ヘッドを持ち、別 draft モデル不要で投機できる
- FP16 ベースライン 115.4 tok/s に対し、MTP n=1 で 133.5 tok/s (+15%)
- 受理率は n=1 で 0.60、n=3 で 0.36、n=5 で 0.21 と急落
- n=3/n=5 はオーバーヘッドが利益を上回り、n=5 はベースラインより遅い
- n=1 が最速 (公式推奨通り)
次回 ② では、これを FP8 / AWQ4 にも当てはめて量子化との組み合わせを比較します。「全量子化で n=1 だけが有効」という傾向が他の量子化でも同じか、Ollama との比較も見ていきます。