ChatGPTやClaudeのような大規模言語モデル(LLM)を使っていると、「返答が遅い」と感じたことはないでしょうか。LLMは1トークンずつ順番に生成する自己回帰方式を採用しており、100トークンの出力には100回の前方伝播が必要です。モデルが大きくなるほど1回あたりの計算コストも増大し、ユーザー体験やサーバーコストに直結する深刻な問題となっています。
この問題に対して、2023年にGoogleとDeepMindが独立に提案したのがSpeculative Decoding(投機的デコーディング)です。CPUの投機的実行(speculative execution)からヒントを得た手法で、小さなドラフトモデルで複数トークンを先読みし、大きなターゲットモデルで一括検証することで、出力の品質を一切犠牲にせずに推論を2〜3倍高速化できます。
Speculative Decodingを理解することは、以下のような場面で直接役立ちます。
- LLMサービスのレイテンシ改善: チャットボットやコード補完ツールの応答速度を改善する際に、モデルを変えずに推論速度を上げられます
- 推論コストの削減: GPUの利用時間を短縮することで、サーバーコストを直接削減できます
- vLLMやTGIでの実用: 主要な推論フレームワークがSpeculative Decodingをサポートしており、設定一つで有効化できます
本記事の内容
- 自己回帰生成のボトルネックとメモリバウンドの問題
- Speculative Decodingの基本アイデアと全体の流れ
- 受理・棄却サンプリングの数理(出力分布が変わらないことの証明)
- 期待される高速化率の理論的分析
- Pythonによるシミュレーション実装
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
自己回帰生成のボトルネック
なぜLLM推論は遅いのか
LLMの推論が遅い原因を理解するために、まずGPUの性能特性を考えましょう。GPUには2種類のボトルネックがあります。
- 計算バウンド(compute-bound): 演算ユニットの処理速度が律速。行列積など、大量の演算を行う処理で発生します
- メモリバウンド(memory-bound): GPUメモリ(HBM)からのデータ読み出し速度が律速。データ量に対して演算量が少ない処理で発生します
学習時は、バッチサイズが大きく、一度にまとまった行列積を実行するため、計算バウンドになります。GPUの演算ユニットを効率的に使い切ることができます。
一方、推論の生成フェーズでは状況が全く異なります。トークンを1つずつ生成するため、各ステップで計算するのは「1トークン分のQuery」と「キャッシュ済みのKey/Value全体」のAttentionだけです。これはバッチサイズ1の行列-ベクトル積に相当し、演算量に対してモデルの重みを読み出すメモリアクセスが支配的になります。
具体的な数値で見てみましょう。LLaMA-70Bの場合、モデルパラメータは約140GBです(FP16)。NVIDIA A100のHBM帯域は2TB/sなので、全パラメータを1回読み出すだけで:
$$ \frac{140 \text{ GB}}{2000 \text{ GB/s}} = 70 \text{ ms} $$
1トークン生成に70ms、つまり毎秒わずか14トークンが理論上限です。これはGPUの演算能力を全く使い切れていない状態です。A100のFP16理論演算性能は312 TFLOPSですが、LLaMA-70Bの1トークン生成に必要な演算量は約140 GFLOPSなので、GPU演算能力の利用率(arithmetic intensity)は:
$$ \text{利用率} = \frac{140 \times 10^9}{312 \times 10^{12}} \approx 0.045\% $$
わずか0.05%以下しかGPUの計算能力を使えていません。これがメモリバウンドの本質です。
素朴な解決策とその限界
メモリバウンドの問題に対する素朴な解決策はバッチ処理です。複数のリクエストをまとめて処理すれば、モデルの重みを1回読み出す間に複数トークンの計算を行えるため、GPU利用率が上がります。
しかし、バッチ処理にはKVキャッシュのメモリ容量という制約があります。LLaMA-70Bで系列長2048の場合、1リクエストあたりのKVキャッシュは約5GBを消費し、GPUメモリの大部分を占めてしまいます。
また、バッチ処理はスループット(単位時間あたりの処理量)は改善しますが、個々のリクエストのレイテンシは改善しません。チャットアプリケーションのように、1人のユーザーへの応答を速くしたい場合には不十分です。
ここで発想を転換しましょう。メモリバウンドの本質は「1トークンの計算のために全パラメータを読み出す」ことの非効率さでした。では、1回のパラメータ読み出しで複数トークンを検証できれば、効率が上がるのではないでしょうか。これがSpeculative Decodingの基本アイデアです。
Speculative Decodingの基本アイデア
CPUの投機的実行との類似性
Speculative Decodingは、CPUアーキテクチャにおける投機的実行(speculative execution)から着想を得ています。CPUでは、分岐命令(if文)の結果が確定するまで待つと処理が停滞するため、「おそらくこちらの分岐に進むだろう」と予測して先に命令を実行します。予測が当たればそのまま結果を使い、外れれば巻き戻して正しい方を実行します。
LLMでもこれと同じことをします。小さなモデル(ドラフトモデル $M_q$)が「おそらくこのトークン列が生成されるだろう」と予測し、大きなモデル(ターゲットモデル $M_p$)がそれを検証します。
全体の流れ
Speculative Decodingの1イテレーションは、以下の3ステップで構成されます。
ステップ1: ドラフト生成
小さく高速なドラフトモデル $M_q$ を使って、$\gamma$ 個のトークン候補を自己回帰的に生成します。
$$
\tilde{x}_1 \sim q(\cdot \mid x_{ ドラフトモデルは小さい(例: 7Bパラメータ)ので、$\gamma$ 個の生成は高速に完了します。 ステップ2: 並列検証 ターゲットモデル $M_p$ に、元の文脈 $x_{ $$
p(\cdot \mid x_{ これは学習時のように $\gamma+1$ トークンを一括で処理する前方伝播なので、1トークンの生成とほぼ同じ時間で完了します。KVキャッシュを使えば、新たに追加された $\gamma$ トークン分の計算だけで済みます。 ステップ3: 受理・棄却判定 各候補トークン $\tilde{x}_i$ を、ターゲットモデルの確率分布 $p$ とドラフトモデルの確率分布 $q$ の比に基づいて受理または棄却します。受理確率は: $$
\alpha_i = \min\left(1, \frac{p(\tilde{x}_i \mid x_{ 最初に棄却されたトークン以降は全て破棄します。つまり、$\tilde{x}_1, \ldots, \tilde{x}_{k-1}$ が全て受理され、$\tilde{x}_k$ が棄却された場合: 全て受理された場合は、ボーナスとして位置 $\gamma+1$ のトークンも $p(\cdot \mid x_{ 通常のデコーディングでは、ターゲットモデルの前方伝播1回で1トークンしか得られません。Speculative Decodingでは、ドラフトモデルの $\gamma$ 回 + ターゲットモデルの1回の前方伝播で、最低1トークン、最大 $\gamma+1$ トークンが得られます。 ドラフトモデルの推論がターゲットモデルの推論より十分高速であれば、全体として大きな高速化が達成できます。例えば、ドラフトモデルがターゲットモデルの10倍速く、$\gamma=5$ で平均3トークンが受理される場合: $$
\text{高速化率} \approx \frac{3 + 1}{1 + 5 \times 0.1} = \frac{4}{1.5} \approx 2.67\text{倍}
$$ これが出力分布を一切変えずに達成できるのがSpeculative Decodingの最大の強みです。では次に、なぜ出力分布が変わらないのかを数学的に証明しましょう。 棄却が起きた場合、位置 $k$ のトークンはターゲット分布 $p$ でもドラフト分布 $q$ でもない修正分布からサンプリングされます。この修正分布 $p’$ は次のように定義されます。 $$
p'(x) = \frac{\max(0, \, p(x) – q(x))}{\sum_{x’} \max(0, \, p(x’) – q(x’))}
$$ この式の意味を直感的に理解しましょう。$p(x) > q(x)$ となるトークン $x$ は、ドラフトモデルが過小評価しているトークンです。ドラフトモデルはこれらのトークンを十分な頻度で提案しないため、棄却後の再サンプリングで補正する必要があります。逆に $p(x) \leq q(x)$ のトークンは、受理ステップですでに十分な頻度で採用されているため、修正分布には含まれません。 Speculative Decodingの核心的な定理は、「各位置で最終的に出力されるトークンの分布が、ターゲットモデル $M_p$ から直接サンプリングした場合と完全に一致する」ことです。 任意のトークン $x$ について、最終的にそのトークンが出力される確率を計算します。 トークン $x$ が出力されるのは、以下の2つの排他的なケースです。 ケース1: ドラフトモデルが $x$ を提案し、受理された場合 $$
P(\text{ケース1}) = q(x) \cdot \min\left(1, \frac{p(x)}{q(x)}\right)
$$ ケース2: ドラフトモデルが何か別のトークンを提案して棄却され、修正分布から $x$ がサンプリングされた場合 まず、棄却が起きる確率を求めます。何らかのトークン $x’$ が提案されて棄却される確率は: $$
P(\text{棄却}) = \sum_{x’} q(x’) \cdot \left(1 – \min\left(1, \frac{p(x’)}{q(x’)}\right)\right)
$$ ここで、$\min(1, p(x’)/q(x’))$ を場合分けして計算します。 $p(x’) \leq q(x’)$ の場合、$\min(1, p(x’)/q(x’)) = p(x’)/q(x’)$ なので: $$
q(x’) \cdot \left(1 – \frac{p(x’)}{q(x’)}\right) = q(x’) – p(x’)
$$ $p(x’) > q(x’)$ の場合、$\min(1, p(x’)/q(x’)) = 1$ なので: $$
q(x’) \cdot (1 – 1) = 0
$$ したがって棄却確率は: $$
P(\text{棄却}) = \sum_{x’: p(x’) \leq q(x’)} (q(x’) – p(x’))
$$ 確率の合計は1なので($\sum p(x’) = \sum q(x’) = 1$)、これは次のように書き換えられます。 $$
P(\text{棄却}) = \sum_{x’: p(x’) > q(x’)} (p(x’) – q(x’))
$$ この等式が成り立つ理由は、$\sum (q(x’) – p(x’))$ を全トークンにわたって足すと0になるため、正の部分と負の部分の絶対値の和が等しくなることから従います。 棄却後に修正分布から $x$ がサンプリングされる確率は: $$
P(\text{ケース2}) = P(\text{棄却}) \cdot p'(x) = P(\text{棄却}) \cdot \frac{\max(0, p(x) – q(x))}{\sum_{x’} \max(0, p(x’) – q(x’))}
$$ 分母の $\sum_{x’} \max(0, p(x’) – q(x’))$ は $P(\text{棄却})$ に等しいので: $$
P(\text{ケース2}) = \max(0, \, p(x) – q(x))
$$ 合計 2つのケースを合計します。$p(x) \leq q(x)$ の場合: $$
P(x) = q(x) \cdot \frac{p(x)}{q(x)} + 0 = p(x)
$$ $p(x) > q(x)$ の場合: $$
P(x) = q(x) \cdot 1 + (p(x) – q(x)) = p(x)
$$ どちらの場合も $P(x) = p(x)$ となり、最終的な出力分布はターゲットモデルの分布に完全に一致します。$\blacksquare$ この証明のポイントは、修正分布 $p’$ が「ドラフトモデルが取りこぼした確率密度」を正確に補償するように設計されていることです。これにより、Speculative Decodingは近似ではなく厳密に同じ分布からのサンプリングを実現しています。 では次に、この手法がどの程度の高速化を達成できるかを理論的に分析しましょう。 ドラフトモデルの各トークンが受理される確率(受理率)を $\alpha$ とします。$\gamma$ 個の候補のうち、最初に棄却されるまでの連続受理数 $K$ は幾何分布に従います。 $$
P(K = k) = \begin{cases} \alpha^k (1 – \alpha) & \text{if } k < \gamma \\ \alpha^\gamma & \text{if } k = \gamma \end{cases}
$$ $K = k$ の場合、$k$ 個の受理トークン + 1個のサンプリングトークン = $k+1$ 個のトークンが得られます($k = \gamma$ の場合もボーナストークンを含めて $\gamma+1$ 個)。 1イテレーションあたりの期待トークン数 $\mathbb{E}[N]$ は: $$
\mathbb{E}[N] = \sum_{k=0}^{\gamma-1} (k+1) \alpha^k (1-\alpha) + (\gamma+1) \alpha^\gamma
$$ この和を計算します。まず $S = \sum_{k=0}^{\gamma-1} (k+1) \alpha^k (1-\alpha)$ とおきます。 $(k+1)\alpha^k$ の和は、$\frac{d}{d\alpha}\left[\alpha^{k+1}\right] = (k+1)\alpha^k$ を利用して: $$
\sum_{k=0}^{\gamma-1} (k+1)\alpha^k = \frac{d}{d\alpha}\sum_{k=0}^{\gamma-1} \alpha^{k+1} = \frac{d}{d\alpha}\left[\frac{\alpha(\alpha^\gamma – 1)}{\alpha – 1}\right]
$$ 計算を簡略化するために、等比級数の公式を利用して最終結果を直接示します: $$
\mathbb{E}[N] = \frac{1 – \alpha^{\gamma+1}}{1 – \alpha}
$$ この式から重要な性質が読み取れます。 1イテレーションにかかる時間は「ドラフトモデルの $\gamma$ 回の推論 + ターゲットモデルの1回の推論」です。ターゲットモデルの1回の推論時間を $T_p$、ドラフトモデルの1回の推論時間を $T_q$ とすると: $$
\text{高速化率} = \frac{\mathbb{E}[N] \cdot T_p}{\gamma T_q + T_p} = \frac{T_p \cdot \frac{1 – \alpha^{\gamma+1}}{1 – \alpha}}{\gamma T_q + T_p}
$$ ターゲットモデルの推論時間に対するドラフトモデルの推論時間の比 $c = T_q / T_p$ を導入すると: $$
\text{高速化率} = \frac{1 – \alpha^{\gamma+1}}{(1 – \alpha)(\gamma c + 1)}
$$ この式から、高速化率を最大化する最適な $\gamma$ の存在がわかります。$\gamma$ を大きくすると分子は増加しますが(候補が多いほどトークンが得られる)、分母の $\gamma c$ も増加します(ドラフト生成のコストが増える)。最適な $\gamma$ は $\alpha$ と $c$ の関数です。 $c = 0.1$(ドラフトが10倍速い)の場合の具体的な数値を見てみましょう: 受理率が高いほど大きな高速化が得られます。実用上、ドラフトモデルとターゲットモデルの品質差が小さい(例:LLaMA-7BとLLaMA-70B)場合、多くのテキスト(特に一般的な英文や定型的なコード)で $\alpha = 0.7$ 以上が達成されます。 理論的な分析ができたので、次にPythonでSpeculative Decodingを実装してみましょう。 まず、ドラフトモデルとターゲットモデルの確率分布をシミュレーションで定義し、Speculative Decodingの受理・棄却メカニズムを実装します。実際のLLMの代わりに、語彙上の確率分布を直接指定します。 このシミュレーションでは、実際のニューラルネットワークの代わりにソフトマックス分布を直接使用しています。ドラフトモデルの分布 $q$ は、ターゲットモデルの分布 $p$ の温度を変えた近似として生成しています。temperature_ratioが1に近いほど2つの分布は似ており、受理率が高くなります。 平均獲得トークン数が理論最大の $\gamma + 1$ にどれだけ近づくかが、ドラフトモデルの品質を反映しています。 Speculative Decodingの出力分布がターゲット分布 $p$ と一致することを、大量のサンプルから経験的に検証します。 左のグラフでは、ターゲット分布(青)、ドラフト分布(オレンジ)、Speculative Decodingの経験分布(緑)の3つを比較しています。ドラフト分布はターゲット分布と明らかに異なりますが、Speculative Decodingの出力はターゲット分布にほぼ完全に一致しています。右のグラフでは差分を拡大していますが、これはサンプリングの統計的ノイズによるもので、サンプル数を増やせば0に収束します。 KLダイバージェンスがほぼ0であることからも、出力分布の一致が確認できます。 最後に、受理率を変化させたときの期待トークン数と理論的な高速化率を可視化します。 左のグラフから、受理率が高いほど1イテレーションで得られるトークン数が増加し、$\gamma$ が大きいほどその傾向が顕著であることがわかります。しかし右のグラフでは、$\gamma$ を大きくしすぎるとドラフトモデルの計算コストが蓄積し、受理率が低い領域では高速化率が頭打ちになることが見て取れます。 実用上は $\gamma = 5 \sim 8$ が多くのシナリオで良好なバランスを達成します。受理率が0.7以上であれば、2倍以上の高速化が期待できます。 Speculative Decodingの性能は、ドラフトモデルの品質と速度のバランスに大きく依存します。主な選択肢は以下の3つです。 1. 同一ファミリーの小さなモデル: LLaMA-70Bに対してLLaMA-7Bを使う方法です。語彙やトークナイザが共通なため、実装がシンプルです。ただし受理率は0.5〜0.7程度にとどまることが多いです。 2. ターゲットモデルの蒸留モデル: ターゲットモデルの出力を教師として小さなモデルを学習させます。受理率が高くなる(0.7〜0.9)反面、ドラフトモデルの追加学習が必要です。 3. Self-Speculative Decoding: ターゲットモデル自体の一部の層をスキップしてドラフトとして使う手法です。追加のモデルが不要な反面、高速化率は控えめです。LayerSkip(Meta, 2024)やMedusa(Cai et al., 2024)がこのアプローチに相当します。 主要な推論フレームワークでのSpeculative Decodingのサポート状況は以下の通りです。 Speculative Decodingは全ての状況で高速化を達成できるわけではありません。特に効果的な場面と、効果が限定的な場面があります。 効果的な場面:
– バッチサイズが小さい(1〜4程度)のリアルタイム推論
– コード生成など、予測しやすいパターンが多いタスク
– ターゲットモデルとドラフトモデルの速度差が大きい場合 効果が限定的な場面:
– バッチサイズが大きい場合(すでに計算バウンドに近い)
– 創造的なテキスト生成(受理率が低くなる)
– GPUメモリが限られ、ドラフトモデルの追加読み込みが困難な場合 本記事では、Speculative Decodingの仕組みを理論から実装まで解説しました。 Speculative Decodingは、vLLMやTGIなどの主要な推論フレームワークに実装されており、設定一つで有効化できる実用的な技術です。LLMの推論最適化は活発に研究が進んでおり、Speculative Decodingはその基礎となる重要な概念です。 次のステップとして、以下の記事も参考にしてください。
なぜ高速化できるのか
受理・棄却サンプリングの数理
修正分布の定義
出力分布がターゲット分布に一致する証明
期待される受理トークン数の分析
受理率 $\alpha$ と期待受理トークン数
高速化率の見積もり
受理率 $\alpha$
最適 $\gamma$
期待トークン数
高速化率
0.5
4
1.94
1.38x
0.7
6
3.15
1.97x
0.8
8
4.56
2.53x
0.9
12
8.41
3.82x
Pythonによるシミュレーション実装
ドラフトモデルとターゲットモデルのシミュレーション
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
def create_distributions(vocab_size, temperature_ratio=0.8):
"""ターゲットモデルとドラフトモデルの確率分布を生成する。
ドラフトモデルはターゲットモデルに近いが、temperatureが異なる分布とする。
"""
# ターゲットモデルのlogits(ランダム生成)
logits = np.random.randn(vocab_size) * 2.0
# ターゲット分布(ソフトマックス)
p = np.exp(logits) / np.sum(np.exp(logits))
# ドラフト分布(温度を変えた近似分布)
q = np.exp(logits * temperature_ratio) / np.sum(np.exp(logits * temperature_ratio))
return p, q
def speculative_decode_step(p, q, gamma):
"""Speculative Decodingの1イテレーションを実行する。
Parameters
----------
p : np.ndarray
ターゲットモデルの確率分布
q : np.ndarray
ドラフトモデルの確率分布
gamma : int
ドラフトトークン数
Returns
-------
n_accepted : int
受理されたトークン数(ボーナストークン含む)
tokens : list
生成されたトークン列
"""
tokens = []
n_accepted = 0
for i in range(gamma):
# ドラフトモデルからサンプリング
x_draft = np.random.choice(len(q), p=q)
# 受理確率の計算
acceptance_prob = min(1.0, p[x_draft] / q[x_draft])
# 受理・棄却判定
if np.random.random() < acceptance_prob:
# 受理
tokens.append(x_draft)
n_accepted += 1
else:
# 棄却: 修正分布からサンプリング
p_prime = np.maximum(0, p - q)
p_prime_sum = np.sum(p_prime)
if p_prime_sum > 0:
p_prime = p_prime / p_prime_sum
else:
p_prime = p # フォールバック
x_corrected = np.random.choice(len(p_prime), p=p_prime)
tokens.append(x_corrected)
n_accepted += 1 # 棄却後の修正サンプリングも1トークン
return n_accepted, tokens
# 全て受理: ボーナストークンをターゲット分布からサンプリング
x_bonus = np.random.choice(len(p), p=p)
tokens.append(x_bonus)
n_accepted += 1
return n_accepted, tokens
# 実験: 語彙サイズ1000で実行
vocab_size = 1000
p, q = create_distributions(vocab_size, temperature_ratio=0.8)
gamma = 5
# 1000イテレーション実行して統計を取る
n_iterations = 1000
tokens_per_iter = []
for _ in range(n_iterations):
n, _ = speculative_decode_step(p, q, gamma)
tokens_per_iter.append(n)
mean_tokens = np.mean(tokens_per_iter)
print(f"語彙サイズ: {vocab_size}")
print(f"ドラフトトークン数 γ: {gamma}")
print(f"平均獲得トークン数: {mean_tokens:.2f}")
print(f"理論最大: {gamma + 1}")
出力分布の検証
def verify_distribution(p, q, gamma, n_samples=100000):
"""出力分布がターゲット分布pに一致することを検証する。"""
# Speculative Decodingで最初のトークンの分布を収集
first_tokens = np.zeros(len(p))
for _ in range(n_samples):
_, tokens = speculative_decode_step(p, q, gamma)
first_tokens[tokens[0]] += 1
# 経験分布
empirical = first_tokens / n_samples
return empirical
# 検証(小さい語彙で可視化しやすくする)
vocab_small = 20
p_small, q_small = create_distributions(vocab_small, temperature_ratio=0.7)
empirical = verify_distribution(p_small, q_small, gamma=5, n_samples=200000)
# 可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左: 3つの分布を比較
x_pos = np.arange(vocab_small)
width = 0.25
axes[0].bar(x_pos - width, p_small, width, label='Target $p$', alpha=0.8, color='#00d4ff')
axes[0].bar(x_pos, q_small, width, label='Draft $q$', alpha=0.8, color='#ffa726')
axes[0].bar(x_pos + width, empirical, width, label='Speculative (empirical)',
alpha=0.8, color='#66bb6a')
axes[0].set_xlabel('Token ID')
axes[0].set_ylabel('Probability')
axes[0].set_title('Distribution Comparison')
axes[0].legend()
axes[0].set_xticks(x_pos[::2])
# 右: ターゲットとSpeculative Decodingの差
diff = empirical - p_small
axes[1].bar(x_pos, diff, color=['#ef5350' if d < 0 else '#66bb6a' for d in diff], alpha=0.8)
axes[1].axhline(y=0, color='white', linewidth=0.5)
axes[1].set_xlabel('Token ID')
axes[1].set_ylabel('Difference (Speculative - Target)')
axes[1].set_title(f'Distribution Error (max |diff| = {np.max(np.abs(diff)):.4f})')
axes[1].set_xticks(x_pos[::2])
plt.tight_layout()
plt.savefig('speculative_decoding_distribution.png', dpi=150, bbox_inches='tight')
plt.show()
print(f"最大絶対誤差: {np.max(np.abs(diff)):.5f}")
print(f"KLダイバージェンス (empirical || target): {np.sum(empirical * np.log(empirical / p_small + 1e-10)):.6f}")
受理率と高速化率の関係
def theoretical_expected_tokens(alpha, gamma):
"""理論的な期待トークン数を計算する。"""
return (1 - alpha**(gamma + 1)) / (1 - alpha + 1e-10)
def theoretical_speedup(alpha, gamma, cost_ratio):
"""理論的な高速化率を計算する。"""
expected_tokens = theoretical_expected_tokens(alpha, gamma)
return expected_tokens / (gamma * cost_ratio + 1)
# 受理率を変えてシミュレーション
alphas = np.linspace(0.1, 0.95, 20)
gammas = [3, 5, 8, 12]
cost_ratio = 0.1 # ドラフトモデルはターゲットの10%の計算コスト
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左: 期待トークン数
for g in gammas:
expected = [theoretical_expected_tokens(a, g) for a in alphas]
axes[0].plot(alphas, expected, 'o-', label=f'γ = {g}', markersize=4)
axes[0].set_xlabel('Acceptance Rate α')
axes[0].set_ylabel('Expected Tokens per Iteration')
axes[0].set_title('Expected Tokens vs Acceptance Rate')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 右: 高速化率
for g in gammas:
speedups = [theoretical_speedup(a, g, cost_ratio) for a in alphas]
axes[1].plot(alphas, speedups, 'o-', label=f'γ = {g}', markersize=4)
axes[1].axhline(y=1, color='gray', linestyle='--', alpha=0.5, label='No speedup')
axes[1].set_xlabel('Acceptance Rate α')
axes[1].set_ylabel('Speedup')
axes[1].set_title(f'Theoretical Speedup (cost ratio c = {cost_ratio})')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('speculative_decoding_speedup.png', dpi=150, bbox_inches='tight')
plt.show()
実用上の考慮事項
ドラフトモデルの選択
実際のフレームワークでの実装
フレームワーク
方式
設定例
vLLM
ドラフトモデル
--speculative-model meta-llama/Llama-3.2-1B
TGI
Medusa
--speculative-decoding 2
llama.cpp
ドラフトモデル
--model-draft small.gguf -ngld 99
TensorRT-LLM
ドラフトモデル/Medusa
設定ファイルで指定
Speculative Decodingが効果的な場面
まとめ