0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qwen3.8-27B の投機デコード、実測で 2 倍になった(DFlash2 vs MTP を同じベンチで比較)

0
Last updated at Posted at 2026-08-20

初めに

RTX 4090 24GB × 4 の 1 台(WSL2)で Qwen3.8-27B を vLLM で動かすと、単一ストリームの decode は 29 tok/s 前後

投機デコード(speculative decoding)を入れてみたところ、58〜59 tok/s(約 2 倍) になりました。

ただ、この 2 倍は「スイッチを入れるだけ」では出ません。draft token 数(num_speculative_tokens、以下 K)の選定、WSL2 の UVA、Ada(SM 89)の attention backend、KV cache の予算……と落とし穴が続きます。

さらに、当初「このモデルでは使えない」と思っていた MTP(vLLM 標準の投機デコード)が、実は DFlash2 と同程度の性能を出せる ことも、後から分かりました。今回は DFlash2 と MTP の 2 方式を、同じベンチマークで比較した結果をまとめます。

1. 投機デコードのおさらい

投機デコードは「小さいモデル(draft)が先に K token 書いて、大きいモデル(target)がまとめて答え合わせ(verify)する」仕組みです。

  • verify に通った token だけが進み、1 回の forward で複数 token 進める
  • 出力自体は(理想的な実装なら)通常 decode と変わらない
  • 効き目は「K をどれだけ採用してもらえるか(受容率)」と「draft + verify の追加コスト」のトレードオフ

草稿の作り方には流派があります。

MTP DFlash / DFlash2
draft を書く主体 モデル本体の MTP ヘッド 独立した小型 draft model(1.92B / 5 層)
書き方 1 token ずつ再帰的に予測 双方向 attention + conv でブロック丸ごと一括予測
K を増やすと draft コストが線形に増える draft コストはほぼ不変
1 token あたりの受容率 高い やや落ちる
追加依存 なし(モデル内蔵) draft モデル + 実装パッチ

DFlash2 は DFlash に candidate selector(codebook から top-k 候補を歩く)を足した後継で、draft の質を大きく上げています。

2. 検証環境

項目 内容
GPU RTX 4090 24GB × 4(TP=4、PCIe のみ)
OS Windows 11 + WSL2(kernel 6.6.114、mirrored mode)
target Qwen/Qwen3.8-27B(64 層 = GDN 48 + full attention 16、BF16)
draft(DFlash2 用) Qwen3.8-27B-DFlash2(1.92B、5 層、sliding window 2048、embedding/lm_head 共有)
実行基盤 vLLM nightly 0.27.2rc1.dev77+gac7509e2b(検証済み image)+ PR #52816 の Python オーバーレイ
max-model-len 131072
attention backend flash_attn(FA2。FA3 は Hopper 専用)
ベンチ 2k token の OCR 風 prompt、temp 0、単一ストリーム、warm(初回 JIT 後の安定値)

vLLM は GitHub から完全に遮断された環境だったため、PR #52816 の差分を別マシンで取得して手元に適用しました(純 Python / Triton のみで、C++ コンパイルは不要)。

3. まず DFlash2 で K を 1〜10 まで振ってみた

一番知りたかったのはここです。結果:

K decode 速度 (warm) 1 iteration あたりの採用 token 数 受容率 p0..
なし (baseline) 29.4 tok/s 1.00 -
1 約 34 tok/s 1.87 .87
3 約 49 tok/s 2.98 .83/.65/.50
4 約 50 tok/s 3.24 .76/.60/.48/.40
7 約 58.5 tok/s 3.92 .77/.60/.50/.42/.29/.19/.15
10 約 55.5 tok/s 3.84 .69/.55/.43/.37/...

読み取れること:

  • K=7 がピークで、baseline 比 約 2.0 倍
  • K=10 にすると下がる。「とりあえず大きめに」が一番やってはいけない設定
  • K=3 → K=4 はほぼ横ばい(49 → 50)。採用数は伸びているのに速度が伸びない → このあたりから draft/verify の追加コストが効き始めている

ちなみに最初の測定は 34.5 tok/s でしたが、これはコールドスタート値。初回リクエストに Triton JIT コンパイルと CUDA graph キャプチャが含まれるためです。warm 状態で測り直すと 49.6 → 58.5 と上がりました。測定は必ず warm で。

4. なぜ K=7 なのか——答えは checkpoint 側に書いてあった

K=10 で落ちる理由は、後から draft model の config を見直して腑に落ちました。

DFlash2 の conv 層は block_size = 1 + num_speculative_tokens で動きます。K=7 のとき block_size は 8 で、これは draft model を学習したときの block_size と同じ値 です。

dflash_config: { block_size: 8, conv_group_size: 16, conv_kernel_size: 2,
                 selector_rank: 256, selector_top_k: 16, ... }

つまり:

  • K=7 は「たまたま速かった値」ではなく、checkpoint の学習条件に一致する値
  • K=10 は block_size 11 となり、学習時に見ていない長さのブロックを書かせることになる。採用数が 3.92 → 3.84 に落ちているのはそのまま草稿品質の劣化

K を決める前に、draft model の config で block_size を確認する。K = block_size - 1 が最速の出発点。 これは他のモデルに移っても使える考え方です。

事前に立てた仮説は普通に外れました。私は最初 K=4 で始めるつもりでした(llama.cpp の別バックエンドで「32k context では width 4 が width 7 より 29% 速い」というデータを見ていたため)。結果は K=7 が K=4 を約 17% 上回りました。アーキテクチャも draft の仕組みも違うベンチから、K の最適値だけを持ってくることはできません。 傾向の参考にはなりますが、最終的な値は自分の環境で振るしかありません。振ってみれば数十分で済みます。

5. MTP も同じベンチで測ってみた

DFlash2 の導入が落ち着いた後、ふと「Qwen3.8-27B は MTP ヘッド内蔵なのでは?」と config を確認してみました。

config.json (text_config): { mtp_num_hidden_layers: 1, mtp_use_dedicated_embeddings: False }

内蔵していました。 つまり vLLM 標準の method=mtp がそのまま使えます。追加の draft model もパッチも不要です。

同じベンチで測った結果:

方式 K decode 速度 (warm) 1 iteration あたりの採用 token 数 受容率 p0..
MTP 7 59.2 tok/s(58.5〜60.3) 4.82 .87/.79/.69/.54/.44/.28/.21
MTP 12 約 52.8 tok/s 5.03 .82/.72/.62/.56/...
DFlash2 7 約 58.5 tok/s 3.92 .77/.60/.50/.42/...
baseline - 29.4 tok/s 1.00 -

注目すべきは 2 点:

  1. MTP K=7 は DFlash2 K=7 とほぼ同等(むしろ僅かに上)。約 2.0 倍。
  2. 受容率は MTP のほうが大幅に高い(4.82 vs 3.92 token/iter)のに、tok/s はほぼ同じ。これは MTP の draft フェーズがモデル本体を再帰的に回すため、1 iteration あたりのコストが重い(約 81ms vs DFlash2 の約 67ms)からです。DFlash2 は軽量 draft model(1.92B)を使うぶん草稿は安いが、受容率で負ける——ちょうど釣り合って、実測トップスピードはほぼ同点という結果になりました。

両者とも K=7 がピークですが、理由は違います

  • DFlash2: block_size = 1 + K が学習条件(8)に一致するかどうか
  • MTP: draft ヘッドの再帰コストが K に比例して増える(K=12 で 52.8 に低下)

「K=7 が正解」という点だけ同じで、背後にある制約は別物、ということです。

なお、この環境の vLLM の古いコードコメントには「MTP は Qwen3.8-27B では 3.6 倍の純損失」という記録があり、当初はそれを鵜呑みにして DFlash2 一択だと思い込んでいました。実際に同じベンチで測ると 現在の nightly では再現しない ことが分かりました。他所の計測・コメントも、自分の環境で測り直すべき、という良い例です。

6. 正しさの検証

投機デコードを導入したら「出力が壊れていないか」も確認すべきです。やったことは 2 つ:

  1. 決定性: 同一 prompt・temp 0 で 2 回実行 → 出力は完全一致(DFlash2 / MTP とも)
  2. baseline との一致: 素の decode の出力と比較 → DFlash2 は 99.62%、MTP は 99.55% 一致

わずかな差は、verify 経路で GDN(Gated DeltaNet の線形 attention)が Triton カーネルを、素の decode では fused CUDA カーネルを使うための数値精度の差です(低信頼度 token の argmax が稀に反転する)。意味論的には同一で、品質劣化はありませんでした。

7. 導入で踏んだ落とし穴(WSL2 / Ada)

7.1 WSL2 で UVA is not available になり起動すらしない

DFlash2 を有効にすると、vLLM は V2 model runner を強制します(PR の仕様)。V2 model runner は UvaBuffer(host と device の共有メモリ)を必要とします。

一方、WSL2 では vLLM は pinned memory / UVA をデフォルトで無効にします。結果、is_uva_available() が False を返し、worker が全滅して起動失敗します。

対処は環境変数 1 つ:

-e VLLM_WSL2_ENABLE_PIN_MEMORY=1

WSL2 の kernel は 6.6.114 で、vLLM が要求する下限(4.19.121)は十分超えています。「WSL2 だから一律で無効」という保守的な既定値に引っかかっていただけ、というオチでした。WSL2 で投機デコード系の新機能を試すときは、まずここを疑うと早いです。

7.2 Ada では attention backend を明示する

DFlash2 は投機スロット用に non-causal を扱える backend が必要です。Ada(SM 89)では FA2(flash_attn が通常経路。FA3 は Hopper 専用なので選べません。target 側と speculative-config 側の両方で指定しました。

7.3 KV cache を削らないと drafter が乗らない

24GB × 4 だと、draft model の分だけ確実に VRAM が足りなくなります。sharded された draft の重みが 1 枚あたり約 1GiB、これに draft 側の KV とアクティベーションが乗ります。

その分、KV cache を 7.0 GiB → 4.5 GiB まで落としました(トークンプール約 25 万)。

--kv-cache-memory 4831838208

投機デコードは**「余っている VRAM で速度を買う」機能**です。余っていなければ context か同時実行数のどちらかを売ることになります。

7.4 速くなるのは decode だけ

投機デコードが効くのは生成部分だけ。TTFT(最初の 1 token まで)は改善しません。むしろ同時実行数が増えると草稿・検証の分だけ TTFT は悪化する方向です。

  • 入力が短く出力が長く、同時実行が少ない用途 → よく効く
  • 入力が長い(prefill 支配)用途 → 体感はそこまで変わらない

私の用途(日本語ドキュメントの OCR)では、対話では明確に速くなり、長文入力では「そこそこ」。1 台で両方やるなら、投機あり/なしのコンテナを分けて用途で使い分けるのが素直です。

8. 2 方式のまとめ(この環境での実測)

項目 MTP K=7 DFlash2 K=7
warm decode 59.2 tok/s(約 2.0 倍) 約 58.5 tok/s(約 2.0 倍)
採用 token/iter 4.82 3.92
1 iteration コスト 約 81ms 約 67ms
受容率 高い(.87 スタート) やや低い(.77 スタート)
draft の主体 モデル内蔵 MTP ヘッド 外部 draft model(1.92B)
追加依存 なし draft モデル + PR #52816 パッチ
WSL2 の追加対応 不要 VLLM_WSL2_ENABLE_PIN_MEMORY=1
K の制約 ヘッド再帰コスト(K 増で劣化) 学習時 block_size(K = block_size - 1
KV cache 予算 7.0 GiB のまま 4.5 GiB に削減が必要
出力の正しさ baseline と 99.55% 一致 baseline と 99.62% 一致

結論: この環境では、MTP K=7 が「最も簡単で最高速(ほぼ同点)」です。 DFlash2 は外部 draft model を積むぶん受容率は高いものの、そのぶん iteration が重く、実測トップスピードは MTP とほぼ同点でした。追加依存ゼロで同じ性能が出るなら、運用コストで MTP が勝ちます。

DFlash2 を選ぶべきなのは「draft model の受容率がもっと高いモデルがある」「K を大きくしても draft が安い」など、MTP との差が出る条件が揃ったときです。

9. 起動スクリプト

2 つのコンテナを使い分けています(port 8000 / served name claude-opus-5 / API key sk-123456 は共通)。

9.1 MTP(推奨・デフォルト)

qwen38-vllm.sh の要点:

IMAGE="${IMAGE:-docker.1ms.run/vllm-openai:qwen38-verified}"
ENABLE_MTP="${ENABLE_MTP:-true}"               # 2026-08-20 からデフォルト true
NUM_SPECULATIVE_TOKENS="${NUM_SPECULATIVE_TOKENS:-7}"
KV_CACHE_MEMORY="${KV_CACHE_MEMORY:-7516192768}"  # 7.0 GiB(削減不要)

if [ "${ENABLE_MTP}" = "true" ]; then
  VLLM_ARGS+=(--speculative-config \
    "{\"method\":\"mtp\",\"num_speculative_tokens\":${NUM_SPECULATIVE_TOKENS}}")
fi

docker run $TTY_FLAG --rm \
  --gpus "\"device=${GPU_DEVICES}\"" \
  -v "${MODEL_DIR}:/models/Qwen3.8-27B:ro" \
  ... "${IMAGE}" "${VLLM_ARGS[@]}"

9.2 DFlash2(外部 draft model 版)

qwen38-dflash-vllm.sh の要点:

IMAGE="${IMAGE:-docker.1ms.run/vllm-openai:qwen38-dflash2}"  # 検証済み image + PR #52816 overlay
DRAFT_DIR="/root/HuggingFaceCache/Qwen3.8-27B-DFlash2"
NUM_SPECULATIVE_TOKENS="${NUM_SPECULATIVE_TOKENS:-7}"
KV_CACHE_MEMORY="${KV_CACHE_MEMORY:-4831838208}"  # 4.5 GiB(drafter 分を確保)

VLLM_ARGS=(
  /models/Qwen3.8-27B
  --served-model-name claude-opus-5
  --tensor-parallel-size 4
  --kv-cache-memory "${KV_CACHE_MEMORY}"
  --attention-backend flash_attn
  --speculative-config \
    "{\"method\":\"dflash\",\"model\":\"/models/Qwen3.8-27B-DFlash2\",\
      \"num_speculative_tokens\":${NUM_SPECULATIVE_TOKENS},\"attention_backend\":\"flash_attn\"}"
)

docker run $TTY_FLAG --rm \
  --gpus "\"device=${GPU_DEVICES}\"" \
  -v "${MODEL_DIR}:/models/Qwen3.8-27B:ro" \
  -v "${DRAFT_DIR}:/models/Qwen3.8-27B-DFlash2:ro" \
  -e VLLM_WSL2_ENABLE_PIN_MEMORY=1 \
  ... "${IMAGE}" "${VLLM_ARGS[@]}"

9.3 使い分け

# MTP(推奨・追加依存なし)
bash /root/myvllm/qwen38-vllm.sh

# DFlash2(外部 draft model を使う)
bash /root/myvllm/qwen38-dflash-vllm.sh

# 素の baseline(投機なし)に戻す
ENABLE_MTP=false bash /root/myvllm/qwen38-vllm.sh

まとめ

RTX 4090 × 4 という「データセンター向けではない」構成でも、投機デコードは普通に動いて 2 倍 出ました。そして今回は、当初の想定を覆す形で「外部 draft model を使う DFlash2 と、モデル内蔵 MTP がほぼ同点」という結果になりました。

  • K は振って決める。 公式推奨値も、他所のベンチの値も、そのままでは当たらない
  • DFlash2 の K は draft model の学習時 block_size から逆算できる(K = block_size - 1
  • どちらの方式も K=7 がピークだったが、背後にある制約は別物(学習ブロック長 vs ヘッドの再帰コスト)
  • WSL2 は pinned memory / UVA が既定で無効。V2 model runner を使う機能はここで落ちるVLLM_WSL2_ENABLE_PIN_MEMORY=1
  • Ada では attention backend を明示する(FA3 は選べない)
  • draft の分だけ KV cache を削る。context か同時実行数を売ることになる
  • 速くなるのは decode だけ。TTFT は改善しない
  • 測定は必ず warm で(コールドスタートは JIT 込みで低く出る)
  • 出力の正しさも確認する(今回 99.6% 一致・決定性あり)

個人的な感想として、投機デコードは「入れれば速くなる機能」というより、自分の環境で 1 回ちゃんと測る手間とセットの機能 です。設定を 1 つ間違えるだけで、2 倍のはずが 1.7 倍になったり、そもそも起動しなかったりします。逆に言うと、測りさえすれば数十分で決まる話でもありました。

参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?