Zusammenfassung
- Speculative Decoding ist die geheime Waffe zur Beschleunigung der LLM-Inferenz: 2–3× Speedup bei null Genauigkeitsverlust, mittlerweile ein Standardfeature in Inferenzdiensten (Stand 2026)
- 3 wesentliche Spekulationsstrategien: Draft-Modell, Medusa Multi-Head und Tree Speculation, jeweils mit optimalen Anwendungsfällen
- Schlüssel der Draft-Modellauswahl: Die Draft-Modellgeschwindigkeit muss ≥ 5× der Zielmodellgeschwindigkeit betragen, Akzeptanzrate ≥ 70 %, um einen Speedup von 2×+ zu erreichen
- Medusa Multi-Head benötigt kein zusätzliches Modell: Vorhersageköpfe werden dem Zielmodell hinzugefügt, mit geringen Trainingskosten und einfacher Bereitstellung
- Dieser Artikel bietet vLLM + Speculative-Decoding-Bereitstellungslösungen und das Training benutzerdefinierter Draft-Modelle in der Praxis
Inhaltsverzeichnis
Speculative Decoding: Die geheime Waffe zur Beschleunigung der LLM-Inferenz
Der Engpass der autoregressiven Dekodierung
Der Kernengpass der LLM-Inferenz ist die serielle Natur der autoregressiven Dekodierung: Für die Generierung jedes Tokens ist ein vollständiger Forward-Pass erforderlich, was zu einer extrem geringen GPU-Auslastung führt.
| Modellgröße |
Latenz pro Token |
GPU-Auslastung |
Durchsatz |
| 7B |
15ms |
25% |
40 tok/s |
| 14B |
28ms |
18% |
22 tok/s |
| 70B |
80ms |
12% |
8 tok/s |
| 405B |
250ms |
5% |
2.5 tok/s |
Grundidee von Speculative Decoding
┌──────────────────────────────────────────────────────────────┐
│ Speculative Decoding vs. Autoregressive Decoding │
│ │
│ Autoregressive Decoding (Token-für-Token-Generierung) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [BOS] → f() → t1 → f() → t2 → f() → t3 → f() → t4 │ │
│ │ 4 Forward-Passes, 4 Token │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Speculative Decoding (Draft + Verifikation) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Draft-Modell schnelle Generierung: t1' t2' t3' t4' (1 Forward-Pass) │ │
│ │ Zielmodell parallele Verifikation: [t1' t2' t3' t4'] → f() (1 Forward-Pass) │ │
│ │ Angenommen t1' t2' t3' korrekt, t4' falsch: │ │
│ │ Akzeptiere t1' t2' t3', lehne t4' ab, korrigiere zu t4 │ │
│ │ 2 Forward-Passes, 4 Token → 2× Speedup │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
2026 Vergleich der Speculative-Decoding-Ansätze
| Ansatz |
Speedup |
Genauigkeitsverlust |
Zusätzliches VRAM |
Bereitstellungskomplexität |
| Draft-Modell |
2–3× |
Null |
30–50 % |
Mittel |
| Medusa Multi-Head |
1,5–2,5× |
Null |
5–10 % |
Gering |
| Eagle Tree Speculation |
2,5–3,5× |
Null |
10–15 % |
Mittel |
| Self-Speculation |
1,5–2× |
Null |
0 % |
Gering |
| Speculative Sampling + Quantisierung |
2–3,5× |
Minimal |
15–25 % |
Mittel |
Grundlagen von Speculative Decoding und mathematische Fundamente
Akzeptanzrate und Speedup-Verhältnis
Das Speedup-Verhältnis von Speculative Decoding hängt von der Akzeptanzrate der Draft-Token ab:
Theoretischer Speedup = γ / (1 + (1-α) × γ)
Wobei:
- γ = Draft-Länge (Anzahl der Token pro Spekulation)
- α = Akzeptanzrate (Wahrscheinlichkeit, dass Draft-Token vom Zielmodell akzeptiert werden)
Beispiele:
- γ=5, α=0,8: Speedup = 5 / (1 + 0,2×5) = 5/2 = 2,5×
- γ=5, α=0,9: Speedup = 5 / (1 + 0,1×5) = 5/1,5 = 3,3×
- γ=5, α=0,6: Speedup = 5 / (1 + 0,4×5) = 5/3 = 1,67×
- γ=3, α=0,8: Speedup = 3 / (1 + 0,2×3) = 3/1,6 = 1,875×
Akzeptanzrate vs. Speedup-Verhältnis
| Akzeptanzrate α |
γ=3 |
γ=5 |
γ=7 |
γ=10 |
| 0,5 |
1,2× |
1,25× |
1,27× |
1,25× |
| 0,6 |
1,36× |
1,47× |
1,52× |
1,47× |
| 0,7 |
1,58× |
1,82× |
1,94× |
1,96× |
| 0,8 |
1,88× |
2,5× |
2,92× |
3,33× |
| 0,9 |
2,31× |
3,33× |
4,38× |
5,88× |
Implementierung des Verifikationsalgorithmus
import torch
import torch.nn.functional as F
def speculative_decode(
target_model,
draft_model,
input_ids,
draft_length=5,
temperature=1.0,
):
draft_tokens = []
draft_probs = []
current_ids = input_ids.clone()
for _ in range(draft_length):
with torch.no_grad():
outputs = draft_model(current_ids)
next_token_logits = outputs.logits[:, -1, :]
next_token_prob = F.softmax(next_token_logits / temperature, dim=-1)
next_token = torch.multinomial(next_token_prob, num_samples=1)
draft_tokens.append(next_token)
draft_probs.append(next_token_prob)
current_ids = torch.cat([current_ids, next_token], dim=-1)
draft_token_ids = torch.cat(draft_tokens, dim=-1)
with torch.no_grad():
target_outputs = target_model(current_ids)
target_logits = target_outputs.logits[:, -(draft_length + 1):, :]
target_probs = F.softmax(target_logits / temperature, dim=-1)
accepted_tokens = []
n_accepted = 0
for i in range(draft_length):
draft_token = draft_token_ids[:, i]
draft_prob = draft_probs[i].gather(1, draft_token.unsqueeze(-1))
target_prob = target_probs[:, i].gather(1, draft_token.unsqueeze(-1))
accept_prob = torch.min(
torch.ones_like(target_prob),
target_prob / (draft_prob + 1e-10),
)
if torch.rand(1, device=accept_prob.device) < accept_prob:
accepted_tokens.append(draft_token)
n_accepted += 1
else:
residual_prob = F.relu(target_probs[:, i] - draft_probs[i])
residual_prob = residual_prob / residual_prob.sum(dim=-1, keepdim=True)
corrected_token = torch.multinomial(residual_prob, num_samples=1)
accepted_tokens.append(corrected_token)
break
if n_accepted == draft_length:
bonus_prob = target_probs[:, -1]
bonus_token = torch.multinomial(bonus_prob, num_samples=1)
accepted_tokens.append(bonus_token)
result = torch.cat(accepted_tokens, dim=-1)
return result, n_accepted
Draft-Modellauswahl und Training
Grundsätze der Draft-Modellauswahl
| Grundsatz |
Anforderung |
Grund |
| Geschwindigkeit |
≥5× Zielmodell |
Die Draft-Generierung darf kein Engpass werden |
| Akzeptanzrate |
≥70 % |
Unter 70 % ist der Speedup geringer als 2× |
| Größe |
≤1/5 Zielmodell |
Kontrollierbarer VRAM-Mehrbedarf |
| Vokabular |
Identisch mit Zielmodell |
Vermeidung von Vokabular-Mapping-Verlusten |
Gängige Draft-Modell-Kombinationen
| Zielmodell |
Draft-Modell |
Geschwindigkeitsverhältnis |
Akzeptanzrate |
Speedup |
| LLaMA-3-70B |
LLaMA-3-8B |
6× |
75 % |
2,3× |
| Qwen2.5-72B |
Qwen2.5-7B |
5,5× |
78 % |
2,5× |
| DeepSeek-V3 |
DeepSeek-V2-Lite |
7× |
72 % |
2,2× |
| Mistral-Large |
Mistral-7B |
6× |
76 % |
2,4× |
| Gemma-2-27B |
Gemma-2-2B |
8× |
70 % |
2,1× |
Draft-Modell-Training
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer
from datasets import load_dataset
class DraftModelTrainer:
def __init__(self, target_model_path, draft_model_path, output_path):
self.target = AutoModelForCausalLM.from_pretrained(
target_model_path, torch_dtype=torch.float16, device_map="auto"
)
self.draft = AutoModelForCausalLM.from_pretrained(
draft_model_path, torch_dtype=torch.float16, device_map="auto"
)
self.tokenizer = AutoTokenizer.from_pretrained(target_model_path)
self.output_path = output_path
def generate_distillation_data(self, prompts, num_samples=50000):
distill_data = []
for prompt in prompts:
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.target.device)
with torch.no_grad():
outputs = self.target.generate(
**inputs,
max_new_tokens=256,
temperature=0.7,
do_sample=True,
num_return_sequences=5,
)
for output in outputs:
text = self.tokenizer.decode(output, skip_special_tokens=True)
distill_data.append({"text": text})
if len(distill_data) >= num_samples:
break
return distill_data
def train(self, distill_data, epochs=3, lr=5e-5):
from trl import SFTTrainer, SFTConfig
sft_config = SFTConfig(
output_dir=self.output_path,
num_train_epochs=epochs,
per_device_train_batch_size=4,
gradient_accumulation_steps=8,
learning_rate=lr,
warmup_ratio=0.03,
bf16=True,
logging_steps=100,
save_strategy="epoch",
max_seq_length=2048,
)
trainer = SFTTrainer(
model=self.draft,
train_dataset=distill_data,
args=sft_config,
)
trainer.train()
self.draft.save_pretrained(self.output_path)
return self.draft
Optimierung der Draft-Modell-Akzeptanzrate
| Optimierungsmethode |
Verbesserung der Akzeptanzrate |
Ansatz |
| Distillations-Feintuning |
+10–15 % |
Draft-Modell mit Zielmodell-Ausgaben trainieren |
| Vokabular-Ausrichtung |
+5–8 % |
Sicherstellen, dass Draft- und Zielvokabular übereinstimmen |
| Temperatur-Ausrichtung |
+3–5 % |
Anpassung der Sampling-Temperaturen zwischen Draft und Ziel |
| Kontext-Ausrichtung |
+5–10 % |
Dieselbe System-Prompt für das Draft-Modell verwenden |
| Kombinierte Optimierung |
+20–30 % |
Alles oben Genannte |
Medusa Multi-Head Speculation in der Praxis
Medusa-Architektur
┌──────────────────────────────────────────────────────────────┐
│ Medusa Multi-Head-Architektur │
│ │
│ Ursprüngliches LLM-Backbone │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Input → Transformer Layers → Hidden State │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ Ursprünglicher LM-Head (t+1 vorhersagen) │
│ ┌──────────┐ │
│ │ LM Head │ → P(t+1) │
│ └──────────┘ │
│ │
│ Medusa Head 0 (t+2 vorhersagen) │
│ ┌──────────────┐ │
│ │ Linear+Softmax│ → P(t+2) │
│ └──────────────┘ │
│ │
│ Medusa Head 1 (t+3 vorhersagen) │
│ ┌──────────────┐ │
│ │ Linear+Softmax│ → P(t+3) │
│ └──────────────┘ │
│ │
│ Medusa Head K-1 (t+K vorhersagen) │
│ ┌──────────────┐ │
│ │ Linear+Softmax│ → P(t+K) │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
Medusa-Implementierung
import torch
import torch.nn as nn
from transformers import PreTrainedModel
class MedusaHead(nn.Module):
def __init__(self, hidden_size, vocab_size):
super().__init__()
self.linear = nn.Linear(hidden_size, vocab_size, bias=False)
def forward(self, hidden_states):
return self.linear(hidden_states)
class MedusaModel(nn.Module):
def __init__(self, base_model, num_heads=4):
super().__init__()
self.base_model = base_model
self.num_heads = num_heads
hidden_size = base_model.config.hidden_size
vocab_size = base_model.config.vocab_size
self.medusa_heads = nn.ModuleList([
MedusaHead(hidden_size, vocab_size)
for _ in range(num_heads)
])
for head in self.medusa_heads:
nn.init.xavier_uniform_(head.linear.weight)
def forward(self, input_ids, attention_mask=None):
outputs = self.base_model.model(
input_ids=input_ids,
attention_mask=attention_mask,
use_cache=True,
)
hidden_states = outputs.last_hidden_state
base_logits = self.base_model.lm_head(hidden_states)
medusa_logits = []
for head in self.medusa_heads:
medusa_logits.append(head(hidden_states))
return base_logits, medusa_logits, outputs.past_key_values
class MedusaDecoding:
def __init__(self, model, tokenizer, num_heads=4, top_k=10):
self.model = model
self.tokenizer = tokenizer
self.num_heads = num_heads
self.top_k = top_k
def generate(self, input_ids, max_new_tokens=256):
generated = input_ids.clone()
for _ in range(max_new_tokens):
base_logits, medusa_logits, _ = self.model(generated)
base_prob = torch.softmax(base_logits[:, -1, :], dim=-1)
next_token = torch.argmax(base_prob, dim=-1, keepdim=True)
candidates = [next_token]
for head_idx in range(self.num_heads):
head_prob = torch.softmax(
medusa_logits[head_idx][:, -1, :], dim=-1
)
top_tokens = torch.topk(head_prob, self.top_k)
candidates.append(top_tokens.indices[:, :1])
candidate_ids = torch.cat(candidates, dim=-1)
with torch.no_grad():
verify_outputs = self.model.base_model(candidate_ids)
verify_logits = self.model.base_model.lm_head(
verify_outputs.last_hidden_state
)
accepted = [next_token]
for i, cand in enumerate(candidates[1:]):
verify_prob = torch.softmax(verify_logits[:, i, :], dim=-1)
if verify_prob.gather(1, cand).item() > 0.1:
accepted.append(cand)
else:
corrected = torch.argmax(verify_prob, dim=-1, keepdim=True)
accepted.append(corrected)
break
new_tokens = torch.cat(accepted, dim=-1)
generated = torch.cat([generated, new_tokens], dim=-1)
if new_tokens.shape[-1] > 1:
break
return generated
Medusa-Training
class MedusaTrainer:
def __init__(self, medusa_model, lr=1e-4):
self.model = medusa_model
self.optimizer = torch.optim.AdamW(
[p for p in self.model.medusa_heads.parameters()],
lr=lr,
weight_decay=0.01,
)
self.base_model = medusa_model.base_model
for param in self.base_model.parameters():
param.requires_grad = False
def train_step(self, input_ids, labels):
with torch.no_grad():
outputs = self.base_model.model(input_ids=input_ids)
hidden_states = outputs.last_hidden_state
total_loss = 0
for head_idx, head in enumerate(self.model.medusa_heads):
shift = head_idx + 1
head_logits = head(hidden_states[:, :-shift, :])
head_labels = labels[:, shift:]
loss = F.cross_entropy(
head_logits.reshape(-1, head_logits.size(-1)),
head_labels.reshape(-1),
)
total_loss += loss
total_loss /= len(self.model.medusa_heads)
total_loss.backward()
self.optimizer.step()
self.optimizer.zero_grad()
return total_loss.item()
| Medusa-Konfig |
Heads |
Top-K |
Speedup |
Zusätzliches VRAM |
| Medusa-2 |
2 |
5 |
1,5× |
3 % |
| Medusa-4 |
4 |
10 |
2,0× |
5 % |
| Medusa-4 |
4 |
20 |
2,2× |
5 % |
| Medusa-8 |
8 |
10 |
2,5× |
10 % |
Tree Speculation und der Eagle-Ansatz
Eagle Tree Speculation Grundlagen
┌──────────────────────────────────────────────────────────────┐
│ Eagle Tree Speculation │
│ │
│ Traditionelle lineare Spekulation: │
│ t1 → t2 → t3 → t4 → t5 (1 Pfad) │
│ │
│ Eagle Tree Speculation: │
│ t1 │
│ / \ │
│ t2a t2b │
│ / \ / \ │
│ t3a t3b t3c t3d │
│ │
│ Mehrere Kandidatenpfade werden parallel verifiziert, der Pfad mit der höchsten Akzeptanzrate wird ausgewählt │
│ Vorteil: Selbst wenn ein Pfad abgelehnt wird, können andere weiterhin akzeptiert werden │
└──────────────────────────────────────────────────────────────┘
Eagle-Implementierung
class EagleSpeculativeDecoder:
def __init__(self, target_model, draft_model, tree_width=4, tree_depth=3):
self.target = target_model
self.draft = draft_model
self.tree_width = tree_width
self.tree_depth = tree_depth
def build_speculation_tree(self, input_ids):
tree_nodes = [{"ids": input_ids.clone(), "parent": None}]
leaves = []
for depth in range(self.tree_depth):
new_nodes = []
for node in tree_nodes[-self.tree_width ** depth:]:
with torch.no_grad():
outputs = self.draft(node["ids"])
probs = torch.softmax(outputs.logits[:, -1, :], dim=-1)
top_k = torch.topk(probs, self.tree_width)
for i in range(self.tree_width):
child_ids = torch.cat([
node["ids"],
top_k.indices[:, i:i+1],
], dim=-1)
child = {
"ids": child_ids,
"parent": node,
"prob": top_k.values[:, i].item(),
"token": top_k.indices[:, i],
}
new_nodes.append(child)
if depth == self.tree_depth - 1:
leaves.append(child)
tree_nodes.extend(new_nodes)
return tree_nodes, leaves
def verify_tree(self, tree_nodes, leaves):
best_path = []
best_score = 0
for leaf in leaves:
path = []
node = leaf
while node["parent"] is not None:
path.append(node)
node = node["parent"]
path.reverse()
with torch.no_grad():
full_ids = leaf["ids"]
outputs = self.target(full_ids)
target_probs = torch.softmax(outputs.logits, dim=-1)
score = 1.0
accepted = 0
for i, step in enumerate(path):
target_prob = target_probs[:, -(len(path) - i), :].gather(
1, step["token"]
).item()
score *= target_prob
if target_prob > 0.3:
accepted += 1
else:
break
if score > best_score:
best_score = score
best_path = path[:accepted]
return best_path
Tree Speculation vs. Lineare Spekulation
| Ansatz |
Kandidatenpfade |
Akzeptanzrate |
Speedup |
Rechenmehrbedarf |
| Linear γ=5 |
1 |
70 % |
2,3× |
Gering |
| Tree w=2,d=3 |
8 |
82 % |
2,8× |
Mittel |
| Tree w=3,d=3 |
27 |
88 % |
3,2× |
Mittel-Hoch |
| Tree w=4,d=3 |
64 |
92 % |
3,5× |
Hoch |
| Eagle(w=4,d=4) |
256 |
95 % |
3,8× |
Hoch |
Produktionseinsatz: vLLM + Speculative Decoding
vLLM Speculative-Decoding-Konfiguration
from vllm import LLM, SamplingParams
llm = LLM(
model="Qwen/Qwen2.5-72B-Instruct",
speculative_model="Qwen/Qwen2.5-7B-Instruct",
num_speculative_tokens=5,
speculative_max_model_len=4096,
gpu_memory_utilization=0.9,
tensor_parallel_size=4,
trust_remote_code=True,
)
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=512,
)
outputs = llm.generate(
["Bitte erklären Sie die Grundprinzipien des Quantencomputings"],
sampling_params=sampling_params,
)
for output in outputs:
print(output.outputs[0].text)
vLLM Speculative-Decoding-Leistung
| Konfiguration |
Zielmodell |
Draft-Modell |
Durchsatz (tok/s) |
Speedup |
| Keine Spekulation |
72B |
Keins |
8 |
1× |
| Draft-Spekulation |
72B |
7B |
18 |
2,25× |
| Draft-Spekulation |
72B |
0,5B |
22 |
2,75× |
| Medusa-4 |
72B |
Keins (eingebaut) |
16 |
2,0× |
| Eagle |
72B |
7B |
25 |
3,1× |
Überlegungen zum Produktionseinsatz
| Überlegung |
Beschreibung |
Lösung |
| Draft-Modell-VRAM |
Zusätzliche 30–50 % Auslastung |
Draft-Modell auf INT8 quantisieren |
| Schwankende Akzeptanzrate |
Erhebliche Unterschiede je nach Aufgabe |
Draft-Länge dynamisch anpassen |
| KV-Cache-Verwaltung |
KV abgelehnter Token muss bereinigt werden |
vLLM übernimmt dies automatisch |
| Batch-Verarbeitungskompatibilität |
Konflikt zwischen Speculative Decoding und Continuous Batching |
von vLLM nun unterstützt |
| First-Token-Latenz |
Speculative Decoding erhöht die First-Token-Latenz |
Spekulation in Chat-Szenarien deaktivieren |
Dynamische Spekulationsstrategie
class DynamicSpeculativeConfig:
def __init__(self):
self.min_draft_length = 2
self.max_draft_length = 8
self.target_acceptance_rate = 0.75
self.current_draft_length = 5
self.history = []
def update(self, acceptance_rate):
self.history.append(acceptance_rate)
if len(self.history) > 100:
self.history = self.history[-100:]
avg_rate = sum(self.history) / len(self.history)
if avg_rate > 0.85:
self.current_draft_length = min(
self.current_draft_length + 1,
self.max_draft_length,
)
elif avg_rate < 0.65:
self.current_draft_length = max(
self.current_draft_length - 1,
self.min_draft_length,
)
return self.current_draft_length
Zusammenfassung und weiterführende Literatur
Wesentliche Erkenntnisse
- Speculative Decoding ist das Gratis-Essen der LLM-Inferenzbeschleunigung: 2–3× Speedup bei null Genauigkeitsverlust
- Draft-Modell: Ausschlaggebend bei der Auswahl ist Geschwindigkeit ≥ 5× + Akzeptanzrate ≥ 70 %, Distillations-Feintuning kann die Akzeptanzrate um 20–30 % verbessern
- Medusa Multi-Head: Kein zusätzliches Modell nötig, 5 % VRAM-Mehrbedarf für 2× Speedup, einfachste Bereitstellung
- Eagle Tree Speculation: Multipfad-Parallelverifikation, 3,5×+ Speedup, ideal für Szenarien mit hohem Durchsatz
Empfohlene Ansätze
| Szenario |
Empfohlener Ansatz |
Speedup |
| Schnelle Bereitstellung |
Medusa-4 |
2,0× |
| Allgemeine Inferenz |
Draft-Modell (7B→72B) |
2,5× |
| Hoher Durchsatz |
Eagle Tree Speculation |
3,5× |
| VRAM-beschränkt |
Medusa + quantisiertes Draft |
2,2× |
Müssen Sie das Format von Inferenzdaten konvertieren? Nutzen Sie unser JSON-zu-YAML-Tool und den Text-Diff-Vergleich, um Debug-Daten für Speculative Decoding schnell zu verarbeiten.
Weiterführende Literatur