Префилл и декодирование: как работает генерация
Нажали Enter — и генерация идёт в две фазы с совершенно разной физикой. Префилл обрабатывает весь ваш промпт одним параллельным проходом — много вычислений, быстро на токен, но вы ждёте всё это до первого слова. Декодирование затем выдаёт ответ по одному токену за раз — каждый шаг заново читает из памяти практически все веса модели, чтобы произвести один токен. «Первый токен медленный, потом стримит» — не причуда вашей установки; это и есть те две фазы.
Фаза 1: префилл — промпт
- Все токены промпта проходят через модель одновременно: большие параллельные матричные умножения — ровно то, для чего созданы GPU. Утилизация высокая; токенов в секунду много.
- Вы чувствуете это как время до первого токена (TTFT): тишину после нажатия Enter.
- Проход также заполняет KV-кэш ключами/значениями каждого токена промпта — черновиком, который будет читать декодирование.
- Длиннее промпт — линейно длиннее TTFT: большие системные промпты, документы RAG и история чата префиллятся каждый раз, если только движок не кэширует префиксы.
Фаза 2: декодирование — ответ
- Один токен на прямой проход. Этому проходу нужны все веса; выдача одного токена означает стриминг всей модели из VRAM.
- Потолок — пропускная способность памяти, а не вычисления: грубо decode tok/s ≈ эффективная пропускная способность ÷ байты весов. 8 GB весов на карте с ~400 GB/s эффективных ≈ потолок 50 tok/s.
- Вот почему квантизация ускоряет декодирование: меньше бит на вес — меньше байтов на стриминг каждый шаг (см. скорость).
Почему асимметрия: префилл обрабатывает тысячи токенов на одно чтение весов (огромные матрицы, высокая арифметическая интенсивность — ограничение по вычислениям), декодирование обрабатывает ровно один (каждый байт весов достаётся одному токену — ограничение по пропускной способности). Те же веса, противоположные узкие места.
total_time ≈ prompt_tokens / prefill_tok_s + output_tokens / decode_tok_s
Разбор на примере
prompt: 1,000 токенов префилл 2,000 tok/s → 0.5 с (TTFT)
output: 300 токенов декодирование 40 tok/s → 7.5 с
итого ≈ 8.0 с — 94% реального времени приходится на декодирование
- Первая полсекунды обработала 1,000 токенов; следующие 7.5 секунд производят 300. Та же модель, та же GPU, разница в 50× — другая фаза, а не другое старание.
- 40 tok/s — это примерно 160 английских символов в секунду (≈4 символа/токен): быстрее, чем кто-либо печатает, поэтому стриминг читается комфортно (токены).
- Те же 40 tok/s на корейском — примерно 40–60 символов/с: та же скорость токенов, меньше текста на токен.
- Длина вывода — ручка на стороне декодирования: max_tokens ограничивает длительность генерации, стоп-последовательности завершают её раньше. Обе меняют реальное время линейно (параметры движка).
- Стриминг меняет восприятие, а не физику: вы смотрите, как происходит декодирование, вместо ожидания — при тех же 40 tok/s.
Разбор на примере 2: случай RAG
prompt: 12,000 токенов (система + 20 найденных чанков) префилл 1,500 tok/s → 8.0 с
output: 500 токенов декодирование 40 tok/s → 12.5 с
итого ≈ 20.5 с — один только TTFT — 39% ожидания
Такова форма локальных сетапов, перегруженных ретривалом: жирные промпты означают, что модель «думает» секунды, прежде чем начать печатать. Лекарства, в порядке эффективности:
- Извлекайте меньше — топ 3–5 чанков обычно бьют топ 20 (RAG).
- Держите системный промпт стабильным, чтобы движок мог переиспользовать кэшированный префикс.
- Подрезайте историю: старые реплики можно суммаризовать, а не проигрывать целиком.
KV-кэш: растущая черновая память
Без кэша генерация токена N пересчитывала бы внимание по всем N−1 более ранним токенам с нуля — каждый шаг повторяет идентичную работу, полная стоимость квадратична по длине. Вместо этого:
- Префилл вычисляет ключи и значения всех токенов промпта и сохраняет их один раз.
- Каждый шаг декодирования вычисляет query/key/value только нового токена и дописывает его K/V в кэш.
- Внимание нового токена — его запрос, скалярно умноженный на все сохранённые ключи.
- Один декодированный токен, по порядку: эмбеддинг, прогон слоёв с чтением кэша, проекция на словарь, сэмплирование — дописать его K/V, повторить.
Теперь каждый шаг декодирования линеен по длине контекста (чтение кэша) вместо квадратичного (пересчёт всего). Цена — сам кэш, который растёт с каждым токеном:
KV bytes ≈ 2 × layers × kv_heads × head_dim × bytes_per_value × context
модель класса 8B (32 слоя, 8 KV-голов, размер головы 128, FP16):
2 × 32 × 8 × 128 × 2 B ≈ 128 KiB на токен
→ ~2.6 GB при 20k контекста, ~17 GB при 128k
Этот кэш конкурирует с весами за VRAM — чаты с длинным контекстом роняют машины, которые спокойно гоняют ту же модель с короткими промптами (KV-кэш, математика VRAM).
Батчинг меняет экономику
Читать 8 GB весов, чтобы выдать один токен, расточительно; то же чтение может обслуживать много последовательностей сразу. Соберите в батч 8 запросов — один стрим весов производит 8 токенов — и пропускная способность растёт почти линейно, пока не насытятся вычисления. В этом весь трюк обслуживания (API для обслуживания, параллелизм). В одиночку на ноутбуке вы платите полную цену за токен; серверы амортизируют её между одновременными пользователями.
Чтение метрик
| Метр | Фаза | Главный фактор |
|---|---|---|
| TTFT | префилл | длина промпта, вычисления, попадания кэша префиксов |
| Скорость обработки промпта (tok/s) | префилл | вычисления, размер батча |
| Скорость генерации (tok/s) | декодирование | пропускная способность памяти, байты весов, батч |
| Пропускная способность (req/s) | обе | размер батча, переиспользование кэша префиксов, параллельные слоты |
| Память контекста | обе | KV-байты на токен, квантизация кэша |
Мерьте, прежде чем крутить: каждый серьёзный движок пишет скорости prompt-eval и генерации на каждый запрос. Минута логов скажет, какая фаза съедает вашу задержку; угадывание — нет.
Настройка следует за фазой: более короткие промпты и кэширование префиксов чинят TTFT; квантизация и меньшие веса чинят декодирование (параметры движка, диагностика).
Связанные разделы: скорость · KV-кэш · параллелизм · токены