はじめに
ここまでの2記事では、Qwen3.5-2B 内蔵の MTP で遊んできました。「n=1 だけ有効で +15〜19%」という控えめだけど着実な結果です。
ただ、世の中には「いや 2B じゃちょっと…」というケースもあります。コード生成とか長文要約とかになると、できれば 4B / 8B を使いたい。
そして 4B クラスになると、投機的デコーディングの恩恵もまた違う形で出てくるはず。今回紹介する D-Flash は、前回までの MTP とはまったく別方式の drafter で、論文 (z-lab, 2026) では EAGLE-3 比 2.5x、Qwen3-8B で最大 6x の lossless speedup と書かれています。なかなか強気な数字です。
「12GB の RTX 4070 で 4B + D-Flash って入るのか?」「公称通り速くなるのか?」というのが本記事のテーマです。
例によって先に結論を書きます。
- FP8 baseline 60 tok/s → D-Flash で 71 tok/s (+18%)
- 同条件の MTP n=3 も 70.6 tok/s (+18%) で、ほぼ同速
- 12GB GPU では vLLM のスケジューラ制約が効いて D-Flash の並列性が発揮されない
- 4B 自体が 12GB では ~60 tok/s の壁 があり、2B+MTP (147 tok/s) には届かない
「D-Flash すごい」という記事を書こうとしたら、12GB GPU での現実の壁にぶつかった、という話です。
D-Flash とは何か
D-Flash は z-lab がリリースしている block diffusion drafter です。論文は "DFlash: Block Diffusion for Flash Speculative Decoding" (Chen et al., 2026)。
通常の draft モデル方式 (たとえば EAGLE-3) は、draft 側でも autoregressive に 1 token ずつ生成します。先読み数を増やすと draft 側の forward 回数が線形に増えるので、draft latency を抑えるためにすごく浅い draft モデル (1 層 Transformer など) を使わざるを得ません。
D-Flash はここを block diffusion で並列化します。
- target モデルの中間層から特徴量を抽出
- 軽量な projection を通して draft 層の KV cache に注入
- draft は注入された KV を見ながら、block size 分の token を 1 回の forward で一気に生成
block size 16 で動かすと、1 step で 16 token がほぼ並列に出てきます。autoregressive draft より深いネットワークを使えるので、受理率を犠牲にせずに draft latency を抑えられる、という設計です。
公式 benchmark では Qwen3-8B で最大 6x、4B で HumanEval 3.7x の lossless speedup を主張しています。ただこれらは十分な VRAM がある環境での数字です。12GB GPU での話は後述します。
ちなみに draft モデルは target の embedding と LM head を再利用するので、parameter 増加は最小限です。今回の 4B 用 drafter z-lab/Qwen3.5-4B-DFlash は BF16 で約 1.0 GiB でした。
vLLM での指定方法
vLLM では --speculative-config で投機的デコーディング方式を指定します。
D-Flash は外部 drafter を必要とするので、model フィールドに drafter モデルを書きます。
vllm serve RedHatAI/Qwen3.5-4B-FP8-dynamic \
--dtype auto \
--speculative-config '{"method": "dflash", "model": "z-lab/Qwen3.5-4B-DFlash", "num_speculative_tokens": 15}' \
--attention-backend flash_attn \
--max-num-batched-tokens 4096 \
--max-model-len 2048 \
--gpu-memory-utilization 0.937 \
--enforce-eager
--attention-backend flash_attn は D-Flash 公式手順で必須です。num_speculative_tokens: 15 は block size 16 に対応する公式推奨値 (block 内 1 token は前処理に使われる扱い)。
比較する MTP は前記事と同じく qwen3_next_mtp、n=3 で測ります。
12GB GPU の VRAM 事情
Qwen3.5-4B FP8 を 12GB GPU で動かすのは、率直に言ってかなり厳しいです。
理由は Qwen3.5 のアーキテクチャ側に2つあります。
1. Vision Encoder が BF16 のまま乗る
Qwen3.5 はネイティブマルチモーダルで、全サイズに Vision Encoder が含まれます。FP8 量子化は LLM 本体だけが対象で、Vision Encoder は BF16 のまま ロードされます。今回の構成では Vision Encoder だけで約 3.0 GiB です。
2. Mamba の SSM state が固定サイズ
Qwen3.5 は Gated DeltaNet + Mamba のハイブリッドアーキです。Mamba の再帰状態 (SSM state) は max-model-len に関係なくほぼ固定サイズ (~0.9 GiB) を占有します。普通の Transformer は seq_len を下げれば KV cache も小さくなりますが、ここは下がりません。
VRAM 内訳をまとめるとこうなります (vLLM 起動ログから推定)。
| 項目 | サイズ |
|---|---|
| LLM 本体 (FP8) | 約 4.0 GiB |
| Vision Encoder (BF16) | 約 3.0 GiB |
| D-Flash drafter (BF16) | 約 1.0 GiB |
| KV cache | 約 1.3 GiB |
| activation バッファ | 約 1.0 GiB |
| 合計 | 約 10.3 GiB |
RTX 4070 (11.59 GiB) のうち、デスクトップ環境などの常時消費分を差し引くと実用可能なのは約 11.0 GiB。本当にギリギリです。
CUDA graph を有効にすると graph capture に追加で 1 GiB 以上必要になり、簡単に OOM します。そのため今回は --enforce-eager で CUDA graph を無効化 した状態で全構成を測っています。
結果
計測条件は前記事までと同じで Medium (出力 256 tokens) と Long (出力 512 tokens)。warmup 1 回 + 本計測 5 回の中央値です。
Medium
| 構成 | tok/s | baseline 比 | acceptance |
|---|---|---|---|
| Base-FP8 | 59.8 | 1.00 | - |
| MTP-FP8-n3 | 70.3 | 1.18 | 0.33 |
| DFlash-FP8-n15 | 62.8 | 1.05 | 0.04 |
Long
| 構成 | tok/s | baseline 比 | acceptance |
|---|---|---|---|
| Base-FP8 | 60.0 | 1.00 | - |
| DFlash-FP8-n15 | 71.0 | 1.18 | 0.054 |
| MTP-FP8-n3 | 70.6 | 1.18 | 0.33 |
D-Flash と MTP がほぼ同速 という結果になりました。Medium では MTP が上回り、Long ではほぼ同等。受理率は D-Flash が 0.04〜0.05 と非常に低く、MTP は 0.33 と上回っています。
論文で主張していた 2x 以上の speedup には遠く届かない結果です。理由は次節に書きます。
vLLM のスケジューラ制約
D-Flash の結果がふるわなかった最大の要因は vLLM のスケジューラが num_speculative_tokens=15 に対してバッチサイズを自動で絞る 動作です。
起動ログに以下の警告が出ています。
WARNING: max_num_scheduled_tokens is set to 512 based on the speculative decoding settings.
This may lead to suboptimal performance. Consider increasing max_num_batched_tokens.
計算式は:
max_num_scheduled_tokens = max_num_batched_tokens / (num_speculative_tokens + 1)
= 4096 / (15 + 1) = 256 → 512 に切り上げ
max_num_batched_tokens=4096 でも、D-Flash n=15 では実効的に 512 tokens/step に絞られます。このスロットリングによって D-Flash が本来持つ「1 step 16 token 並列」という並列性が発揮できていません。
解決するには max_num_batched_tokens を 4096 × 16 = 65536 相当まで増やす必要がありますが、VRAM が足りません。12GB GPU では D-Flash n=15 の本来の性能を引き出すのは構造的に困難という結論です。
MTP の起動ログには同じ警告は出ておらず、MTP は n=3 でもスロットリングを受けていません。これが Medium で MTP が D-Flash を上回った理由です。
出力サンプル
数値だけでは性能感が掴みづらいので、実際の出力も見比べてみます。プロンプトは Long 条件 (地方自治体の生成 AI 導入ガイド) を使いました。
今回は enable_thinking: false を指定して計測しているため、全構成で最初から日本語の回答本文が出力されます。
[DFlash-FP8-n15 Long の出力冒頭]
# 地方自治体が生成 AI を導入する際の進め方:実務者向けガイド
地方自治体は、近年「DX(デジタルトランスフォーメーション)」の推進により、
業務効率化や住民サービスの向上を急務としています。その中で、生成 AI(Generative AI)は、
公文書の作成支援、市民相談の自動化、政策立案のシミュレーションなど、
多岐にわたる課題解決の可能性を秘めています。
## 1. 導入目的の整理
まず「なぜ AI を導入するか」を明確に定義することが不可欠です。目的が曖昧だと、
後々の評価や予算承認に支障をきたします。
具体例:市民相談の 30% を AI による自動回答で対応し、窓口の待ち時間を 20% 削減する。
[MTP-FP8-n3 Long の出力冒頭]
# 地方自治体が生成 AI を導入する際の進め方:実務者向けガイド
地方自治体は、近年「DX(デジタルトランスフォーメーション)」の推進により、
業務効率化や住民サービスの向上を目的として IT 投資を加速させています。
## 1. 導入目的の整理
生成 AI を導入する前に、単なる「流行り」ではなく、自治体の抱える具体的な課題と、
AI が解決できる範囲を明確に定義する必要があります。
具体例:文書作成業務の効率化 ─ 月次報告書ドラフト作成を自動化し、
職員の作業時間を 50% 削減する。
両構成とも実用的な日本語が出ています。速度は同じでも出力の細部はわずかに違います (どちらが「正解」とは言えない)。
受理率と速度の関係
今回の受理率を整理すると:
- D-Flash: Medium 0.04 / Long 0.054 (5% 未満)
- MTP: Medium 0.33 / Long 0.33
一見 MTP のほうが圧倒的に受理率が高いのに速度が同等なのは、D-Flash が「受理率が低くても1回の forward で多数の token を扱う」設計だからです。受理率 0.05 でも 15 draft token に対して 0.75 token 回収しているのは MTP n=3 (0.33 × 3 ≈ 1.0 token) と同水準です。
ただ前述のスロットリングが効いているため、「1 forward で 15 token 並列」という設計の本領が出せていない状態です。
12GB GPU での教訓
- 4B 系は
--enforce-eagerを前提に設計 する。CUDA graph に必要な 1 GiB を諦めるしかない -
--gpu-memory-utilizationは 0.937 が実質上限 -
--max-model-lenを下げても Mamba SSM state は減らない ので KV cache の節約はほぼ無理 - D-Flash n=15 は
max_num_batched_tokens=4096では スロットリングを受けて本来の性能が出ない - 12GB GPU で D-Flash の公称性能を出すには、より大きな
max_num_batched_tokensが必要 → VRAM 不足で困難
まとめ
- D-Flash は block diffusion を draft に使う方式 で、draft 側を並列化して draft latency を抑える
- 12GB GPU (RTX 4070) では vLLM スケジューラのスロットリングが効き、D-Flash n=15 は Medium +5% / Long +18% にとどまった
- 同条件の MTP n=3 も +18% で、今回の環境では D-Flash と MTP がほぼ同速
- 4B + D-Flash / MTP のどちらも ~70 tok/s で、2B + MTP-n1 (147 tok/s) の半分以下
12GB GPU での D-Flash は「動く」が「公称通りの高速化は出ない」というのが正直な評価です。
全体を通しての所感
シリーズ3本で Qwen3.5-2B + MTP / Qwen3.5-4B + D-Flash を試してきました。
結果を並べると:
| サイズ | 構成 | Long tok/s |
|---|---|---|
| 2B | Base-FP8 | 124 |
| 2B | MTP-FP8-n1 | 147 (+19%) |
| 4B | Base-FP8 | 60 |
| 4B | DFlash-FP8-n15 | 71 (+18%) |
| 4B | MTP-FP8-n3 | 71 (+18%) |
12GB GPU の現実として、4B + 投機デコーディングでも ~70 tok/s が上限 になっています。Vision Encoder + Mamba state + drafter でほぼ VRAM が埋まり、スケジューラも制限を受けます。
対して 2B + MTP-n1 (147 tok/s) は 4B 系の2倍以上速い。「4B の品質がほしいなら 4B を使う、速度がほしいなら 2B + MTP」という棲み分けになります。
「4B + D-Flash で 2B より速くする」という使い方は、VRAM 余裕のある 16〜24GB GPU なら現実的かもしれません。12GB では CUDA graph も使えず、スケジューラも絞られるので、本来の D-Flash の性能評価としてはフェアな条件ではないと思います。
Speculative decoding は「モデルをそのままに decode を速くする」という方向でちゃんと機能します。ただし GPU の VRAM とスケジューラの制約が強く絡むので、「環境込みで実測する」のが大事だと改めて感じました。新しいモデルを触るときに「投機ヘッドはあるか」「drafter は出ているか」が自然にチェック項目になりそうです。
補足: 「以前の計測では 2x 以上出ていたのでは?」
筆者自身の事前計測では「Qwen3.5-4B FP8 + D-Flash n=15 で 2.4〜2.6x」という数字が出ていました。本記事の 1.18x との落差が大きいため、何が変わったのかを整理しておきます。
旧計測値と今回の比較
| Long tok/s | baseline 比 | acceptance | |
|---|---|---|---|
| 旧計測 (thinking ON) | 156.8 | 2.59x | 0.204 |
| 今回 (thinking OFF) | 71.0 | 1.18x | 0.054 |
tok/s が半分以下、acceptance rate も 1/4 以下になっています。
原因1: thinking モードが acceptance rate を水増ししていた
最大の原因はここです。
Qwen3.5 は thinking モードがデフォルト ON で、vLLM の /v1/chat/completions で chat_template_kwargs: {enable_thinking: false} を明示しないと <think>...</think> ブロックが挿入されます。
thinking 区間の出力は、こんな構造になります。
<think>
まず問題を整理します。地方自治体が生成 AI を…
次に考慮すべきは…ということになります。
したがって、以下のような手順が適切です…
</think>
この区間のトークン列は、
- 「まず」「次に」「つまり」「したがって」という接続詞の反復
- 直前の問いかけ文字列を言い換えるパターン
- 自己参照構造 (「この問題は X → X の解決には Y → Y とは…」)
という特性があり、統計的に予測しやすいです。Speculative decoding のドラフターは「次トークンの確率分布がシャープなほど受理率が上がる」ので、thinking 区間では acceptance rate が実際の回答トークンよりも大幅に高くなります。
旧計測では 512 token の出力のうち、かなりの割合が thinking 区間だった可能性が高く、その部分で acceptance rate が嵩上げされていたと考えられます。シリーズ前2記事の 2B + MTP でも同じ構造で旧値と新値にギャップがあり、「2B MTP-FP8 n=3 Long が 195 tok/s → 131 tok/s」という変化がまさにこれです。
原因2: vLLM nightly のスケジューラ制約
本文でも触れた max_num_scheduled_tokens の自動スロットリングです。
max_num_scheduled_tokens = max_num_batched_tokens / (num_speculative_tokens + 1)
= 4096 / (15 + 1) = 256 → 512 に切り上げ
旧計測時のバージョンではこの制約がなかった可能性があります。「同じ設定でも vLLM のバージョンが違うと結果が変わる」典型例です。具体的にどのコミットで入ったかは changelog では追い切れていませんが、今回使用の vllm/vllm-openai:nightly (v0.21.1rc1.dev243) では起動時に明確な WARNING として出ています。
D-Flash ベンチを読むときのチェックポイント
投機的デコーディングの計測値を比較するときは、以下を必ず確認してください。
| 確認項目 | 今回の設定 |
|---|---|
enable_thinking |
false (明示) |
| vLLM バージョン | nightly v0.21.1rc1.dev243 |
max_num_batched_tokens |
4096 |
num_speculative_tokens |
15 |
| concurrency | 1 (リクエスト逐次) |
| 出力 token 数 | 256 / 512 |
これらが揃わないと、同じモデル・同じ method でも tok/s が 2x 以上変わることがあります。特に thinking ON/OFF と vLLM バージョン の 2 点は影響が大きく、数字だけ引用するときに見落としがちです。