Skip to content
Published on

LLMサービング:Speculative Decodingプロダクションベンチマーク 2026

Authors
  • Name
    Twitter
LLMサービング:Speculative Decodingプロダクションベンチマーク 2026

ベンチマークの目的と範囲

本文書は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 Instruct70BTP=4主力ベンチマークモデル
Qwen 2.5 72B Instruct72BTP=4クロス検証用
Mistral Large 2 (123B)123BTP=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 RatioTPOT P50 (ms)TPOT P95 (ms)E2E Speedup追加GPUメモリ
Baseline (no spec)--42.358.71.00x0 GB
Vanilla SDLlama 3.1 8B0.6219.831.21.95x~16 GB
Medusa-25 heads0.6817.127.82.21x~0.8 GB
EAGLE-1EAGLE head0.7314.924.12.52x~1.5 GB
EAGLE-3EAGLE-3 head0.8112.319.62.89x~1.8 GB
Prompt LookupN-gram (n=3)0.4128.545.31.38x0 GB

カテゴリ別詳細結果(EAGLE-3基準)

プロンプトタイプによる性能差が大きいため、カテゴリ別の結果を個別に確認する必要がある。

カテゴリAccept RatioE2E SpeedupTPOT P50 (ms)特記事項
short_qa0.842.95x11.8短い出力だが予測精度が高い
summarization0.833.12x11.5最大のspeedup
code_generation0.722.31x15.2構文予測は高いがロジック部で低下
creative_writing0.762.67x13.4意外にも高いaccept ratio
translation0.853.05x11.9翻訳は入力への依存度が高く予測が容易
structured_output0.883.21x10.9JSON/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)

ConcurrencyThroughput (tokens/s)E2E SpeedupAccept Ratio備考
181.32.89x0.81最適条件
2156.22.71x0.80ほぼ維持
4289.52.43x0.79わずかに低下
8498.71.98x0.78低下開始
16721.31.52x0.77顕著な低下
32890.41.18x0.76効果微小
64965.10.95x0.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対比コスト削減
Baseline3,200$0.00408-
Vanilla SD5,440$0.0024041%削減
EAGLE-37,680$0.0017058%削減
Medusa-26,400$0.0020450%削減

注意:上記の数値は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_tokensAccept RatioMean Accepted LengthTPOT P50 (ms)E2E SpeedupGPUメモリ増加
10.910.9135.21.20x+0.3 GB
30.862.5816.82.52x+0.8 GB
50.814.0512.32.89x+1.8 GB
70.765.3211.13.02x+2.9 GB
100.696.9010.82.95x+4.5 GB
150.588.7011.52.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-35Accept ratio 0.8以上を期待
TPOT P95 35ms未満Vanilla SDまたはMedusa5低い運用複雑度
スループット最大化EAGLE-3、concurrency 8以下7Concurrency制限必須
コスト最小化EAGLE-3558%コスト削減

ベンチマークの限界

このベンチマーク結果を解釈する際、以下の限界を認識すべきである。

  1. ハードウェア依存性:A100 SXM基準の結果であり、A10GやL4ではspeedup比率が異なる。特にNVLinkのないPCIe接続ではTP通信オーバーヘッドが大きくなりspeedupが減少する。
  2. プロンプトセットの偏り:韓国語/英語混合プロンプトを使用しており、純粋なコード生成や数学的推論の比率が高いサービスではaccept ratioが異なりうる。
  3. vLLMバージョン依存:vLLMのspeculative decoding実装は急速に改善されており、バージョンによって数値が変わりうる。
  4. KV cacheの影響max_model_lenを4096に固定しており、8192以上に増やすとKV cacheメモリ制約によりconcurrency処理能力が変化する。
  5. 量子化未適用:今回のベンチマークは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ルーティングが必須である。

参考文献