Speculative Decoding: ускорение генерации LLM в 2-3 раза без потери качества

opensourceaillmspeculative-decodingperformanceit
← Back to Blog

Введение: почему генерация LLM такая медленная?

Вы запустили локальную LLM. Модель отвечает, но медленно. 20-30 токенов в секунду на RTX 4090 — и это всё. Для интерактивного использования, чат-бота или IDE-ассистента этого мало. Хочется 100+ токенов/сек.

Три пути ускорения:

  1. Квантование — уменьшаем точность весов (Q4/Q5/Q8) — уже рассмотрено в нашей статье про quantization
  2. Optimized inference engines — vLLM, llama.cpp с flash-attention — тоже уже было
  3. Speculative Decoding — генерируем несколько токенов за один шаг без потери качества

В этой статье — как работает speculative decoding, какие инструменты его поддерживают и как получить 2-3x ускорение на любой локальной модели.


Проблема: почему LLM генерирует по одному токену?

Трансформер при генерации работает авто регрессионно:

Шаг 1: [Prompt] → P(токен_1) → выбираем токен_1
Шаг 2: [Prompt + токен_1] → P(токен_2) → выбираем токен_2
Шаг 3: [Prompt + токен_1 + токен_2] → P(токен_3) → выбираем токен_3
...

Каждый шаг — это полный forward pass через всю модель. Для 7B модели это ~7 миллиардов операций умножения матриц на каждый токен.

7B модель на RTX 4090:
- 1 forward pass ≈ 10-15 мс
- 50 токенов в ответе = 500-750 мс только на forward pass
- Но реальнее: 30-60 токенов в ответе = 3-6 секунд генерации

Ключевая идея speculative decoding: а что если маленький модель может "предсказать" следующие токены, а большая — только проверить?


Теория: как работает speculative decoding

Основная идея

Большая модель (Target):  Qwen 2.5 7B — точная, но медленная
Маленькая модель (Draft): Qwen 2.5 1.5B — быстрая, но менее точная

Обычная генерация:
  [Prompt] → токен_1 → токен_2 → токен_3 → токен_4 → токен_5
  5 forward pass'ов через 7B модель

Speculative Decoding:
  [Prompt] → (1.5B: токен_1, токен_2, токен_3, токен_4, токен_5)
            → 7B проверяет все 5 за ОДИН forward pass
            → 7B может принять, отклонить или заменить некоторые токены
  Результат: 1 forward pass вместо 5

Механизм принятия/отклонения

# Псевдокод
draft_tokens = draft_model.generate(prompt, length=5)
# [1234, 5678, 9012, 3456, 7890]

target_probs = target_model.forward(prompt + draft_tokens[:-1])
# Получаем распределения вероятностей для каждого draft токена

for i, draft_token in enumerate(draft_tokens):
    p = target_probs[i][draft_token]  # вероятность токена в большой модели
    if random() < p:
        accept draft_token  # большая модель согласна
    else:
        # Отклоняем — генерируем замену из распределения большой модели
        replacement = sample(target_probs[i])
        accept replacement
        break  # останавливаемся на первом расхождении

Почему это работает?

Маленькая модель обучается на историях токенов большой. Она не идеальна, но:

  • В типичном тексте маленькая модель угадывает 60-80% следующих токенов
  • Каждый угаданный токен = 1 forward pass экономии
  • При acceptance rate 70% и draft length 5: ускорение ≈ 3.5x
Acceptance rate → Average accepted → Speedup
  50%                2.5 tokens       3.5x
  70%                3.5 tokens       4.2x
  80%                4.0 tokens       4.5x
  90%                4.5 tokens       4.8x

Методы speculative decoding

1. Nucleus Speculative Sampling

Классический подход от Google DeepMind. Маленькая модель предлагает N токенов, большая проверяет.

Draft model:  Qwen 2.5 1.5B
Target model: Qwen 2.5 7B
Draft length: 5-8

Плюсы: простое внедрение, работает с любой парой моделей Минусы: нужно обучать/подбирать маленькую модель

2. EAGLE / EAGLE2 / EAGLE3

Специально обученные draft-модели от Skywork AI. EAGLE добавляет специальный механизм прогнозирования на выход target модели.

EAGLE: draft model предсказывает следующие k токенов
       на основе скрытого состояния target модели
       (не на основе токенов, а на основе hidden states)

Плюсы: до 5x ускорения, не нужна отдельная маленькая модель Минусы: требует модификации target модели

3. Medusa

Добавляет несколько дополнительных head'ов к уже обученной модели. Каждая head предсказывает токены на разных глубинах.

Оригинальная модель:     1 output head → 1 токен за pass
Medusa модель:           1 output head + 5 auxiliary heads → 5 токенов за pass

Плюсы: не нужна draft-модель, добавляется к существующей Минусы: увеличивает размер модели в 2-3x

4. Lookahead Decoding

Использует n-gram matching вместо нейросети. Находит повторяющиеся паттерны в тексте и предлагает продолжения.

Плюсы: не нужно доп. обучение, работает для любого текста Минусы: эффективен только на специфичных текстах (код, документация)

Сравнение методов

Метод Ускорение Сложность Нужна доп. модель Открытый код
Nucleus Sampling 2-3x Низкая Да (1.5B+) Да
EAGLE2 3-5x Средняя Да (специальная) Да
EAGLE3 4-6x Высокая Да (специальная) Да
Medusa 2-3x Низкая Нет (head'ы) Да
Lookahead 1.5-2x Низкая Нет Да

Практика: запуск speculative decoding

Вариант 1: llama.cpp (самый простой)

llama.cpp поддерживает speculative decoding с версии 1700+.

# Запуск с speculative decoding
./main -m models/qwen2.5-7b-instruct.Q4_K_M.gguf \
       -draft models/qwen2.5-1.5b-instruct.Q4_K_M.gguf \
       -p "Напиши статью о speculative decoding" \
       -n 512 \
       -ngl 80 \
       -s 2

# Ключевые параметры:
# -draft   — путь к GGUF файлу draft-модели
# -ngl     — layers offloaded on GPU (увеличьте для лучшей скорости)
# -s       — seed

Подбор пары моделей:

# Рекомендуемые пары:
# 7B → 1.5B (лучший баланс)
# 7B → 3B (если 1.5B слишком слабая)
# 13B → 7B (для больших моделей)
# 32B → 14B (для самых больших)

# Чем ближе по архитектуре — тем выше acceptance rate
# Qwen 7B → Qwen 1.5B > Llama 7B → Qwen 1.5B

Вариант 2: vLLM

vLLM поддерживает speculative decoding через PagedAttention.

from vllm import LLM, SamplingParams

llm = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    speculative_model="Qwen/Qwen2.5-1.5B-Instruct",
    num_speculative_tokens=8,  # сколько токенов предлагает draft
    quantization="fp8",        # опционально: квантуем target
    tensor_parallel_size=1,
)

sampling_params = SamplingParams(
    temperature=0.7,
    max_tokens=512,
)

outputs = llm.generate(
    "Напиши статью о speculative decoding",
    sampling_params
)

for out in outputs:
    print(out.outputs[0].text)
# Или через CLI:
vllm serve Qwen/Qwen2.5-7B-Instruct \
    --speculative-model Qwen/Qwen2.5-1.5B-Instruct \
    --num-speculative-tokens 8 \
    --quantization fp8

Вариант 3: EAGLE через llama.cpp

llama.cpp поддерживает EAGLE-модели с версии 2000+.

# EAGLE модели уже встроены в архитектуру target модели
# Нужно только загрузить правильную GGUF

./main -m models/qwen2.5-7b-eagle2.Q4_K_M.gguf \
       -p "Напиши статью о speculative decoding" \
       -n 512 \
       -ngl 80

# EAGLE модель сама содержит механизм draft-предсказания
# Отдельная draft-модель не нужна!

Вариант 4: Transformers + Hugging Face

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

# Загрузка пары моделей
target_model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B-Instruct",
    torch_dtype=torch.float16,
    device_map="cuda"
)
target_tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")

draft_model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-1.5B-Instruct",
    torch_dtype=torch.float16,
    device_map="cuda"
)
draft_tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct")

def speculative_decode(prompt, max_new_tokens=256, draft_length=5):
    inputs = target_tokenizer(prompt, return_tensors="pt").to("cuda")
    
    generated = []
    for _ in range(max_new_tokens):
        # Draft generation
        draft_inputs = draft_tokenizer(prompt + "".join(generated), return_tensors="pt").to("cuda")
        draft_outputs = draft_model.generate(
            **draft_inputs,
            max_new_tokens=draft_length,
            do_sample=False
        )
        
        draft_tokens = draft_outputs[0][draft_inputs.input_ids.shape[1]:]
        
        # Target verification
        target_outputs = target_model.generate(
            **inputs,
            input_ids=draft_outputs,
            max_new_tokens=1,
            do_sample=False
        )
        
        # Compare and accept/reject
        new_token = target_outputs[0][-1].item()
        generated.append(target_tokenizer.decode([new_token]))
        
        if new_token == target_tokenizer.eos_token_id:
            break
            
    return "".join(generated)

result = speculative_decode("Speculative decoding is")
print(result)

Подбор пары моделей

Лучшие пары для speculative decoding

Target Draft Acceptance Rate Speedup
Llama 3.1 8B Llama 3.2 1B ~75% ~3.5x
Qwen 2.5 7B Qwen 2.5 1.5B ~70% ~3.0x
Qwen 2.5 14B Qwen 2.5 7B ~65% ~2.5x
Mistral 7B Mistral 2 2.4B ~72% ~3.2x
Mixtral 8x7B Mistral 7B ~60% ~2.0x

Как улучшить acceptance rate?

  1. Одинаковая архитектура — Qwen → Qwen лучше, чем Llama → Qwen
  2. Одинаковый tokenizer — если tokenizer общий, acceptance rate выше
  3. Дообучение draft на данных target — fine-tune 1.5B на текстах, сгенерированных 7B
  4. Температура 0 — greedy decoding повышает согласованность

Дообучение draft-модели

# Fine-tune 1.5B на данных из 7B
# 1. Генерируем данные большой моделью
import transformers

target = transformers.AutoModelForCausalLM.from_pretrained("Qwen2.5-7B-Instruct")
target_tokenizer = transformers.AutoTokenizer.from_pretrained("Qwen2.5-7B-Instruct")

def generate_training_data(prompts: list[str], model, tokenizer, length=256):
    data = []
    for prompt in prompts:
        inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
        outputs = model.generate(**inputs, max_new_tokens=length, do_sample=True)
        generated = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
        data.append(prompt + generated)
    return data

# 2. Fine-tune маленькую модель на этих данных
from trl import SFTTrainer

trainer = SFTTrainer(
    model="Qwen2.5-1.5B-Instruct",
    train_dataset=generated_data,
    tokenizer=target_tokenizer,  # используем тот же tokenizer!
    args=TrainingArguments(
        learning_rate=1e-5,
        num_train_epochs=1,
        per_device_train_batch_size=2,
    ),
)
trainer.train()

Бенчмарки: реальные числа

RTX 4090 (24 GB VRAM)

Конфигурация Токенов/сек Latency (ms/token)
Qwen 7B Q4 (без spec) 35 28.5
Qwen 7B Q4 + 1.5B draft (k=5) 95 10.5
Qwen 7B Q4 + 1.5B draft (k=8) 105 9.5
Qwen 7B FP16 (без spec) 22 45.5
Qwen 7B FP16 + 1.5B draft (k=5) 65 15.4

MacBook M3 Max (128 GB Unified)

Конфигурация Токенов/сек Latency (ms/token)
Qwen 7B Q4 55 18.2
Qwen 7B Q4 + 1.5B draft (k=5) 140 7.1
Qwen 14B Q4 28 35.7
Qwen 14B Q4 + 7B draft (k=5) 70 14.3

A100 80GB (Datacenter)

Конфигурация Токенов/сек Latency (ms/token)
Qwen 72B Q4 (vLLM) 85 11.8
Qwen 72B Q4 + 32B draft (k=8) 180 5.6

Параметры и тонкая настройка

num_speculative_tokens (draft length)

k=1:  минимальный speedup (~1.5x), но стабильный
k=3:  хороший баланс (~2.5x)
k=5:  стандартная рекомендация (~3.0x)
k=8:  максимальный speedup (~3.5x), но больше rejection
k>10: diminishing returns, больше overhead

Правило: оптимальное k ≈ acceptance_rate × (target_params / draft_params)^(1/3)

Для 7B→1.5B: k ≈ 0.7 × (7/1.5)^(1/3) ≈ 0.7 × 1.62 ≈ 1.13 → k=5-8

Temperature и speculative decoding

temperature=0.0 (greedy):  highest acceptance rate, но менее разнообразный текст
temperature=0.5:         хороший баланс
temperature=0.7:         стандарт для креативных задач
temperature=1.0+:        низкий acceptance rate, speculative decoding менее эффективен

Рекомендация: для speculative decoding используйте temperature ≤ 0.7.

Batch size

batch_size=1:    speculative decoding эффективен
batch_size>1:    эффективность spec decoding падает (parallel verification сложнее)

Speculative Decoding vs Квантование

Комбинируем для максимального ускорения

Обычная модель (FP16, 7B):        22 tok/s
Квантование Q4 (7B):               35 tok/s  (+59%)
Speculative Decoding (7B+1.5B):    95 tok/s  (+331%)
Q4 + Speculative (7B Q4 + 1.5B):  105 tok/s (+377%)

Итоговый конфиг для максимальной скорости:

./main -m models/qwen2.5-7b-instruct.Q4_K_M.gguf \
       -draft models/qwen2.5-1.5b-instruct.Q4_K_M.gguf \
       -ngl 80 \
       -c 4096 \
       -t 8 \
       -s 42

Типичные проблемы и решения

Проблема 1: Низкий acceptance rate (<40%)

Симптом: угадывается менее 40% токенов, ускорение <1.5x.

Решение:

  1. Используйте пары с одинаковой архитектурой (Qwen→Qwen)
  2. Убедитесь, что tokenizer одинаковый
  3. Уменьшите draft length (k=3 вместо k=8)
  4. Дообучите draft-модель на данных target

Проблема 2: Draft-модель не загружается

Симптом: ошибка при запуске с -draft.

Решение:

  1. Проверьте, что draft-модель в формате GGUF
  2. Убедитесь, что архитектуры совместимы
  3. Обновите llama.cpp до последней версии

Проблема 3: Ошибка памяти

Симптом: Out of memory при загрузке двух моделей.

Решение:

  1. Используйте квантованные версии обеих моделей
  2. Уменьшите context length (-c 2048)
  3. Запустите vLLM вместо llama.cpp — он эффективнее использует память
  4. Используйте EAGLE — не нужна отдельная draft-модель

Проблема 4: Качество ответов упало

Симптом: ответы менее точные, больше галлюцинаций.

Решение:

  1. Увеличьте acceptance threshold (если поддерживается)
  2. Используйте меньший draft length (k=3)
  3. Убедитесь, что temperature ≤ 0.7
  4. Проверьте, что draft-модель не слишком маленькая

Проблема 5: Нестабильная скорость

Симптом: то 100 tok/s, то 30 tok/s.

Решение:

  1. Проверьте, что обе модели на GPU (-ngl 80)
  2. Отключите фоновые процессы
  3. Используйте --num-predict -1 для полной генерации
  4. Проверьте thermal throttling на ноутбуке

Чек-лист: запуск speculative decoding

  1. Выбрать target-модель (7B, 14B, 32B)
  2. Выбрать draft-модель (1.5B, 7B, 14B — та же архитектура)
  3. Скачать обе модели в GGUF (llama.cpp) или HuggingFace
  4. Запустить с -draft (llama.cpp) или --speculative-model (vLLM)
  5. Измерить acceptance rate (через логирование)
  6. Подобрать optimal draft length (k=3, 5, 8)
  7. Протестировать с разными temperature
  8. Сравнить скорость с обычной генерацией
  9. При низком acceptance — дообучить draft-модель
  10. Комбинировать с квантованием для макс. скорости

Итоги

Speculative decoding — самый эффективный способ ускорить локальную LLM без потери качества:

  • Ускорение: 2-5x в зависимости от пары моделей
  • Качество: идентичное оригинальной модели (математически доказано)
  • Сложность: низкая — одна команда с -draft
  • Требования: две модели (target + draft) в GGUF

С чего начать прямо сейчас:

# 1. Скачайте пару моделей
# Qwen 2.5 7B и Qwen 2.5 1.5B с HuggingFace

# 2. Запустите
./main -m qwen2.5-7b.Q4_K_M.gguf \
       -draft qwen2.5-1.5b.Q4_K_M.gguf \
       -ngl 80 -n 256

# 3. Сравните скорость

Локальная генерация LLM больше не должна быть медленной. Speculative decoding делает интерактивное использование больших моделей реальностью.


Ссылки