LLM Speculative Decoding Optimierung in der Praxis: Von Draft-Modellen bis zur Medusa Multi-Head Beschleunigung

AI与大数据

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 75 % 2,3×
Qwen2.5-72B Qwen2.5-7B 5,5× 78 % 2,5×
DeepSeek-V3 DeepSeek-V2-Lite 72 % 2,2×
Mistral-Large Mistral-7B 76 % 2,4×
Gemma-2-27B Gemma-2-2B 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
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

  1. Speculative Decoding ist das Gratis-Essen der LLM-Inferenzbeschleunigung: 2–3× Speedup bei null Genauigkeitsverlust
  2. Draft-Modell: Ausschlaggebend bei der Auswahl ist Geschwindigkeit ≥ 5× + Akzeptanzrate ≥ 70 %, Distillations-Feintuning kann die Akzeptanzrate um 20–30 % verbessern
  3. Medusa Multi-Head: Kein zusätzliches Modell nötig, 5 % VRAM-Mehrbedarf für 2× Speedup, einfachste Bereitstellung
  4. 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

Probiere diese browser-lokalen Tools aus — keine Registrierung erforderlich →

#推测解码#LLM推理加速#Speculative Decoding#Medusa头#大模型推理优化#2026