Головна / Статті

Спекулятивне декодування, ранній вихід та квантування: швидше декодування LLM.

Як спекулятивне декодування та ранній вихід зменшують затримку інференсу LLM, коли попередня затвердження документів приносить користь, та як вони поєднуються з квантуванням та організацією кешу KV.

Видавець: REACTAPP.TOP8 хв читання1877 слів

Джерела: **Foundations of LLM Inference Optimization: Speculative Decoding and Early Exit | Part 3D**

Обробка інформації за допомогою LLM відбувається у двох фазах. Фаза попереднього заповнення обробляє весь запит паралельно та може утримувати GPU у роботі. Фаза декодування виводить по одному токену за раз, причому кожен токен залежить від попереднього, тому апаратне забезпечення переважно перебуває у стані очікування. Саме ця послідовна залежність є причиною високої затримки в інтерактивному режимі, навіть коли кількість операцій типу FLOPs на папері здається достатньою.

Prefill phase:
  Input: [token_1, token_2, ..., token_512]  → all 512 tokens processed in parallel
  Matrix shape: [batch, 512, 4096]
  GPU utilization: high — large matrix, full Tensor Core throughput

Decode phase (one step):
  Input: [token_513]  → one token processed
  Matrix shape: [batch, 1, 4096]
  GPU utilization: low - tiny matrix, most CUDA cores idle

Чому декодування знаходиться на критичному шляху

На кожному кроці декодування модель виконує запити до постійно зростаючої кеш-пам’яті типу KV. Обсяг роботи на кожному кроці зростає з довжиною контексту, і у випадку чату неможливо вільно об’єднувати запити в групи, оскільки показники задовільності щодо затримок обмежують час, протягом якого запит може чекати на обробку. Арифметичні розрахунки показують, наскільки мало ресурсів чіпа фактично використовується на один крок:

One attention head, decode step:

Query Q: [8, 1, 128]   →  8 × 128 = 1,024 elements
Keys  K: [8, 2048, 128] →  8 × 2048 × 128 = 2M elements
Values V: [8, 2048, 128] →  same

Matrix multiply (QKᵀ) per head:
  Shape: [8, 1, 128] × [8, 128, 2048] → [8, 1, 2048]
  FLOPs: 8 × 1 × 128 × 2048 ≈ 2.1M FLOPs per head

For 32 heads: ≈ 67M FLOPs per layer
For 32 layers: ≈ 2.1B FLOPs total per decode step

A100 peak at BF16: ~312 TFLOPS = 312 × 10¹² FLOPs/sec

Time to execute 2.1B FLOPs at 100% utilization:
  2.1 × 10⁹ / 312 × 10¹² ≈ 0.0067 ms

Actual observed decode latency per step: ~10–30ms

Effective compute utilization: < 0.1%

Ефективне використання менше 0,1% означає, що процес декодування обмежується обсягом пам’яті та кількістю кроків, а не потужністю обчислень. Існує два способи подолати цю проблему: отримувати більше прийнятих токенів за кожен дорогий крок або виконувати менше роботи за крок. Спекулятивне декодування використовує перший підхід, а передчасний вихід — другий.

Спекулятивне декодування: попередній проект, потім перевірка

Невелика модель-проект пропонує кілька майбутніх токенів. Цільова модель перевіряє їх усі одночасно під час одного паралельного проходження даних, зберігає найдовший спільний префікс та знову генерує дані при першій відмові. Кожен прийнятий токен-проект — це токен, за який ви не заплатили повну вартість цільового кроку.

Without speculative decoding:
  Generate 5 tokens: 5 × 30ms = 150ms

With speculative decoding (K=4):
  Draft 4 tokens: 4 × 1.5ms = 6ms
  1 target verification pass: ~35ms  (slightly longer than decode,
                                       processes K+1=5 positions)
  Expected accepted tokens per round:
    (1 - 0.80^5) / (1 - 0.80) ≈ 3.36 tokens
  Time per round: 6ms + 35ms = 41ms
  Time per token: 41ms / 3.36 ≈ 12.2ms
Speedup: 30ms → 12.2ms ≈ 2.5×

Прискорення повністю залежить від того, наскільки часто чернетка та цільовий варіант збігаються. Простий текст із низькою ентропією дозволяє використовувати довгі чернетки; несподіваний токен скорочує кількість ітерацій. Ось цей цикл у концептуальній формі (реальні реалізації також керують кешем KV у обох моделях):

import torch
import torch.nn.functional as F

def speculative_decode(target_model, draft_model, input_ids,
                       max_new_tokens, K=4, temperature=1.0):
    """
    Conceptual speculative decoding loop.
    Real implementations handle KV cache management across both models.
    """
    generated = input_ids.clone()
    while generated.shape[1] - input_ids.shape[1] < max_new_tokens:
        # --- Draft phase ---
        draft_tokens = []
        draft_probs = []
        draft_input = generated.clone()
        for _ in range(K):
            with torch.no_grad():
                draft_logits = draft_model(draft_input).logits[:, -1, :]
            q = F.softmax(draft_logits / temperature, dim=-1)
            token = torch.multinomial(q, num_samples=1)
            draft_tokens.append(token)
            draft_probs.append(q)
            draft_input = torch.cat([draft_input, token], dim=1)
        # --- Verify phase: one target forward pass over all K+1 positions ---
        verify_input = torch.cat([generated] + draft_tokens, dim=1)
        with torch.no_grad():
            target_logits = target_model(verify_input).logits
        # target_logits[:, -K-1:, :] covers all K draft positions + bonus
        # --- Accept/reject ---
        accepted = 0
        for i in range(K):
            p = F.softmax(target_logits[:, -(K+1)+i, :] / temperature, dim=-1)
            q = draft_probs[i]
            token = draft_tokens[i]
            # Acceptance probability
            accept_prob = torch.min(
                torch.ones_like(p.gather(1, token)),
                p.gather(1, token) / (q.gather(1, token) + 1e-9)
            )
            if torch.rand(1) < accept_prob:
                generated = torch.cat([generated, token], dim=1)
                accepted += 1
            else:
                # Sample corrected token and stop this round
                corrected_dist = F.relu(p - q)
                corrected_dist = corrected_dist / corrected_dist.sum(dim=-1, keepdim=True)
                corrected_token = torch.multinomial(corrected_dist, num_samples=1)
                generated = torch.cat([generated, corrected_token], dim=1)
                break
        else:
            # All K accepted - take bonus token
            bonus_logits = target_logits[:, -1, :]
            p_bonus = F.softmax(bonus_logits / temperature, dim=-1)
            bonus_token = torch.multinomial(p_bonus, num_samples=1)
            generated = torch.cat([generated, bonus_token], dim=1)
    return generated

Якщо можливо, оберіть чернетку з тієї ж сім’ї, наприклад, дистиляовану чи квантовану версію, та вимірюйте показник прийняття на власному трафіку, а не на прикладах з публічних блогів.

Коли чернетки відхиляються

Якщо розподіл даних чернетки відхиляється від цільового, показник прийняття різко падає, і ви оплачуєте витрати на чернетку майже даремно. Постійно стежте за показником прийняття та повертайтеся до звичайного декодування, якщо він знижується.

Швидка перевірка за допомогою округлих чисел: припустимо, що один крок цільової системи коштує 10 мс, а чернетка пропонує 5 токенів, з яких у середньому приймається близько 3 (60%). Витрати на кожен прийнятий токен знижуються порівняно з кроками з одним токеном, навіть після додаткових витрат чернетки, за умови, що показник прийняття залишається стабільним. Якщо показник прийняття знижується до приблизно одного токена, схема стає невигідною. Саме через таку чутливість панелі керування кращі за окремі приклади.

Раннє завершення: пропускати глибокі шари для простих токенів

Деякі архітектури дозволяють токену виходити з мережі на проміжному рівні, коли рівень впевненості є високим, що дозволяє заощадити ресурси на обробці розділових знаків, артиклів та інших передбачуваних токенів.

Layer distribution of exits:Exit at layers 1–8  (very easy tokens like punctuation, articles):  15%
Exit at layers 9–16 (medium tokens, common continuations):           35%
Exit at layers 17–24 (harder tokens, named entities, numbers):       30%
Exit at layers 25–32 (full computation required):                    20%Weighted average layers executed:
  0.15 × 6 + 0.35 × 12 + 0.30 × 20 + 0.20 × 32
  = 0.90 + 4.20 + 6.00 + 6.40
  = 17.5 layers averageSpeedup vs always running 32 layers:
  32 / 17.5 ≈ 1.83×

На відміну від спекулятивного декодування, ранній вихід за замовчуванням дійсно змінює результати, оскільки він пропускає певні шари. Цей підхід жертвує якістю заради швидкості та сильно залежить від точності калібрування рівня впевненості, тому необхідно враховувати різницю в точності та вимірювати її.

Використання квантації, пакетування та кешу KV

Спекулятивне декодування змінює процес формування пропозицій токенів шляхом додавання другої моделі; ранній вихід за замовчуванням змінює глибину обробки всередині однієї моделі. Вони стикаються з схожими проблемами, але по-різному, і обидва підходи іноді можна поєднувати з квантацією, безперервним пакетуванням та використанням кешу KV.

Combined stack example:

Target: 7B model, BF16, FlashAttention, PagedAttention
  → Model: ~14 GB, memory-efficient attention, paged KV cache

Draft: 70M model, INT4, FlashAttention
  → Model: ~35 MB, near-zero memory overhead

Speculative decode:
  → with K=4, α=0.80
  → ~2.5× token generation speedup on long outputs

Full stack speedup vs FP32 no-optimization baseline:
  - Quantization: 2–2.3× throughput
  - FlashAttention: 20–40% attention latency reduction
  - Speculative decoding: 2–2.5× decode speedup (on eligible requests)

Combined: 5–8× improvement in end-to-end tokens/second

Складання елементів допомагає у керуванні процесом. Довжина чернеток впливає на припущення континуального планувальника групування, а поганий планувальник розділяє групи, що знижує ефективність використання ресурсів. Політики видалення даних з кешу KV потребують налаштування, щоб довгі розмови не спричиняли проблем. Кешування запитів на етапі попереднього заповнення також має значення — інакше ви „виправляєте процес декодування“, але при цьому марнуєте ресурси попереднього заповнення. Крім того, поєднання раннього завершення обробки з агресивною квантацією може швидко призвести до галюцинацій. Кохерентний дизайн обробки даних кращий, ніж просте застосування окремого методу без врахування всіх аспектів.

Коли кожен метод є ефективним

Спекулятивне декодування допомагає, коли рівень збігу чернеток високий та цільові кроки значно впливають на затримку. Воно не ефективне, коли чернетки рідко збігаються або накладні витрати на їх створення перевищують отриману вигоду. Раннє завершення обробки корисне, коли багато токенів можна легко обробити та є достатньо бюджету на точність; воно не працює з складними токенами або коли рівень впевненості погано налаштований.

Конкретний приклад: модель обсягом 7 мільярдів параметрів, призначена для чату, з відповідями довжиною від 50 до 150 токенів. Попереднє заповнення системного запиту обсягом 2 тисячі токенів є обтяжливим, але відбувається рідко за кожен раунд, тоді як кроки декодування є основною причиною затримок, які помічають користувачі. Зменшення кількості кроків декодування за допомогою спекулятивного прийняття рішень або скорочення обсягу роботи за крок за рахунок раннього виходу — ось що насправді має значення.

Чи має це значення для малих моделей, які працюють безпосередньо на пристрої? Зазвичай менше, ніж для великих моделей. Чи може проект працювати на CPU? Іноді; потрібно це перевірити. Якщо говорити про порядок пріоритетів, спочатку потрібно виправити проблеми з пакетуванням та кешуванням, потім додати спекулятивне декодування, а нарешті — ранній вихід, якщо ваша технологічна стек підтримує це.

Впровадження у продуктивну інференцію LLM

  • Спочатку виміряйте базову затримку p95 та якість роботи перед будь-якими змінами.
  • Розмістіть проєктну модель та цільову модель на одному й тому ж вузлі. Використання проєктів на різних хостах створює затримки в мережі, які можуть скасувати отримані переваги, а дві моделі разом із кешем KV можуть спричинити вичерпання пам’яті пристрою, який раніше комфортно справлявся з однією моделлю.
  • Записуйте гістограми прийняття, середню довжину прийнятих результатів, показники оцінки якості, рівень використання GPU та затримку p95.
  • Стежте за балансом роботи CPU та GPU під час виконання проєктів на різних пристроях.
  • Перевіряйте якість у форматах A/B для фактологічних тестів. Перед глобальним впровадженням протестуйте фіксовані запити у тестах на фактичну перевірку, програмування та відхилення, порівнюючи показники однакових токенів при налаштуванні спекулятивного декодування для точного збігу розподілу. Для раннього оцінювання відстежуйте кількість перемог у оцінюваних завданнях та людські уподобання, якщо вони є.
  • Розмістіть кожну оптимізацію під флагом функції з можливістю вимкнення та повернення до стандартного декодування, а також пройдіть репетицію скасування змін.
  • Перегляньте все після змін у токенізаторі чи процесі налаштування.

Правильно реалізований спекулятивний декодер має відповідати цільовій розподілу, але перевіряйте це за допомогою спеціальних тестів, а не просто покладаючись на нього. Зазвичай проблеми виникають через використання непов’язаних версій коду, ігнорування впливу параметра температури на результати, або оголошення про успіх на основі показників продуктивності без перевірки якості, причому кожен з цих факторів можна виміряти.

Команди платформи повинні контролювати ці оптимізації на рівні сервісного шару, щоб кожна команда, яка розробляє додаток, не створювала власні версії коду. Необхідно координуватися з адміністраторами сервісу замість того, щоб змінювати параметри в коді додатку, відображати заголовки чи атрибути для відстеження того, чи використовувався спекулятивний декодування чи передчасний вихід, а також позначати витрати на обробку токенів за оптимізаціями, щоб відділ фінансів міг бачити результати.

Залишений вузький місце

Коли процес декодування стане швидшим, користувачі все одно можуть чекати на виклики інструментів, отримання даних чи обробку Markdown на стороні клієнта. Залишковою обмежувальною фактором може бути пропускна здатність пам’яті, мережа чи сам клієнт, а не кількість операцій типу FLOPs, тому необхідно простежити весь процес від початку до кінця та провести аналіз перед тим, як застосовувати всі методи з наукових публікацій. Спекулятивне декодування також не замінює ефективного пошуку даних чи якісних запитів; воно лише зменшує витрати на обробку наступних токенів, коли модель вже знає, що хоче сказати.

Послідовне декодування є структурним обтяженням авторегресивного генерування. Спекулятивне декодування та ранній вихід з процесу зменшують це обтяження за умов, які можна виміряти, тому їх слід впроваджувати як будь-яку функцію для продакшну: з використанням метрик, флагів, можливостей скасування змін та чіткого відповідального особи в команді платформи обслуговування.

Пов’язана література

  • LLM Inference End to End: Prefill, Decode, KV Cache, and Sampling: Детальний огляд процесу обробки даних за допомогою токенайзера у потоковому режимі, пояснення того, чому процедура prefill обмежена обчислювальними ресурсами, а decode — пропускною здатністю, а також як технології GQA, пейджування та спекуляції впливають на ефективність роботи.
  • KV cache is the unseen database eating GPU memory in LLM decode: Порівняння процедур prefill та decode, математичні аспекти KV-кешу, технології GQA/MQA, PagedAttention, квантування та механізми видалення даних для ефективної обробки в продакшні.