- プレイブックの概要
- Phase 1: 現在のサービング状態を測定する
- Phase 2: Draft モデルの選択
- Phase 3: vLLM のサービング設定
- Phase 4: Accept Ratio のモニタリング
- Phase 5: トラフィッククラス別のルーティング
- Phase 6: ロールバックと Fallback
- Phase 7: 定期点検 (週次)
- トラブルシューティング
- デプロイ前チェックリスト
- クイズ
- 参考資料

プレイブックの概要
本ドキュメントは、LLM サービングに speculative decoding を導入する際に従うべき段階別の実行ガイドである。概念の説明よりも「どの順序で、どの設定で、どの基準で判断するか」に焦点を当てた。
Speculative decoding の中心的なアイデアは単純だ。小さく速い draft モデルが複数のトークンを一度に推測し、大きく正確な target モデルがそれを一度の forward pass で検証する。Draft トークンの大半が受理(accept)されれば、target モデルが一トークンずつ生成するより 2-3 倍速い。原論文は Leviathan et al.(arXiv:2211.17192) が提示したもので、出力分布が target モデルと数学的に同一であることが証明されている。
Phase 1: 現在のサービング状態を測定する
speculative decoding の導入前に、現状を定量的に把握しておく必要がある。比較の基準がなければ効果を証明できない。
ベースライン測定スクリプト
import time
import json
import statistics
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
def measure_baseline(
model: str,
prompts_file: str,
num_runs: int = 3,
) -> dict:
"""現在のサービングの latency/throughput ベースラインを測定"""
prompts = json.load(open(prompts_file))
all_ttft = [] # Time To First Token
all_tpot = [] # Time Per Output Token
all_e2e = [] # End-to-End latency
total_tokens = 0
for run in range(num_runs):
for prompt in prompts:
start = time.perf_counter()
first_token_time = None
token_count = 0
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt["text"]}],
max_tokens=prompt.get("max_tokens", 256),
temperature=0.0,
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
if first_token_time is None:
first_token_time = time.perf_counter()
token_count += 1
end = time.perf_counter()
all_ttft.append(first_token_time - start)
all_e2e.append(end - start)
if token_count > 1:
all_tpot.append((end - first_token_time) / (token_count - 1))
total_tokens += token_count
return {
"ttft_p50_ms": round(statistics.median(all_ttft) * 1000, 1),
"ttft_p95_ms": round(sorted(all_ttft)[int(len(all_ttft) * 0.95)] * 1000, 1),
"tpot_p50_ms": round(statistics.median(all_tpot) * 1000, 1),
"tpot_p95_ms": round(sorted(all_tpot)[int(len(all_tpot) * 0.95)] * 1000, 1),
"e2e_p50_ms": round(statistics.median(all_e2e) * 1000, 1),
"e2e_p95_ms": round(sorted(all_e2e)[int(len(all_e2e) * 0.95)] * 1000, 1),
"total_tokens": total_tokens,
"avg_tokens_per_sec": round(total_tokens / sum(all_e2e), 1),
}
# 使用例
baseline = measure_baseline("meta-llama/Llama-3.1-70B-Instruct", "eval_prompts.json")
json.dump(baseline, open("baseline_metrics.json", "w"), indent=2)
print(json.dumps(baseline, indent=2))
測定項目の定義
| 指標 | 説明 | 期待される改善幅 |
|---|---|---|
| TTFT (Time To First Token) | 最初のトークンまでの待機時間 | 変化なしか小幅に増加 |
| TPOT (Time Per Output Token) | トークンあたり生成時間 | 2-3x 改善 |
| E2E Latency | 応答完了までの総時間 | 1.5-2.5x 改善 |
| Throughput (tokens/sec) | 秒あたり生成トークン数 | 1.5-2.5x 改善 |
| Accept ratio | Draft トークンの受理率 | 0.6-0.85 が目標 |
Phase 2: Draft モデルの選択
Draft モデルの選択が speculative decoding の性能の 70% を決める。誤った選択をすると、かえって baseline より遅くなる。
選択基準と候補
Target モデル -> Draft モデルのマッチングガイド
Llama 3.1 70B -> Llama 3.1 8B (同じ family、語彙が同一)
または EAGLE-3 draft head (学習が必要、最高性能)
Mistral Large -> Mistral 7B (同じ tokenizer)
Qwen 2.5 72B -> Qwen 2.5 1.5B または Qwen 2.5 7B
自前学習モデル -> 同じ tokenizer の小型モデル
または Medusa head / EAGLE head の学習
Draft モデル種別ごとの比較
| 種別 | 代表手法 | Accept ratio | 追加メモリ | 学習の要否 | 論文 |
|---|---|---|---|---|---|
| 独立した小型モデル | Vanilla SD | 0.5-0.7 | モデルサイズ分 | 不要 | arXiv:2211.17192 |
| Medusa heads | Medusa | 0.6-0.75 | ~数百 MB | 軽量な学習 | arXiv:2401.10774 |
| EAGLE head | EAGLE-1/2/3 | 0.7-0.85 | ~1-2 GB | 学習が必要 | arXiv:2401.15077 |
| Self-speculative | LayerSkip | 0.4-0.6 | なし | 不要 | - |
| N-gram ベース | Prompt Lookup | 0.3-0.6 | なし | 不要 | - |
Draft モデルの互換性検証
from transformers import AutoTokenizer
def verify_draft_compatibility(target_model: str, draft_model: str) -> dict:
"""Draft/Target モデル間の tokenizer 互換性を検証"""
target_tok = AutoTokenizer.from_pretrained(target_model)
draft_tok = AutoTokenizer.from_pretrained(draft_model)
# 1. 語彙サイズの一致を確認
vocab_match = target_tok.vocab_size == draft_tok.vocab_size
# 2. 特殊トークンの一致を確認
special_match = (
target_tok.bos_token_id == draft_tok.bos_token_id and
target_tok.eos_token_id == draft_tok.eos_token_id and
target_tok.pad_token_id == draft_tok.pad_token_id
)
# 3. サンプルテキストのエンコード結果を比較
test_texts = [
"Hello, how are you?",
"ソウルの天気はどうですか?",
"def fibonacci(n): return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)",
]
encoding_match = all(
target_tok.encode(t) == draft_tok.encode(t) for t in test_texts
)
return {
"vocab_size_match": vocab_match,
"special_tokens_match": special_match,
"encoding_match": encoding_match,
"compatible": vocab_match and special_match and encoding_match,
"target_vocab_size": target_tok.vocab_size,
"draft_vocab_size": draft_tok.vocab_size,
}
result = verify_draft_compatibility(
"meta-llama/Llama-3.1-70B-Instruct",
"meta-llama/Llama-3.1-8B-Instruct"
)
print(result)
# {'vocab_size_match': True, 'special_tokens_match': True,
# 'encoding_match': True, 'compatible': True, ...}
Phase 3: vLLM のサービング設定
独立した Draft モデル方式
# vLLM で speculative decoding を有効化 (Llama 3.1 70B + 8B)
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-70B-Instruct \
--speculative-model meta-llama/Llama-3.1-8B-Instruct \
--num-speculative-tokens 5 \
--speculative-disable-mqa-scorer \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.92 \
--max-model-len 4096 \
--port 8000
EAGLE-3 方式 (推奨)
# EAGLE-3 draft head を使用 (より高い accept ratio)
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-70B-Instruct \
--speculative-model eagle3-llama3.1-70b-instruct \
--speculative-method eagle \
--num-speculative-tokens 5 \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.92 \
--max-model-len 4096 \
--use-v2-block-manager \
--port 8000
主要パラメータのチューニング
# speculative_decoding_config.yaml
# この設定を基に実験し、accept ratio と latency を見ながら調整する
# Draft トークン数: 多すぎると reject が増え、少なすぎると利点が減る
num_speculative_tokens: 5 # 開始値。3-7 の範囲で実験する
# Speculative decoding が効果を持たない場合は自動的に無効化
speculative_disable_by_batch_size: 8 # バッチ 8 以上なら無効化
# temperature > 0 のときの扱い
# typical acceptance sampling を使用 (品質を維持)
speculative_draft_tensor_parallel_size: 1 # draft は TP=1 で十分
num_speculative_tokens の最適値を見つける
import subprocess
import json
def find_optimal_spec_tokens(
target_model: str,
draft_model: str,
eval_prompts: str,
candidates: list[int] = [3, 4, 5, 6, 7, 8],
) -> dict:
"""さまざまな num_speculative_tokens の値でベンチマークを実行"""
results = {}
for n in candidates:
print(f"Testing num_speculative_tokens={n}")
# サーバ起動 (実際には subprocess で管理)
# ここでは結果収集のロジックのみ示す
metrics = run_benchmark(target_model, draft_model, n, eval_prompts)
results[n] = {
"accept_ratio": metrics["accept_ratio"],
"tpot_p50_ms": metrics["tpot_p50_ms"],
"e2e_speedup": metrics["baseline_e2e"] / metrics["e2e_p50_ms"],
"gpu_memory_gb": metrics["gpu_memory_gb"],
}
# 最適値の選択: speedup が最も高い値
best_n = max(results, key=lambda n: results[n]["e2e_speedup"])
results["recommended"] = best_n
return results
Phase 4: Accept Ratio のモニタリング
Accept ratio は speculative decoding の健全性を示す中心的な指標だ。vLLM はこのメトリクスを自前で公開している。
Prometheus メトリクスの収集
# prometheus.yml - vLLM メトリクスのスクレイピング設定
scrape_configs:
- job_name: 'vllm-speculative'
scrape_interval: 15s
static_configs:
- targets: ['vllm-server:8000']
metrics_path: /metrics
vLLM が公開する speculative decoding 関連のメトリクス:
# draft トークンの受理率
vllm:spec_decode_draft_acceptance_rate
# 位置別の受理率 (position 0 が最も高く、後ろに行くほど下がる)
vllm:spec_decode_per_position_acceptance_rate{position="0"}
vllm:spec_decode_per_position_acceptance_rate{position="1"}
# 平均受理長
vllm:spec_decode_mean_accepted_length
Grafana のアラートルール
# grafana_alerts.yaml
groups:
- name: speculative_decoding_alerts
rules:
# Accept ratio が 0.5 以下に下がったら警告
- alert: LowAcceptRatio
expr: vllm:spec_decode_draft_acceptance_rate < 0.5
for: 10m
labels:
severity: warning
annotations:
summary: 'Speculative decoding accept ratio の低下'
description: |
Accept ratio が {{ $value | printf "%.2f" }} まで低下しました。
0.5 未満では speculative decoding がかえって overhead になります。
Draft モデルの交換または speculative decoding の無効化を検討してください。
# Accept ratio が 0.3 以下なら即時の無効化を推奨
- alert: CriticalAcceptRatio
expr: vllm:spec_decode_draft_acceptance_rate < 0.3
for: 5m
labels:
severity: critical
annotations:
summary: 'Speculative decoding の無効化が必要'
description: |
Accept ratio {{ $value | printf "%.2f" }}。直ちに fallback デコーディングへ切り替えてください。
Phase 5: トラフィッククラス別のルーティング
すべてのリクエストに speculative decoding を適用してはならない。リクエストの特性によって効果が大きく異なるからだ。
ルーティングの意思決定マトリクス
| リクエスト特性 | Speculative Decoding | 理由 |
|---|---|---|
| 長い出力 (256+ tokens) | ON | トークン生成時間が支配的なので効果が最大 |
| 短い出力 (< 32 tokens) | OFF | Draft モデルのオーバーヘッドが利点を上回る |
| temperature=0 (greedy) | ON (最適) | Draft の予測精度が最も高い |
| temperature > 1.0 | OFF | 高いランダム性で accept ratio が急落 |
| 高い同時リクエスト (batch > 8) | OFF | バッチ処理時に speculative のオーバーヘッドが増加 |
| ストリーミング応答 | ON (条件付き) | TTFT の増加を許容できる場合 |
NGINX ベースのルーティング設定
# /etc/nginx/conf.d/llm-router.conf
upstream vllm_speculative {
server 10.0.1.10:8000; # speculative decoding 有効サーバ
}
upstream vllm_standard {
server 10.0.1.20:8000; # 標準デコーディングサーバ
}
# Lua ベースの動的ルーティング
server {
listen 80;
location /v1/chat/completions {
access_by_lua_block {
local cjson = require "cjson"
ngx.req.read_body()
local body = cjson.decode(ngx.req.get_body_data())
-- ルーティング条件の判定
local use_speculative = true
-- temperature が高ければ標準デコーディング
if body.temperature and body.temperature > 1.0 then
use_speculative = false
end
-- max_tokens が短ければ標準デコーディング
if body.max_tokens and body.max_tokens < 32 then
use_speculative = false
end
-- stream=false かつ短い応答なら標準デコーディング
if not body.stream and body.max_tokens and body.max_tokens < 64 then
use_speculative = false
end
if use_speculative then
ngx.var.upstream = "vllm_speculative"
else
ngx.var.upstream = "vllm_standard"
end
}
proxy_pass http://$upstream;
proxy_set_header Host $host;
}
}
Phase 6: ロールバックと Fallback
自動 Fallback の判定ロジック
import requests
import time
from dataclasses import dataclass
@dataclass
class FallbackConfig:
accept_ratio_threshold: float = 0.4
latency_regression_pct: float = 20.0 # baseline 比で 20% 以上遅くなったら
check_interval_sec: int = 60
consecutive_failures: int = 3
class SpeculativeDecodingGuard:
"""Speculative decoding の状態を監視し、自動 fallback を判断する"""
def __init__(self, config: FallbackConfig, prometheus_url: str):
self.config = config
self.prometheus_url = prometheus_url
self.failure_count = 0
def query_prometheus(self, query: str) -> float:
resp = requests.get(
f"{self.prometheus_url}/api/v1/query",
params={"query": query},
)
result = resp.json()["data"]["result"]
return float(result[0]["value"][1]) if result else 0.0
def should_fallback(self) -> tuple[bool, str]:
# 1. Accept ratio の確認
accept_ratio = self.query_prometheus(
'vllm:spec_decode_draft_acceptance_rate'
)
if accept_ratio < self.config.accept_ratio_threshold:
self.failure_count += 1
if self.failure_count >= self.config.consecutive_failures:
return True, f"accept_ratio={accept_ratio:.2f} < {self.config.accept_ratio_threshold}"
else:
self.failure_count = 0
# 2. Latency regression の確認
current_p95 = self.query_prometheus(
'histogram_quantile(0.95, rate(vllm:e2e_request_latency_seconds_bucket[5m]))'
)
baseline_p95 = self.query_prometheus(
'vllm:baseline_e2e_p95_seconds' # ベースラインのメトリクスを別途記録する必要あり
)
if baseline_p95 > 0:
regression_pct = ((current_p95 - baseline_p95) / baseline_p95) * 100
if regression_pct > self.config.latency_regression_pct:
return True, f"latency regression {regression_pct:.1f}% > {self.config.latency_regression_pct}%"
return False, "healthy"
def run(self):
while True:
should_fb, reason = self.should_fallback()
if should_fb:
print(f"[FALLBACK] Speculative decoding を無効化: {reason}")
self.trigger_fallback()
time.sleep(self.config.check_interval_sec)
def trigger_fallback(self):
"""標準デコーディングサーバへトラフィックを切り替える"""
# 実際の実装: ロードバランサの重み変更または feature flag のトグル
requests.post(
"http://config-server/api/v1/flags",
json={"speculative_decoding_enabled": False},
)
Phase 7: 定期点検 (週次)
週次点検の自動化スクリプト
import json
import datetime
from typing import Any
def weekly_speculative_decoding_report(
prometheus_url: str,
baseline_file: str,
) -> dict[str, Any]:
"""週次の speculative decoding 運用レポートを生成"""
baseline = json.load(open(baseline_file))
report = {
"report_date": datetime.date.today().isoformat(),
"period": "last_7d",
}
# 1. Accept ratio の推移
report["accept_ratio"] = {
"current_avg": query_prom(prometheus_url,
'avg_over_time(vllm:spec_decode_draft_acceptance_rate[7d])'),
"min": query_prom(prometheus_url,
'min_over_time(vllm:spec_decode_draft_acceptance_rate[7d])'),
"max": query_prom(prometheus_url,
'max_over_time(vllm:spec_decode_draft_acceptance_rate[7d])'),
}
# 2. Latency の改善率
current_e2e_p50 = query_prom(prometheus_url,
'histogram_quantile(0.5, rate(vllm:e2e_request_latency_seconds_bucket[7d]))')
report["speedup"] = {
"e2e_p50_speedup": round(baseline["e2e_p50_ms"] / (current_e2e_p50 * 1000), 2),
"baseline_e2e_p50_ms": baseline["e2e_p50_ms"],
"current_e2e_p50_ms": round(current_e2e_p50 * 1000, 1),
}
# 3. Fallback の発生回数
report["fallback_count"] = int(query_prom(prometheus_url,
'count_over_time(ALERTS{alertname="LowAcceptRatio"}[7d])'))
# 4. リソース使用量 (speculative の追加分)
report["gpu_memory_overhead_gb"] = query_prom(prometheus_url,
'avg_over_time(vllm:gpu_cache_usage_perc[7d])') * 80 # A100 80GB 基準
# 5. 推奨事項
recommendations = []
if report["accept_ratio"]["current_avg"] < 0.55:
recommendations.append("Draft モデルの交換または EAGLE-3 head の学習を推奨")
if report["speedup"]["e2e_p50_speedup"] < 1.3:
recommendations.append("改善幅が 1.3x 未満。費用対効果の再検討が必要")
if report["fallback_count"] > 5:
recommendations.append(f"週次 fallback {report['fallback_count']}回。Draft モデルの品質点検")
report["recommendations"] = recommendations
return report
トラブルシューティング
1. Speculative decoding 適用後にかえって遅くなる
症状: E2E latency が baseline 比で 10-30% 増加
診断の順序:
# 1. Accept ratio の確認
curl -s http://localhost:8000/metrics | grep spec_decode_draft_acceptance_rate
# 0.3 未満なら draft モデルの問題
# 2. Draft モデルの推論時間を確認
curl -s http://localhost:8000/metrics | grep spec_decode_draft_latency
# Draft の推論が target の単一トークン推論の 50% 以上なら非効率
# 3. GPU メモリ不足による swap の確認
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
# 95% 以上なら KV cache 不足で speculative の利点が相殺される
解決: accept ratio < 0.5 なら draft モデルを交換する。GPU メモリ不足なら num_speculative_tokens を 3 に下げる。
2. 特定のプロンプトでのみ accept ratio が急落する
原因: コード生成や数式計算など、draft モデルが苦手なドメイン
解決: リクエスト分類器を追加し、該当ドメインは標準デコーディングへルーティングする。
3. ストリーミング応答でトークンがまとまって出る
症状: ユーザーが体感するストリーミングが途切れる -> 複数トークンが一度に -> 途切れる、の繰り返し
原因: Speculative decoding は draft トークンを一度に検証するため、受理されたトークンが burst で届く
解決: クライアント側でトークンをバッファリングし、一定間隔でレンダリングする。または --disable-frontend-multiprocessing オプションを確認する。
4. RuntimeError: Draft model and target model have different vocab sizes
RuntimeError: Draft model vocab size 32000 != target model vocab size 128256
原因: tokenizer が異なるモデル family を draft として使った場合 (例: Llama 2 draft + Llama 3 target)
解決: 同じ model family、同じ tokenizer を使う draft モデルへ交換する。
デプロイ前チェックリスト
- ベースライン測定の完了 (TTFT, TPOT, E2E, throughput)
- Draft/Target モデルの tokenizer 互換性検証を通過
-
num_speculative_tokensの最適値の実験が完了 - Accept ratio > 0.55 を確認 (プロダクショントラフィックのサンプル)
- E2E latency の speedup > 1.3x を確認
- GPU メモリ使用量が 95% 未満であることを確認
- Fallback 自動切り替えロジックの実装とテストが完了
- トラフィッククラス別のルーティングルールの設定が完了
- Prometheus メトリクス + Grafana ダッシュボードの構成が完了
- 週次点検の自動化スクリプトのデプロイが完了
- オンコールチームへ speculative decoding 障害対応ランブックを共有済み
クイズ
Q1. Speculative decoding が出力分布を変えない理由は?
答え: ||Draft モデルが生成したトークンを target モデルが検証する際に rejection sampling を用いるからである。
受理されたトークンは target モデルの分布と正確に一致し、棄却された位置では target モデルが
再サンプリングする。||
Q2. Accept ratio が 0.3 のとき speculative decoding を維持すべきか?
答え: ||維持すべきではない。Accept ratio 0.3 では 5 個の draft トークンのうち平均 1.5 個しか受理されず、draft モデルの推論
オーバーヘッドが利点を相殺する。一般に 0.5 未満なら標準デコーディングの方が速い。||
Q3. EAGLE-3 が独立した小型 draft モデルより accept ratio が高い理由は?
答え: ||EAGLE-3 は target モデルの second-to-top-layer feature を入力として次のトークンを
予測するため、target モデルの内部表現へ直接アクセスできるからである。独立したモデルはこうした情報なしに
自前で予測しなければならない。||
Q4. 大きな batch size で speculative decoding が非効率になる理由は?
答え: ||Speculative decoding の利点は autoregressive decoding の memory-bound な特性を緩和することにあるが、
batch size が大きくなるとすでに compute-bound となり、speculation の利点が減るからである。さらに draft
トークンの検証に必要な追加メモリと演算がバッチ処理の効率を下げる。||
Q5. Temperature が高いと accept ratio が下がる理由は?
答え: ||Temperature が高いと target モデルの出力分布がより uniform になるため、draft モデルが正確な
トークンを予測しにくくなる。Greedy decoding(temperature=0) では最も確率の高いトークンを一つ
当てればよいが、高い temperature では予測の不確実性が大きくなる。||
Q6. vLLM の speculative_disable_by_batch_size パラメータの役割は?
答え: ||処理中のリクエスト数(batch size)が指定値以上になると speculative decoding を自動的に
無効化し、標準デコーディングへ切り替える。高い同時実行性での性能低下を防ぐ安全装置である。||
参考資料
- Fast Inference from Transformers via Speculative Decoding (arXiv:2211.17192)
- Medusa: Simple LLM Inference Acceleration Framework (arXiv:2401.10774)
- EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty (arXiv:2401.15077)
- vLLM Speculative Decoding Documentation
- vLLM Speculators v0.3.0 Blog Post
- EAGLE-3: Scaling up Inference Acceleration (arXiv:2503.01840)