はじめに
社内でPowerPoint自動生成パイプラインをDify上で構築しており、生成モデルには NVIDIA Nemotron 3.5 Lightning(30B、アクティブパラメータ3BのMoEモデル)のBF16版を、DGX Sparkでホストして使っています。
ある日、生成速度が 25 tok/秒 で頭打ちになっていることに気づきました。原因を調べていく過程で「Speculative Decoding(投機的デコーディング)」という手法に出会い、実際に手を動かして検証したので、その過程をまとめます。
なぜ25 tok/秒で止まるのか ── メモリ帯域幅の壁
DGX Sparkは128GBのLPDDR5x統合メモリを持ちますが、帯域幅は 273 GB/秒 です。LLMのデコード(トークン生成)は基本的にメモリ帯域律速で、1トークン生成するたびにモデルの重みをメモリから読み出す必要があります。
Nemotron 3.5 LightningはMoE(Mixture-of-Experts)構造で、全30Bパラメータのうちアクティブになるのは3Bだけです。BF16(2バイト/パラメータ)なら、1トークンあたり理論上は
3B params × 2 bytes ÷ 273 GB/s ≈ 22ms/token → 理論上 ~45 tok/s
程度の上限になります。実測の25 tok/秒はここにMambaレイヤーのキャッシュ読み書きやスケジューリングのオーバーヘッドが乗った結果で、「壊れている」わけではなく、素のBF16構成としては素直な数字でした。
Speculative Decodingとは
ここで初めて仕組みを理解しました。要点は次の3つです。
-
小さな「ドラフトモデル」が先に複数トークンを予測する
本体モデルより軽量なモデルが、次に来そうなトークンを数個まとめて先読みする。 -
本体モデルが一括で検証する
本体モデルは、その予測をトークンごとに生成し直すのではなく、まとめて1回のフォワードパスで検証する。これは1トークン生成するのとほぼ同じコストで、複数トークン分の答え合わせができる。 -
不一致が出た時点で打ち切り、そこから先だけ本体モデルが生成し直す
結果として出力は本体モデル単体で生成した場合と数学的に同一。速くなるのは「当たったときだけ」で、外れが多いと検証コストだけがかさんで逆に遅くなることもある。
つまり 出力品質を落とさずに、メモリ帯域の使い方を効率化する テクニックです。BF16のまま高速化したい今回のようなケースには理にかなっています。
流れを図にすると次のようになります。
Nemotron 3.5 Lightningが用意する3つの方式
Lightning 3.5には外部ドラフトモデルを使う2方式と、モデル内蔵の1方式があります。
- MTP(Multi-Token Prediction): 本体モデルに埋め込まれた予測ヘッドを使う。追加のドラフトモデルのダウンロードが不要。
- DFlash: 軽量なブロック拡散モデルで、ドラフトブロックを1回のフォワードパスでまとめて生成する方式。低同時実行数のデータセンター向け。
- DSpark: セミ自己回帰的にブロック単位で候補トークンを提案する方式。DGX Spark向けに推奨されている、狭い帯域幅環境向けの設計。
ここは名前が紛らわしいのですが、「DSpark」は開発元(Peking University / DeepSeek系)由来の名称で、「DGX Spark」とは偶然の一致ではなく、実際に相性が良いという位置づけです。
ドラフトモデル自体はNVFP4量子化チェックポイントとして配布されており、本体(BF16)とドラフト(NVFP4)で精度を混在させて使う構成が公式にサポートされています。
実際に使った設定(vLLM)
export MODEL_CKPT=/path/to/local/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16
export DSPARK_CKPT=nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4-DSpark
vllm serve --model $MODEL_CKPT \
--max-model-len 131072 \
--max-num-seqs 32 \
--enable-prefix-caching \
--async-scheduling \
--mamba-backend flashinfer \
--mamba-ssm-cache-dtype float16 \
--enable-mamba-cache-stochastic-rounding \
--mamba-cache-philox-rounds 5 \
--speculative_config.model $DSPARK_CKPT \
--speculative_config.num_speculative_tokens 3 \
--gpu-memory-utilization 0.90 \
--port 8000
ポイントは2つあります。
- Nemotron 3.5 LightningはMamba-2とMoE、Attentionのハイブリッド構造なので、
VLLM_ATTENTION_BACKENDではなく--mamba-backend flashinferを効かせる必要がある。 -
num_speculative_tokensは深く設定すればいいわけではない。先読みが外れた分は丸ごと無駄になるので、DGX Spark程度の帯域幅では浅め(3前後)が無難という報告がある。
気をつけたこと ── Acceptance Rateを見る
Speculative decodingは「つければ必ず速くなる」わけではありません。ドラフトモデルの予測が本体モデルにどれだけ受理されるか(acceptance rate)が低いと、検証コストだけが増えて素のデコードより遅くなることもあります。vLLMのログでacceptance rateを確認しながら、num_speculative_tokens を調整するのが実務上のコツでした。
まとめ
- LLMのデコード速度はメモリ帯域幅で頭打ちになる。BF16のまま高速化したいならSpeculative Decodingが有効な選択肢。
- 出力は本体モデル単体と数学的に同一 ── 精度を犠牲にしない高速化手法。
- ハードウェアの帯域幅特性に応じて、ドラフト手法・先読み深さを選ぶ必要がある。