- Authors
- Name

- ベンチマークの目的と範囲
- テスト環境
- ベンチマーク結果:Llama 3.1 70B Instruct
- ベンチマーク結果:同時リクエスト数(Concurrency)の影響
- ベンチマーク結果:コスト効率分析
- num_speculative_tokens感度分析
- 再現可能なベンチマーク実行方法
- プロダクション適用推奨事項
- ベンチマークの限界
- クイズ
- 参考文献
ベンチマークの目的と範囲
本文書は2026年初頭時点でspeculative decoding技法を同一ハードウェア、同一プロンプトセット、同一測定方法論で比較したプロダクションベンチマーク結果を整理する。学術論文の理想的な条件ではなく、実際のサービング環境での数値を提供することが目標である。
比較対象技法:
- Vanilla Speculative Decoding(独立draftモデル) - Leviathan et al., arXiv:2211.17192
- Medusa(多重デコーディングヘッド) - Cai et al., arXiv:2401.10774
- EAGLE-1/EAGLE-3(feature-level speculation) - Li et al., arXiv:2401.15077 / arXiv:2503.01840
- Prompt Lookup Decoding(N-gramマッチング、学習不要)
比較対象から除外した技法と理由:
- Staged Speculative Decoding:実装複雑度に対し実務採用率が低い
- REST(Retrieval-based):外部データストア依存によりサービングアーキテクチャの変更が必要
テスト環境
ハードウェア
GPU: 4x NVIDIA A100 80GB SXM (NVLink接続)
CPU: AMD EPYC 7763 64-Core
RAM: 512GB DDR4
OS: Ubuntu 22.04 LTS
CUDA: 12.4
Driver: 550.90.07
ソフトウェアスタック
vLLM: 0.7.3
PyTorch: 2.5.1
Transformers: 4.47.0
Python: 3.11.10
Targetモデル
| モデル | パラメータ | Tensor Parallel | 備考 |
|---|---|---|---|
| Llama 3.1 70B Instruct | 70B | TP=4 | 主力ベンチマークモデル |
| Qwen 2.5 72B Instruct | 72B | TP=4 | クロス検証用 |
| Mistral Large 2 (123B) | 123B | TP=4 | 大型モデル確認用 |
プロンプトセットの構成
単一タイプではなく、実際のプロダクショントラフィック分布を反映したプロンプトセットを構成した。
# プロンプトセット構成(500件)
prompt_distribution = {
"short_qa": 150, # 1-2文の質問、予想出力50-100トークン
"summarization": 100, # 500-1000単語の文書要約、予想出力150-300トークン
"code_generation": 80, # 関数/クラス生成、予想出力100-500トークン
"creative_writing": 50, # ストーリー/エッセイ、予想出力300-800トークン
"translation": 70, # 韓英/英韓翻訳、予想出力100-300トークン
"structured_output": 50, # JSON/YAML生成、予想出力50-200トークン
}
ベンチマーク実行コード
import json
import time
import statistics
from dataclasses import dataclass, asdict
from openai import OpenAI
@dataclass
class BenchmarkResult:
method: str
model: str
draft_model: str
num_spec_tokens: int
prompt_category: str
ttft_ms: float
tpot_ms: float
e2e_ms: float
output_tokens: int
accept_ratio: float
gpu_memory_gb: float
def run_single_benchmark(
client: OpenAI,
model: str,
prompt: str,
max_tokens: int,
) -> dict:
"""単一プロンプトに対するベンチマーク実行"""
start = time.perf_counter()
first_token_time = None
token_count = 0
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=0.0, # greedy decodingで統一
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
if first_token_time is None:
first_token_time = time.perf_counter()
token_count += 1
end = time.perf_counter()
ttft = (first_token_time - start) if first_token_time else (end - start)
e2e = end - start
tpot = (end - first_token_time) / max(token_count - 1, 1) if first_token_time else e2e
return {
"ttft_ms": round(ttft * 1000, 2),
"tpot_ms": round(tpot * 1000, 2),
"e2e_ms": round(e2e * 1000, 2),
"output_tokens": token_count,
}
def run_full_benchmark(
base_url: str,
model: str,
prompts: list[dict],
warmup_runs: int = 5,
measure_runs: int = 3,
) -> list[dict]:
"""全プロンプトセットに対するベンチマーク実行"""
client = OpenAI(base_url=f"{base_url}/v1", api_key="benchmark")
# Warmup
for i in range(warmup_runs):
run_single_benchmark(client, model, prompts[0]["text"], 64)
results = []
for run_idx in range(measure_runs):
for prompt in prompts:
result = run_single_benchmark(
client, model, prompt["text"], prompt["max_tokens"]
)
result["category"] = prompt["category"]
result["run"] = run_idx
results.append(result)
return results
ベンチマーク結果:Llama 3.1 70B Instruct
全体要約(500プロンプト、3回反復平均)
| 技法 | Draftモデル | Accept Ratio | TPOT P50 (ms) | TPOT P95 (ms) | E2E Speedup | 追加GPUメモリ |
|---|---|---|---|---|---|---|
| Baseline (no spec) | - | - | 42.3 | 58.7 | 1.00x | 0 GB |
| Vanilla SD | Llama 3.1 8B | 0.62 | 19.8 | 31.2 | 1.95x | ~16 GB |
| Medusa-2 | 5 heads | 0.68 | 17.1 | 27.8 | 2.21x | ~0.8 GB |
| EAGLE-1 | EAGLE head | 0.73 | 14.9 | 24.1 | 2.52x | ~1.5 GB |
| EAGLE-3 | EAGLE-3 head | 0.81 | 12.3 | 19.6 | 2.89x | ~1.8 GB |
| Prompt Lookup | N-gram (n=3) | 0.41 | 28.5 | 45.3 | 1.38x | 0 GB |
カテゴリ別詳細結果(EAGLE-3基準)
プロンプトタイプによる性能差が大きいため、カテゴリ別の結果を個別に確認する必要がある。
| カテゴリ | Accept Ratio | E2E Speedup | TPOT P50 (ms) | 特記事項 |
|---|---|---|---|---|
| short_qa | 0.84 | 2.95x | 11.8 | 短い出力だが予測精度が高い |
| summarization | 0.83 | 3.12x | 11.5 | 最大のspeedup |
| code_generation | 0.72 | 2.31x | 15.2 | 構文予測は高いがロジック部で低下 |
| creative_writing | 0.76 | 2.67x | 13.4 | 意外にも高いaccept ratio |
| translation | 0.85 | 3.05x | 11.9 | 翻訳は入力への依存度が高く予測が容易 |
| structured_output | 0.88 | 3.21x | 10.9 | JSON/YAMLの構造的パターンが予測に有利 |
Temperature変化に伴うAccept Ratioの推移
# Temperature別accept ratio測定結果(EAGLE-3、Llama 3.1 70B)
temperature_results = {
0.0: {"accept_ratio": 0.81, "speedup": 2.89},
0.3: {"accept_ratio": 0.76, "speedup": 2.61},
0.5: {"accept_ratio": 0.71, "speedup": 2.38},
0.7: {"accept_ratio": 0.64, "speedup": 2.08},
1.0: {"accept_ratio": 0.55, "speedup": 1.72},
1.2: {"accept_ratio": 0.47, "speedup": 1.41},
1.5: {"accept_ratio": 0.38, "speedup": 1.15}, # ほぼ効果なし
}
# 結論:temperature > 1.0の場合speculative decodingを無効化推奨
TEMP_THRESHOLD = 1.0
ベンチマーク結果:同時リクエスト数(Concurrency)の影響
実際のプロダクションでは単一リクエストではなく多数の同時リクエストを処理する必要がある。Concurrency増加に伴うspeculative decodingの効果変化を測定した。
同時リクエスト数別の性能(EAGLE-3、Llama 3.1 70B)
| Concurrency | Throughput (tokens/s) | E2E Speedup | Accept Ratio | 備考 |
|---|---|---|---|---|
| 1 | 81.3 | 2.89x | 0.81 | 最適条件 |
| 2 | 156.2 | 2.71x | 0.80 | ほぼ維持 |
| 4 | 289.5 | 2.43x | 0.79 | わずかに低下 |
| 8 | 498.7 | 1.98x | 0.78 | 低下開始 |
| 16 | 721.3 | 1.52x | 0.77 | 顕著な低下 |
| 32 | 890.4 | 1.18x | 0.76 | 効果微小 |
| 64 | 965.1 | 0.95x | 0.75 | 逆効果が発生 |
# 同時リクエスト別ベンチマーク実行コード
import asyncio
import aiohttp
import time
async def concurrent_benchmark(
base_url: str,
model: str,
prompts: list[dict],
concurrency: int,
) -> dict:
"""同時リクエスト数別ベンチマーク"""
semaphore = asyncio.Semaphore(concurrency)
results = []
async def single_request(session, prompt):
async with semaphore:
start = time.perf_counter()
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt["text"]}],
"max_tokens": prompt["max_tokens"],
"temperature": 0.0,
"stream": False,
}
async with session.post(
f"{base_url}/v1/chat/completions",
json=payload,
) as resp:
data = await resp.json()
end = time.perf_counter()
tokens = data["usage"]["completion_tokens"]
return {
"e2e_ms": (end - start) * 1000,
"output_tokens": tokens,
"tokens_per_sec": tokens / (end - start),
}
async with aiohttp.ClientSession() as session:
tasks = [single_request(session, p) for p in prompts]
total_start = time.perf_counter()
results = await asyncio.gather(*tasks)
total_elapsed = time.perf_counter() - total_start
total_tokens = sum(r["output_tokens"] for r in results)
return {
"concurrency": concurrency,
"total_tokens": total_tokens,
"total_elapsed_sec": round(total_elapsed, 2),
"throughput_tokens_per_sec": round(total_tokens / total_elapsed, 1),
"avg_e2e_ms": round(statistics.mean(r["e2e_ms"] for r in results), 1),
"p95_e2e_ms": round(
sorted(r["e2e_ms"] for r in results)[int(len(results) * 0.95)], 1
),
}
核心的な観察:Concurrency 32以上ではspeculative decodingの利点が消失し、64以上ではむしろthroughputが低下した。これはKV cacheメモリ競合とdraftモデルの追加演算オーバーヘッドが原因である。
ベンチマーク結果:コスト効率分析
サービングコストはGPU時間で決定されるため、単純なレイテンシ改善よりも「同一予算でどれだけ多くのリクエストを処理できるか」がプロダクションではより重要である。
コスト効率比較(A100 80GB x4、時間あたり$13.04基準)
| 技法 | 時間あたり処理量 (requests) | リクエストあたりコスト ($) | Baseline対比コスト削減 |
|---|---|---|---|
| Baseline | 3,200 | $0.00408 | - |
| Vanilla SD | 5,440 | $0.00240 | 41%削減 |
| EAGLE-3 | 7,680 | $0.00170 | 58%削減 |
| Medusa-2 | 6,400 | $0.00204 | 50%削減 |
注意:上記の数値はconcurrency 8基準である。実際のサービングではトラフィックパターン、リクエストサイズ分布、SLO要件によって異なる。
Draftモデル別学習/維持コスト
| 技法 | 初期学習コスト | 学習時間 | Targetモデル更新時の再学習 |
|---|---|---|---|
| Vanilla SD | $0(既存モデル使用) | 0 | 不要 |
| Medusa-2 | ~$50(A100 1枚、3時間) | 3時間 | 必要 |
| EAGLE-1 | ~$100(A100 1枚、6時間) | 6時間 | 必要 |
| EAGLE-3 | ~$200(A100 1枚、12時間) | 12時間 | 必要 |
num_speculative_tokens感度分析
num_speculative_tokens値に応じた性能変化をEAGLE-3、Llama 3.1 70Bの組み合わせで測定した。
| num_speculative_tokens | Accept Ratio | Mean Accepted Length | TPOT P50 (ms) | E2E Speedup | GPUメモリ増加 |
|---|---|---|---|---|---|
| 1 | 0.91 | 0.91 | 35.2 | 1.20x | +0.3 GB |
| 3 | 0.86 | 2.58 | 16.8 | 2.52x | +0.8 GB |
| 5 | 0.81 | 4.05 | 12.3 | 2.89x | +1.8 GB |
| 7 | 0.76 | 5.32 | 11.1 | 3.02x | +2.9 GB |
| 10 | 0.69 | 6.90 | 10.8 | 2.95x | +4.5 GB |
| 15 | 0.58 | 8.70 | 11.5 | 2.71x | +7.2 GB |
結論:num_speculative_tokens=5〜7が最適区間である。7を超えるとaccept ratioの低下がthroughputの利点を相殺し、GPUメモリ消費のみ増加する。
再現可能なベンチマーク実行方法
全体ベンチマークスクリプト
#!/bin/bash
# run_benchmark.sh - 全体ベンチマーク実行スクリプト
set -euo pipefail
MODEL="meta-llama/Llama-3.1-70B-Instruct"
DRAFT_MODELS=(
"none" # baseline
"meta-llama/Llama-3.1-8B-Instruct" # vanilla SD
"eagle3-llama3.1-70b-instruct" # EAGLE-3
)
SPEC_TOKENS=(0 5 5)
METHODS=("baseline" "vanilla" "eagle3")
RESULTS_DIR="benchmark_results/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$RESULTS_DIR"
for i in "${!METHODS[@]}"; do
method="${METHODS[$i]}"
draft="${DRAFT_MODELS[$i]}"
n_tokens="${SPEC_TOKENS[$i]}"
echo "=== Running benchmark: $method ==="
# サーバー起動
if [ "$method" = "baseline" ]; then
python -m vllm.entrypoints.openai.api_server \
--model "$MODEL" \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.92 \
--port 8000 &
elif [ "$method" = "eagle3" ]; then
python -m vllm.entrypoints.openai.api_server \
--model "$MODEL" \
--speculative-model "$draft" \
--speculative-method eagle \
--num-speculative-tokens "$n_tokens" \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.92 \
--port 8000 &
else
python -m vllm.entrypoints.openai.api_server \
--model "$MODEL" \
--speculative-model "$draft" \
--num-speculative-tokens "$n_tokens" \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.92 \
--port 8000 &
fi
SERVER_PID=$!
sleep 60 # モデルロード待機
# ベンチマーク実行
python benchmark.py \
--prompts eval_prompts.json \
--model "$MODEL" \
--warmup 10 \
--runs 3 \
--output "$RESULTS_DIR/${method}.json"
# サーバー終了
kill $SERVER_PID
wait $SERVER_PID 2>/dev/null || true
sleep 10
done
# 結果集計
python aggregate_results.py --input-dir "$RESULTS_DIR" --output "$RESULTS_DIR/summary.json"
echo "Results saved to $RESULTS_DIR/summary.json"
結果集計と比較
# aggregate_results.py
import json
import sys
from pathlib import Path
def aggregate_results(results_dir: str) -> dict:
"""ベンチマーク結果ファイルを読み込み比較テーブルを生成"""
results_path = Path(results_dir)
summary = {}
for result_file in sorted(results_path.glob("*.json")):
if result_file.name == "summary.json":
continue
method = result_file.stem
data = json.load(open(result_file))
# P50、P95計算
tpot_values = [r["tpot_ms"] for r in data]
e2e_values = [r["e2e_ms"] for r in data]
summary[method] = {
"tpot_p50": round(sorted(tpot_values)[len(tpot_values) // 2], 2),
"tpot_p95": round(sorted(tpot_values)[int(len(tpot_values) * 0.95)], 2),
"e2e_p50": round(sorted(e2e_values)[len(e2e_values) // 2], 2),
"e2e_p95": round(sorted(e2e_values)[int(len(e2e_values) * 0.95)], 2),
"total_requests": len(data),
}
# Speedup計算(baseline比)
if "baseline" in summary:
baseline_e2e = summary["baseline"]["e2e_p50"]
for method in summary:
summary[method]["speedup"] = round(
baseline_e2e / summary[method]["e2e_p50"], 2
)
return summary
if __name__ == "__main__":
results_dir = sys.argv[1] if len(sys.argv) > 1 else "benchmark_results/latest"
summary = aggregate_results(results_dir)
print(json.dumps(summary, indent=2))
プロダクション適用推奨事項
ベンチマーク結果に基づく実務適用ガイドラインである。
技法選択意思決定ツリー
Q: Targetモデルを頻繁に入れ替えるか?
├─ Yes: Vanilla SD(再学習不要)
│ またはPrompt Lookup(学習不要)
└─ No: Q: GPUメモリに余裕があるか?
├─ Yes (10GB以上の余裕): EAGLE-3(最高性能)
└─ No: Q: 軽量学習インフラがあるか?
├─ Yes: Medusa-2(メモリ効率的)
└─ No: Vanilla SD(Llama 3.1 8Bをdraftとして使用)
SLO別推奨設定
| SLO | 推奨技法 | num_speculative_tokens | 備考 |
|---|---|---|---|
| TPOT P95 20ms未満 | EAGLE-3 | 5 | Accept ratio 0.8以上を期待 |
| TPOT P95 35ms未満 | Vanilla SDまたはMedusa | 5 | 低い運用複雑度 |
| スループット最大化 | EAGLE-3、concurrency 8以下 | 7 | Concurrency制限必須 |
| コスト最小化 | EAGLE-3 | 5 | 58%コスト削減 |
ベンチマークの限界
このベンチマーク結果を解釈する際、以下の限界を認識すべきである。
- ハードウェア依存性:A100 SXM基準の結果であり、A10GやL4ではspeedup比率が異なる。特にNVLinkのないPCIe接続ではTP通信オーバーヘッドが大きくなりspeedupが減少する。
- プロンプトセットの偏り:韓国語/英語混合プロンプトを使用しており、純粋なコード生成や数学的推論の比率が高いサービスではaccept ratioが異なりうる。
- vLLMバージョン依存:vLLMのspeculative decoding実装は急速に改善されており、バージョンによって数値が変わりうる。
- KV cacheの影響:
max_model_lenを4096に固定しており、8192以上に増やすとKV cacheメモリ制約によりconcurrency処理能力が変化する。 - 量子化未適用:今回のベンチマークはbf16 precision基準である。GPTQ/AWQ量子化モデルにspeculative decodingを適用した結果は別途ベンチマークが必要である。
クイズ
Q1. このベンチマークで最も高いE2E speedupを記録した技法とその数値は?
正解:EAGLE-3が2.89xで最も高いspeedupを記録した。Structured output(JSON/YAML)カテゴリでは3.21xまで到達した。
Q2. Concurrency 64でspeculative decodingのspeedupが0.95xである理由は?
正解:高いconcurrencyではすでにcompute-bound状態となり、speculationのmemory-bound緩和の利点が消失する。draftモデルの追加演算とKV cacheメモリ競合がむしろ性能を低下させるためである。
Q3. num_speculative_tokensを15に上げるとspeedupがむしろ減少する理由は?
正解:推測トークン数が増えると後方positionのaccept ratioが急激に低下する。15個中、実際に受け入れられるのは平均8.7個で、残り6.3個のdraft演算は無駄になる。このオーバーヘッドが追加受入トークンの利点を相殺する。
Q4. Prompt Lookup Decodingのaccept ratioが0.41と低い理由は?
正解:Prompt Lookupは入力テキストのN-gramを再利用するため、入力と出力の語彙的重複が少ないタスク(翻訳、創作など)ではマッチング確率が低い。文書要約のように入力単語をそのまま使用するタスクでのみ効果的である。
Q5. EAGLE-3の追加GPUメモリがMedusa-2の2倍以上である理由は?
正解:Medusaは数個のlinear layerで構成された軽量headである一方、EAGLE-3はtargetモデルのfeatureを処理するtransformer layerを含みパラメータ数が多い。ただし、この複雑な構造のおかげでaccept ratioが0.81とMedusa(0.68)より高い。
Q6. 翻訳タスクでaccept ratioが0.85と高い理由は?
正解:翻訳はソース文の構造と語彙に強く条件付けされるため、draftモデルが次のトークンを予測しやすい。特に韓英翻訳では高頻度表現の対応パターンが明確であり、予測精度が高くなる。
Q7. このベンチマークを自社環境で再現する際、最初に変更すべき設定は?
正解:プロンプトセットの構成である。自社サービスの実際のトラフィック分布(リクエストタイプ、入出力長、temperature分布)を反映したプロンプトセットを作成しないと意味のあるベンチマークにならない。汎用プロンプトセットの結果は自社環境と大きく異なりうる。
Q8. コスト削減率58%を達成するための前提条件は?
正解:EAGLE-3 draft headの学習完了、concurrency 8以下での運用、temperature 0に近い設定、GPUメモリの余裕(+2GB以上)が前提条件である。実際のプロダクションではトラフィック変動によりconcurrencyが随時変化するため、動的なspeculative decodingのon/offルーティングが必須である。
参考文献
- 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)
- EAGLE-3: Scaling up Inference Acceleration (arXiv:2503.01840)
- vLLM Speculative Decoding Documentation
- Speculators v0.3.0 - Training Support for vLLM
- vLLM Releases - GitHub