【vLLM】Speculative Decodingで推論速度2倍化!設定手順と「RuntimeError」解決法

問題の概要:Speculative Decoding有効化時のエラーと速度向上の課題

大規模言語モデル(LLM)の推論エンジンであるvLLMは、その高速な推論速度が特徴です。さらに、Speculative Decoding(投機的デコード)という技術を利用することで、理論的には推論速度を2倍以上に高速化できる可能性があります。

しかし、実際にvLLMでSpeculative Decodingを有効化しようとすると、以下のようなエラーに遭遇し、設定が成功しないケースが頻発しています。

RuntimeError: The speculated token 1234 is not the same as the target token 5678.
ValueError: Speculative decoding requires both `draft_model` and `target_model` to be set.

また、設定は成功しても、思ったような速度向上(2倍化)が得られない、あるいは逆に速度が低下してしまうという課題もあります。本記事では、これらのエラーの原因を解説し、vLLMでSpeculative Decodingを正しく有効化して推論を高速化するための具体的な手順を紹介します。

原因の解説:なぜエラーが発生するのか?

Speculative Decodingの基本的な考え方は、小さな「ドラフトモデル」が次のトークンを予測し、大きな「ターゲットモデル」(本番モデル)がその予測を効率的に検証・修正するというものです。これにより、ターゲットモデルの実行回数を減らし、全体の推論速度を上げます。

vLLMで発生する主なエラーの原因は以下の3点です。

1. モデルの互換性問題

RuntimeError: The speculated token ... is not the same というエラーは、ドラフトモデルとターゲットモデルのトークナイザー(語彙)が完全に一致していない場合に発生します。Speculative Decodingでは、両モデルが同じ語彙空間を共有していることが絶対条件です。異なるトークナイザーで学習されたモデルを組み合わせると、トークンIDのマッピングがずれ、このエラーが発生します。

2. 設定の不備

ValueError: Speculative decoding requires both `draft_model` and `target_model` エラーは、vLLMのLLMクラスを初期化する際に、Speculative Decodingに必要なパラメータ(speculative_model)が正しく渡されていない場合に発生します。単にenable_speculative=Trueのようなフラグがあるわけではなく、専用の引数を通じて設定する必要があります。

3. モデルサイズのバランス不良

速度が向上しない場合、ドラフトモデルとターゲットモデルのサイズバランスが悪い可能性があります。ドラフトモデルが大きすぎると、ドラフト生成自体に時間がかかり、Speculative Decodingのオーバーヘッドが利益を上回ってしまいます。逆に小さすぎると、予測精度(acceptance rate)が低くなり、ターゲットモデルの検証処理が頻発して効率が悪化します。

解決方法:ステップバイステップ設定ガイド

ここからは、vLLM 0.3.0以降の環境で、Speculative Decodingを正しく有効化する手順を説明します。

ステップ1:互換性のあるモデルペアの選択

最も確実な方法は、同じモデルファミリーから、サイズの異なるモデルを選択することです。例えば、MetaのLlama 3シリーズを使用する場合:

  • ターゲットモデル(本番): `meta-llama/Meta-Llama-3-8B-Instruct`
  • ドラフトモデル(小): `meta-llama/Meta-Llama-3-1.8B-Instruct`

これらはトークナイザーが同一であるため、互換性の問題が起こりにくいです。

ステップ2:vLLMのインストールとインポート

最新版のvLLMをインストールします。Speculative Decodingは比較的新しい機能です。

pip install vllm>=0.3.0

必要なクラスをインポートします。

from vllm import LLM, SamplingParams
from vllm.model_executor.layers.spec_decode import MultiStepSpeculativeModel

ステップ3:Speculative Modelの作成とLLMの初期化

ここが最も重要な設定部分です。MultiStepSpeculativeModelクラスを使ってドラフトモデルをラップし、それをLLMクラスのspeculative_model引数に渡します。

# 1. ドラフトモデルをSpeculative Modelとして準備
speculative_model = MultiStepSpeculativeModel(
    "meta-llama/Meta-Llama-3-1.8B-Instruct", # 小さいドラフトモデル
    num_speculative_tokens=5, # 一度に投機するトークン数。3〜7が一般的。
)

# 2. ターゲットモデル(本番モデル)をLLMとして初期化。speculative_modelを渡す。
llm = LLM(
    model="meta-llama/Meta-Llama-3-8B-Instruct", # 大きいターゲットモデル
    speculative_model=speculative_model, # ここに設定を渡す
    tensor_parallel_size=1, # GPU数に応じて調整
    gpu_memory_utilization=0.9,
)

print("Speculative Decodingが有効化されたLLMを初期化しました。")

num_speculative_tokensは、ドラフトモデルが一度に何トークン先まで予測するかを決めます。大きすぎると予測精度が下がり、小さすぎると効果が限定的です。まずは5前後で試すことをお勧めします。

ステップ4:推論の実行とベンチマーク

通常のvLLMと同様に推論を実行します。Speculative Decodingの効果を測定するために、有効/無効で速度比較を行いましょう。

# 推論用のプロンプトとサンプリングパラメータ
prompts = [
    "日本の首都はどこですか?その都市の魅力を300文字で説明してください。",
]
sampling_params = SamplingParams(temperature=0.7, max_tokens=256)

# 推論実行
outputs = llm.generate(prompts, sampling_params)

# 結果の出力
for output in outputs:
    generated_text = output.outputs[0].text
    print(f"生成テキスト: {generated_text[:200]}...")
    # 速度メトリクス(vLLMは内部統計を保持)
    # throughput (tok/s) は出力されたRequestOutputのmetadataからも取得可能

速度比較を行うには、speculative_modelを渡さずに同じターゲットモデルだけでLLMを初期化し、同じプロンプトで推論時間を計測します。vLLMの組み込み統計機能や、Pythonのtimeモジュールを使って測定できます。

コード例・コマンド例:エラー回避と最適化のテクニック

エラー「RuntimeError: The speculated token …」への対処

このエラーが発生した場合、まずは両モデルのトークナイザーが本当に一致しているか確認します。以下のスクリプトで検証できます。

from transformers import AutoTokenizer

target_tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct")
draft_tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-1.8B-Instruct")

# テスト用テキスト
test_text = "Hello, speculative decoding!"
target_ids = target_tokenizer.encode(test_text)
draft_ids = draft_tokenizer.encode(test_text)

print(f"Target Token IDs: {target_ids}")
print(f"Draft Token IDs: {draft_ids}")
print(f"一致していますか?: {target_ids == draft_ids}")

# 語彙サイズも確認
print(f"Target vocab size: {target_tokenizer.vocab_size}")
print(f"Draft vocab size: {draft_tokenizer.vocab_size}")

IDが一致しない、または語彙サイズが異なる場合は、そのモデルペアは使用できません。必ず同一ファミリーのモデルを選び直してください。

速度が向上しない場合のチューニング

推論速度(Throughput, tok/s)が思ったように上がらない場合は、以下のパラメータを調整してみてください。

speculative_model = MultiStepSpeculativeModel(
    "meta-llama/Meta-Llama-3-1.8B-Instruct",
    num_speculative_tokens=3,  # 値を小さくする(3, 4, 5...と試す)
)

llm = LLM(
    model="meta-llama/Meta-Llama-3-8B-Instruct",
    speculative_model=speculative_model,
    enforce_eager=True,  # グラフ最適化を無効にし、オーバーヘッドを減らす(場合により有効)
    max_model_len=4096,   # 必要に応じてコンテキスト長を制限しメモリ効率を向上
)

また、バッチサイズ(同時処理するリクエスト数)を増やすことで、Speculative Decodingの効果がより顕著になることがあります。実サービスでは、一定数のリクエストをまとめて処理するようにスケジューリングを検討しましょう。

まとめ・補足情報

vLLMのSpeculative Decodingは、適切なモデルペアと設定を用いることで、推論速度を1.5倍から2.5倍に向上させる強力な技術です。成功の鍵は以下の3点に集約されます。

  1. トークナイザーの完全な一致: 必ず同一ファミリーのモデル(例:Llama 3の8Bと1.8B)を組み合わせる。
  2. 正しい初期化手順: MultiStepSpeculativeModelを作成し、LLMクラスのspeculative_model引数に渡す。
  3. バランスの取れたモデル選択: ターゲットモデルに対して、十分に小さく、かつある程度予測精度のあるドラフトモデルを選ぶ(サイズ比は1:4〜1:10が目安)。

現在のvLLMのSpeculative Decodingは、ドラフトモデルにもターゲットモデルと同様のGPUメモリを必要とする点に注意が必要です。将来のアップデートでは、よりメモリ効率の良い方式が導入される可能性があります。最新の情報はvLLMの公式GitHubリポジトリを確認することをお勧めします。

本記事の手順を参考に、高速で効率的なLLM推論サービスの構築に挑戦してみてください。

この記事は役に立ちましたか?