Speculative Decoding: ускорение генерации LLM в 2-3 раза без потери качества
Введение: почему генерация LLM такая медленная?
Вы запустили локальную LLM. Модель отвечает, но медленно. 20-30 токенов в секунду на RTX 4090 — и это всё. Для интерактивного использования, чат-бота или IDE-ассистента этого мало. Хочется 100+ токенов/сек.
Три пути ускорения:
- Квантование — уменьшаем точность весов (Q4/Q5/Q8) — уже рассмотрено в нашей статье про quantization
- Optimized inference engines — vLLM, llama.cpp с flash-attention — тоже уже было
- 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?
- Одинаковая архитектура — Qwen → Qwen лучше, чем Llama → Qwen
- Одинаковый tokenizer — если tokenizer общий, acceptance rate выше
- Дообучение draft на данных target — fine-tune 1.5B на текстах, сгенерированных 7B
- Температура 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.
Решение:
- Используйте пары с одинаковой архитектурой (Qwen→Qwen)
- Убедитесь, что tokenizer одинаковый
- Уменьшите draft length (k=3 вместо k=8)
- Дообучите draft-модель на данных target
Проблема 2: Draft-модель не загружается
Симптом: ошибка при запуске с -draft.
Решение:
- Проверьте, что draft-модель в формате GGUF
- Убедитесь, что архитектуры совместимы
- Обновите llama.cpp до последней версии
Проблема 3: Ошибка памяти
Симптом: Out of memory при загрузке двух моделей.
Решение:
- Используйте квантованные версии обеих моделей
- Уменьшите context length (
-c 2048) - Запустите vLLM вместо llama.cpp — он эффективнее использует память
- Используйте EAGLE — не нужна отдельная draft-модель
Проблема 4: Качество ответов упало
Симптом: ответы менее точные, больше галлюцинаций.
Решение:
- Увеличьте acceptance threshold (если поддерживается)
- Используйте меньший draft length (k=3)
- Убедитесь, что temperature ≤ 0.7
- Проверьте, что draft-модель не слишком маленькая
Проблема 5: Нестабильная скорость
Симптом: то 100 tok/s, то 30 tok/s.
Решение:
- Проверьте, что обе модели на GPU (
-ngl 80) - Отключите фоновые процессы
- Используйте
--num-predict -1для полной генерации - Проверьте thermal throttling на ноутбуке
Чек-лист: запуск speculative decoding
- Выбрать target-модель (7B, 14B, 32B)
- Выбрать draft-модель (1.5B, 7B, 14B — та же архитектура)
- Скачать обе модели в GGUF (llama.cpp) или HuggingFace
- Запустить с
-draft(llama.cpp) или--speculative-model(vLLM) - Измерить acceptance rate (через логирование)
- Подобрать optimal draft length (k=3, 5, 8)
- Протестировать с разными temperature
- Сравнить скорость с обычной генерацией
- При низком acceptance — дообучить draft-модель
- Комбинировать с квантованием для макс. скорости
Итоги
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 делает интерактивное использование больших моделей реальностью.