Speculative Decoding ist eine Technik, um die Inference-Speed von Language Models bis zu 2-3x zu erhöhen, ohne Qualität zu verlieren.
Kernidee
Statt einen Token nach dem anderen zu generieren, vermute mehrere Tokens mit einem schnellen Draft-Modell. Dann verifiziere alle mit dem großen Modell auf einmal.
Standard (Autoregressive):
Input: "Guten"
Step 1: → "Morgen"
Step 2: → "das"
Step 3: → "ist"
... (langsam, 1 Token pro Schritt)
Speculative Decoding:
Input: "Guten"
Draft Phase: Schnelles Modell rät
→ [Morgen, das, ist, eine, Antwort] (5 Tokens auf einmal!)
Verify Phase: Großes Modell prüft
→ Sind alle Token korrekt?
→ Wenn ja: Alle akzeptiert, sparen 5 Schritte!
→ Wenn nein: Bis zum ersten Fehler akzeptieren, 1 neu generieren
Draft-then-Verify Prozess
Schritt 1: Draft-Phase
Ein schnelles Modell generiert mehrere Kandidaten.
def draft_phase(input_ids, draft_model, num_tokens=5):
"""
Schnelles Modell rät mehrere Tokens
"""
draft_tokens = []
for i in range(num_tokens):
# Schnelles Modell: Kann auf CPU sein, kleineres Modell
logits = draft_model(input_ids + draft_tokens)
# Greedy oder Sampling
token = logits.argmax(dim=-1)[-1]
draft_tokens.append(token)
return draft_tokens # [token1, token2, token3, token4, token5]
Schritt 2: Verify-Phase
Das große Modell überprüft die gesamte Sequenz parallel.
def verify_phase(input_ids, draft_tokens, verifier_model):
"""
Großes Modell überprüft alle Draft Tokens auf einmal
Parallel-Vorteil!
"""
# Erweiterte Sequenz mit Draft Tokens
extended_ids = input_ids + draft_tokens
# (1, original_len + draft_len)
# Großes Modell: Parallel über alle Positionen!
logits = verifier_model(extended_ids)
# Output: (1, extended_len, vocab_size)
# Prüfe jeden Draft Token
verified = []
for i, draft_token in enumerate(draft_tokens):
pos = len(input_ids) + i
# Was wäre die Vorhersage des großen Modells an dieser Position?
verifier_distribution = logits[0, pos - 1] # <- Position!
# Ist der Draft Token wahrscheinlich unter der Verifier-Distribution?
draft_prob = verifier_distribution[draft_token]
if draft_prob > acceptance_threshold:
verified.append(draft_token)
else:
# Erster Fehler: Hier mit Verifier Sampling stoppen
true_token = sample_from(verifier_distribution)
verified.append(true_token)
break
return verified
Acceptance Criteria
Nicht alle Draft Tokens werden akzeptiert. Es gibt mehrere Strategien.
1. Greedy Acceptance
Akzeptiere Draft Token nur wenn er die beste Vorhersage ist.
def greedy_acceptance(draft_token, verifier_logits):
"""
Verifier sagt: Welcher Token ist beste Wahl?
"""
best_token = verifier_logits.argmax()
if draft_token == best_token:
return True
else:
return False
Problem: Sehr restriktiv, viele Tokens werden rejected.
2. Probabilistic Acceptance
Akzeptiere Draft Token mit bestimmter Wahrscheinlichkeit.
def probabilistic_acceptance(draft_token, draft_logits, verifier_logits, alpha=0.9):
"""
Acceptiere wenn Draft Prob >= alpha * Verifier Prob
"""
draft_prob = softmax(draft_logits)[draft_token]
verifier_prob = softmax(verifier_logits)[draft_token]
# Akzeptiere wenn Draft nahe bei Verifier Wahrscheinlichkeit
if draft_prob >= alpha * verifier_prob:
return True
else:
# Rejection Sampling: Sample aus Verifier, nicht Draft!
return False
Vorteile:
- Balance zwischen Geschwindigkeit und Qualität
- Alpha=0.9: 90% der Draft Prob ist ausreichend
3. Position-Based Acceptance
Unterschiedliche Threshold pro Position (früh: streng, spät: locker).
def position_based_acceptance(draft_token, verifier_logits, position, total_draft=5):
"""
Erste Tokens: Streng (theta=0.95)
Letzte Tokens: Locker (theta=0.7)
"""
theta = 0.95 - (position / total_draft) * 0.25
# Position 0: 0.95
# Position 4: 0.70
verifier_prob = softmax(verifier_logits)[draft_token]
if verifier_prob >= theta:
return True
else:
return False
Praktische Implementation
vLLM Speculative Decoding
from vllm import LLM
# Großes Modell (Verifier)
model = LLM(
model="meta-llama/Llama-2-70b",
tensor_parallel_size=4
)
# Schnelles Draft Modell
draft_model = LLM(
model="meta-llama/Llama-2-7b",
tensor_parallel_size=1 # Kann auch auf CPU sein
)
# Speculative Decoding aktivieren
outputs = model.generate(
prompt="Erzähle einen Witz:",
speculative_model=draft_model,
num_speculative_tokens=5, # Wie viele Tokens der Draft rät
temperature=0.8
)
llama.cpp Implementation
# Großes Modell (Verifier)
./main -m models/llama-70b.gguf \
-p "Erzähle einen Witz:" \
--speculative-model models/llama-7b.gguf \
--spec-tokens 5 \
--temp 0.8
# Speedup Faktor wird angezeigt
# Beispiel: 2.5x schneller
Custom Implementation in PyTorch
class SpeculativeDecoding:
def __init__(self, draft_model, verifier_model, num_speculative=5):
self.draft = draft_model
self.verifier = verifier_model
self.num_spec = num_speculative
def generate(self, input_ids, max_new_tokens=100):
tokens = input_ids.clone()
for _ in range(max_new_tokens):
# Draft Phase
with torch.no_grad():
# Schnelles Modell: Mehrere Tokens raten
draft_tokens = []
temp_ids = tokens.clone()
for _ in range(self.num_spec):
logits = self.draft(temp_ids)[-1, :]
token = logits.argmax()
draft_tokens.append(token.item())
temp_ids = torch.cat([temp_ids, token.unsqueeze(0)])
# Verify Phase
extended_ids = torch.cat([tokens, torch.tensor(draft_tokens)])
verifier_logits = self.verifier(extended_ids)
# Check each draft token
accepted = []
for i, draft_token in enumerate(draft_tokens):
pos = len(tokens) + i - 1
vlogits = verifier_logits[pos]
# Probabilistic acceptance
draft_prob = torch.softmax(logits, dim=-1)[draft_token]
verifier_prob = torch.softmax(vlogits, dim=-1)[draft_token]
if draft_prob >= 0.9 * verifier_prob:
accepted.append(draft_token)
else:
# Rejection: Verifier sample statt Draft
true_token = torch.multinomial(
torch.softmax(vlogits, dim=-1), 1
).item()
accepted.append(true_token)
break
# Append accepted tokens
tokens = torch.cat([
tokens,
torch.tensor(accepted).unsqueeze(1)
])
if len(tokens) >= max_new_tokens:
break
return tokens
Speedup Analyse
Theoretischer Speedup
Without Speculative: N Tokens × T Durchlaufzeit = N*T
With Speculative:
- Draft Phase: D Durchlaufzeit (schnelles Modell)
- Verify Phase: T Durchlaufzeit (großes Modell)
- Per Iteration: D + T (generiert ~num_spec Tokens)
Speedup ≈ num_spec / (1 + D/T)
Beispiel:
Draft (7B auf CPU): D = 100ms
Verifier (70B auf GPU): T = 200ms
num_spec = 5
Standard: 5 * 200ms = 1000ms (5 Tokens)
Speculative: (100 + 200) * 5/avg_accepted = 300 * (5/4) = 375ms
Speedup: 1000 / 375 = 2.67x
Praktische Messungen
Llama 2 70B + Llama 2 7B Draft:
Without Speculative:
Throughput: 25 Tokens/Sec
Latency (100 tokens): 4 seconds
With Speculative (5 tokens):
Throughput: 65 Tokens/Sec (2.6x!)
Latency (100 tokens): 1.5 seconds
Schlüssel: Draft ist sehr schnell (7B), Verifier ist wertvoll (70B)
Medusa und Eagle
Neuere Varianten von Speculative Decoding.
Medusa (Meta)
Statt separates Draft Modell: Zusätzliche Heads auf hauptem Modell.
Standard LLM: Input → Hidden States → Output Token
Mit Medusa:
Input → Hidden States → Main Head: Token
├─ Medusa Head 1: Token[t+1]
├─ Medusa Head 2: Token[t+2]
├─ Medusa Head 3: Token[t+3]
└─ Medusa Head 4: Token[t+4]
Vorteil: Kein separates Draft Modell nötig!
from medusa import MedusaModel
model = MedusaModel.from_pretrained("meta-llama/Llama-2-70b-medusa")
outputs = model.generate(
input_ids,
max_new_tokens=100,
medusa_num_heads=4 # 4 zusätzliche Heads
)
Eagle (Microsoft)
Autoregressive Heads die mehrere Tokens parallel generieren.
Standard: Token → Token → Token (sequenziell)
Eagle: Token → [Token, Token, Token] (parallel Tokens)
Praktische Performance:
- Llama-70B: 2.4x Speedup
- GPT-4 Qualität mit Llama-70B Speed
Lookahead Decoding
Neuer Ansatz: Deterministisches Speculative Decoding ohne separates Modell.
Idee: Manche Tokens sind "sicher vorherzusagen"
(z.B. nach "Python" kommt sehr wahrscheinlich ein Newline oder Code)
Lookahead Phase:
1. Generiere nächsten Token normal
2. Basierend auf diesem Token: Was sind Tokens t+2, t+3, ... die sehr wahrscheinlich?
3. Generiere alle diese auf einmal
Vorteil: Kein Draft Modell, deterministische Länge
Nachteil: Nicht alle Sequenzen haben "sichere" Tokens
Best Practices
1. Draft Modell Wahl
Groß für hohe Qualität:
70B Verifier + 13B Draft: 1.8x Speedup
Mittel (empfohlen):
70B Verifier + 7B Draft: 2.5x Speedup
Schnell:
70B Verifier + 1B Draft: 2.0x Speedup (Draft zu schnell, viele Rejections)
2. Number of Speculative Tokens
draft_tokens = 1: ~1.1x Speedup (zu wenig)
draft_tokens = 5: 2-3x Speedup (Goldstandard)
draft_tokens = 10: 2.5-3x Speedup (aber weniger Acceptances)
Regel: Starten mit 5, optimieren basierend auf Acceptance Rate
3. Acceptance Threshold
# alpha zu hoch (0.99): Zu viele Rejections
# alpha = 0.9: Balance (empfohlen)
# alpha zu niedrig (0.7): Zu viele Acceptances, kann Qualität leiden
# Messen: Acceptance Rate
acceptance_rate = num_accepted / (num_draft * num_iterations)
# Ziel: 70-80%
4. Memory Overhead
Speculative Decoding braucht 2 Modelle im Memory:
72GB für 70B (verifier) + 14GB für 7B (draft) = 86GB
Optimierungen:
- Draft auf CPU quantisiert
- Draft auf separater GPU
- vLLM managed sharing automatisch
Vergleich mit Alternativen
| Methode | Speedup | Speicher | Komplexität |
|---|---|---|---|
| Standard | 1x | 1x | - |
| Flash Attention | 1.5x | 1x | Niedrig |
| vLLM Paging | 1.2x | 0.8x | Niedrig |
| Speculative | 2.5-3x | 1.5x | Mittel |
| Quantization (INT8) | 2x | 0.5x | Mittel |
| Quantization + Spec. | 4-5x | 0.75x | Hoch |
Häufige Fehler
1. Draft Modell zu klein
❌ 70B + 0.5B: Draft wird rejected zu oft
✅ 70B + 7B: Guter Balance
2. Zu viele speculative tokens
❌ 10+ tokens: Viele Rejections, nicht schneller
✅ 5 tokens: Optimal
3. Falscher Acceptance Threshold
❌ alpha=1.0: Alles wird rejected
✅ alpha=0.9: Balance
❌ alpha=0.5: Zu viele falsche Tokens akzeptiert
