SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor

Prefill чи decode: чому перший токен повільний

Prefill визначає час до першого токена й упирається в обчислення, а decode визначає токени за секунду та пропускну здатність пам’яті. Вимірюйте їх окремо.

Prefill і decode: в одному абзаці

Prefill і decode — це розрізнення, яке пояснює більшість питань щодо затримки self-hosted LLM (велика мовна модель). Prefill зчитує весь промпт за один прохід і обмежується обчислювальною потужністю. Decode генерує відповідь по одному токену за раз і обмежується пропускною здатністю пам’яті. Час до отримання першого токена — це показник prefill. Кількість токенів за секунду — це показник decode.

Обидві фази виконуються на тому самому GPU (графічний процесор), використовують ті самі ваги й працюють у тому самому процесі, тому їх природно розглядати як одне навантаження. Фактично вони поводяться як дві різні програми, що спільно використовують один пристрій. Якщо розділити ці фази, довгий список незрозумілих результатів більше не здаватиметься суперечливим.

Чому prefill обмежений обчисленнями?

Prefill один раз передає весь prompt через кожен layer. Prompt на 2,000 токенів дає кожному matrix multiply 2,000 рядків роботи, тому GPU виконує багато арифметичних операцій для кожного завантаженого байта weight. Це співвідношення — кількість арифметичних операцій на переміщений байт — називається arithmetic intensity. У prefill воно високе. Пристрій працює майже на межі обчислювальної продуктивності, а memory bus має запас пропускної здатності.

Prefill створює два результати: KV cache (тензори key і value) для кожного токена prompt і перший output token. До завершення цього проходу читач не отримує нічого. Тому час prefill і time to first token (TTFT) мають майже однакове значення.

Витрати на prefill зростають разом із довжиною prompt. Лінійна частина — це matrix work для кожного layer. Квадратична частина — attention, під час якого кожен токен звертається до кожного попереднього токена; за великої довжини context вона стає суттєвою. Тому подвоєння prompt щонайменше подвоює TTFT.

Це можна перевірити за одну хвилину. Надішліть на свій server prompt на 200 токенів, а потім prompt на 2,000 токенів, щоразу запитуючи однакову кількість output tokens. TTFT різко зросте. Швидкість streaming після першого токена майже не зміниться.

Чому decode обмежений пропускною здатністю пам’яті?

Decode генерує один токен за крок. Щоб згенерувати цей токен, GPU має зчитати з пам’яті всі ваги моделі, використати кожну вагу для кількох операцій і відкинути її. Arithmetic intensity близька до 1, тому обчислювальні блоки більшість часу очікують.

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

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

ChartPublished memory bandwidth and the decode ceiling it implies for a 16 GB model
The data behind this chart
[
  {
    "device": "CPU, dual channel DDR5-5600",
    "mem_bandwidth_gb_s": 90,
    "decode_ceiling_tok_s": 6
  },
  {
    "device": "NVIDIA A10G",
    "mem_bandwidth_gb_s": 600,
    "decode_ceiling_tok_s": 38
  },
  {
    "device": "NVIDIA L40S",
    "mem_bandwidth_gb_s": 864,
    "decode_ceiling_tok_s": 54
  },
  {
    "device": "NVIDIA RTX 4090",
    "mem_bandwidth_gb_s": 1008,
    "decode_ceiling_tok_s": 63
  },
  {
    "device": "NVIDIA A100 80GB SXM",
    "mem_bandwidth_gb_s": 2039,
    "decode_ceiling_tok_s": 127
  },
  {
    "device": "NVIDIA H100 SXM",
    "mem_bandwidth_gb_s": 3350,
    "decode_ceiling_tok_s": 209
  }
]

У стовпці bandwidth наведено опубліковані виробниками значення специфікацій. У стовпці ceiling це значення поділено на 16 GB — розмір моделі з 8 мільярдами параметрів, збереженої з точністю 16 bit. Це арифметичний розрахунок, а не результат benchmark. Виміряна вами швидкість буде нижчою. Важливо знати, наскільки саме, оскільки це покаже, чи потрібно виправляти serving stack, чи апаратне забезпечення.

Прочитайте 6 рядків по порядку — і закономірність стане очевидною. CPU із двоканальною DDR5 забезпечує приблизно 90 GB/s, що обмежує decode приблизно до 6 токенів за секунду для цієї моделі. L40S досягає приблизно 54. H100 SXM із заявленою пропускною здатністю 3350 GB/s забезпечує приблизно 209.

Саме тому quantization є найсильнішим окремим фактором прискорення decode. Якщо зберігати ту саму модель у 8 bit замість 16, обсяг даних, який потрібно зчитати для кожного токена, зменшиться вдвічі, а верхня межа швидкості приблизно подвоїться. Обчислень не додалося. Потрібно переміщати менше даних у пам’яті.

Як виміряти кожну фазу на власному сервері?

Ollama повертає розподіл у тілі відповіді. Запитайте completion без streaming і зчитайте лічильники.

curl -s http://localhost:11434/api/generate -d '{
  "model": "llama3.2",
  "prompt": "Explain memory bandwidth in two sentences.",
  "stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'

Використовуйте tag моделі, яку ви фактично завантажили. Його покаже ollama list. prompt_eval_count і prompt_eval_duration — це prefill: кількість токенів prompt і час, витрачений на нього. eval_count і eval_duration — це decode. Тривалість вимірюється в nanoseconds, тому швидкість decode дорівнює eval_count / eval_duration * 1e9, а швидкість prefill — prompt_eval_count / prompt_eval_duration * 1e9. Очікуйте, що швидкість prefill буде значно вищою за швидкість decode для того самого запиту. Ця різниця пояснює все інше в цьому розділі.

Для OpenAI-compatible сервера, такого як vLLM, curl може виміряти час до отримання першого байта.

curl -N -s -o /dev/null \
  -w 'pretransfer %{time_pretransfer}s  first_byte %{time_starttransfer}s\n' \
  http://localhost:8000/v1/completions \
  -H 'Content-Type: application/json' \
  -d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'

time_starttransfer — це момент надходження першого байта тіла відповіді, тому разом із "stream": true це TTFT плюс час встановлення з’єднання. Відніміть time_pretransfer, щоб виключити витрати на встановлення з’єднання. Виконайте запит двічі та залиште другий результат, оскільки перший виклик може містити час холодного завантаження моделі.

vLLM також публікує цей розподіл як метрики Prometheus на /metrics. Виконайте curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' — і отримаєте гістограми vllm:time_to_first_token_seconds та vllm:inter_token_latency_seconds. Додайте vllm:num_requests_running і vllm:num_requests_waiting для глибини черги, а vllm:kv_cache_usage_perc — для навантаження на cache. Ці п’ять назв утворюють усю dashboard.

Під навантаженням vllm bench serve --model <name> --num-prompts 200 --request-rate 4 керує роботою сервера та показує час до першого токена і затримку для кожного вихідного токена з використанням перцентилів. Це єдиний спосіб побачити, як дві фази впливають одна на одну. Перш ніж щось оптимізувати, зафіксуйте чисту базову лінію: метод із вимірювання tokens per second у локальній LLM дає результат, який зберігається після перезавантаження.

Чому довгий системний prompt затримує появу першого токена, але не швидкість потокової генерації?

Тому що системний prompt — це робота на етапі prefill. Він обробляється один раз, під час того самого проходу, що й решта prompt, до появи першого токена. Після цього проходу він зберігається лише як записи KV cache, і decode читає їх разом з усіма іншими записами. Тому системний prompt на 3,000 токенів збільшує TTFT для кожного запиту, але майже не змінює кількість токенів за секунду.

Майже, але не повністю. Додаткові записи KV cache повторно читаються на кожному кроці decode, тому дуже довгий prompt трохи сповільнює decode. У наступному розділі це розглянуто докладніше.

Рішення — припинити повторно обчислювати той самий префікс. Сервер із prefix caching зберігає KV cache спільного префікса та повторно використовує його, тому другий запит із тим самим системним prompt повністю пропускає цю частину prefill. У vLLM ця функція називається automatic prefix caching; перевірте vllm serve --help для своєї версії, оскільки значення за замовчуванням змінювалося між релізами. Цей KV cache у GPU — не те саме, що prompt cache, за який API-провайдер виставляє рахунок, тому перед налаштуванням будь-якого з них варто прочитати про різницю між KV cache і prompt cache.

Чому decode сповільнюється в міру заповнення context?

Є дві причини, і обидві пов’язані з KV cache.

Перша — пропускна здатність пам’яті. На кожному кроці decode attention зчитує keys і values усіх попередніх токенів. Weights створюють фіксовані витрати на кожен токен. KV cache постійно зростає. Його розмір можна обчислити за config.json моделі: кількість байтів на токен дорівнює 2, помноженим на num_hidden_layers, num_key_value_heads, розмір head (hidden_size, поділене на num_attention_heads), і кількість байтів на елемент. Початкове 2 враховує один key і один value.

Для поширеної конфігурації моделі на 8 billion параметрів із 32 шарами, 8 key і value heads у GQA (grouped query attention), розміром head 128 і точністю 16 bit це становить 2 x 32 x 8 x 128 x 2 = 131,072 байти, тобто приблизно 128 KiB на токен. Тому діалог на 8,000 токенів використовує приблизно 1 GB KV cache для кожного запиту.

Друга причина — місткість. Цей 1 GB пам’яті вже не можна використати для weights або context іншого користувача. Сервер виділяє KV pool один раз під час запуску. У vLLM це налаштовується через --gpu-memory-utilization. Коли pool заповнений, нові запити очікують. Зростання vllm:num_requests_waiting, коли vllm:kv_cache_usage_perc тримається близько 1, є точною ознакою такого стану. Деякі stack-и переривають активний запит і пізніше повторно обчислюють його cache замість постановки запиту в чергу. Для користувача це проявляється як зупинка посеред stream.

Довгий context має подвійну ціну: більше роботи під час prefill на початку та більше читання пам’яті на кожному токені протягом решти відповіді.

Чому batching підвищує пропускну здатність і погіршує tail latency?

Оскільки decode обмежений пропускною здатністю пам’яті, додаткові запити майже не створюють навантаження на обчислювальну частину. Одне читання ваг може створити токен для кожної послідовності в batch, тому загальна пропускна здатність зростає майже лінійно разом із розміром batch, доки не закінчиться KV pool або batch не стане достатньо великим, щоб знову перейти в режим обмеження обчисленнями. Continuous batching перебудовує batch на кожному кроці, тому виконаний запит виходить, а запит із черги приєднується без очікування завершення сусідніх запитів.

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

Prefill робить цей ефект сильнішим. Великий prompt, що надходить під час потоку, займає пристрій на один довгий крок, тому всі поточні потоки бачать паузу. Chunked prefill усуває більшу частину цієї проблеми, розділяючи довгий prompt на частини та додаючи кожну частину до batch для decode. Станом на August 2026 engine vLLM V1 робить це за замовчуванням і надає керування балансом через --max-num-batched-tokens. Документація з налаштування vLLM прямо описує цей компроміс: менші значення, приблизно 2048, забезпечують кращу міжтокенну затримку (ITL), оскільки менше операцій prefill переривають decode, а більші значення забезпечують кращий TTFT, оскільки в один batch поміщається більше токенів prefill. Цей єдиний flag задає співвідношення prefill і decode як числове значення, яке можна змінювати. Прийнятний рівень p99 залежить від пропускної спроможності, а скільки одночасних користувачів може обслуговувати один self-hosted LLM визначається за тими самими метриками.

Чому потужніший GPU іноді нічого не змінює?

Тому що потужніший зазвичай означає більше обчислювальної потужності, а decode не потребує обчислень.

Порівняйте два рядки наведеної вище таблиці. A100 80GB має заявлену пропускну здатність 2039 GB/s, а L40S — 864 GB/s. Межа швидкості decode відповідає цьому співвідношенню: 127 токенів за секунду проти 54. RTX 4090 за більшістю показників є дуже швидкою картою, але її пропускна здатність 1008 GB/s обмежує швидкість на рівні 63. Незалежно від інших відмінностей між двома картами, decode для одного потоку визначається показником пропускної здатності у специфікації.

Отже, є два способи пришвидшити decode: читати менше байтів на токен — квантувати ваги або запускати меншу модель — або придбати GPU з більшою пропускною здатністю. Prefill працює навпаки. Він потребує обчислювальної потужності, тому швидша карта справді скорочує TTFT для довгих промптів. Якщо проблема полягає в тому, що перший токен з’являється через чотири секунди, потужніше обладнання може її усунути. Якщо проблема в тому, що текст виводиться повільно, найімовірніше, це не допоможе.

Чи варто запускати prefill і decode на окремих workers?

Великі стеки для обслуговування моделей саме так і працюють. Ця техніка називається розділенням prefill і decode. Один пул workers виконує лише prefill, другий — лише decode. KV cache, створений першим пулом, передається другому через швидке з’єднання. Це працює, оскільки для цих фаз потрібні різне обладнання та різне планування. Для prefill потрібні обчислювальні ресурси та великі пакети токенів. Для decode потрібні пропускна здатність пам’яті та багато одночасних послідовностей. Розділення дає змогу кожному пулу масштабуватися незалежно. Воно також не дозволяє одному великому prompt блокувати всі активні потоки.

На одному VPS (virtual private server) з одним GPU це майже ніколи не виправдано. Ви ділитимете один пристрій сам із собою, а передавання покажчика перетвориться на мережеве передавання гігабайтів cache. Техніка стає доцільною, коли у вас достатньо accelerator, щоб виділити окремі машини для кожної фази, і достатньо стабільного трафіку, щоб підтримувати роботу обох пулів. Якщо ресурсів менше, chunked prefill забезпечує майже таку саму ізоляцію за допомогою одного flag.

Що змінити, якщо показник незадовільний

Якщо TTFT занадто високий:

  • Скоротіть промпт. Вартість prefill залежить від кількості токенів у промпті, а системний промпт оплачується в кожному запиті.
  • Увімкніть кешування префікса, щоб повторюваний префікс обчислювався один раз, а не під час кожного запиту.
  • Збільште --max-num-batched-tokens, щоб на кожному кроці виконувалося більше роботи prefill.
  • Перевірте чергу, перш ніж звинувачувати модель. Значення vllm:num_requests_waiting вище нуля означає, що обробка запиту ще не почалася. Це проблема пропускної здатності.

Якщо кількість токенів за секунду занадто низька:

  • Квантизуйте ваги. Менше байтів на вагу означає менше байтів, які потрібно прочитати для кожного токена.
  • Порівняйте опубліковану пропускну здатність пам’яті вашої відеокарти з наведеною вище діаграмою та перевірте, наскільки близько ви перебуваєте до граничного значення.
  • Зменште --max-num-batched-tokens, щоб prefill рідше переривав decode.
  • Перевірте довжину контексту. Розмова, яка розрослася до тисяч токенів, на кожному кроці зчитує значно більший KV cache.

Середовище виконання також має значення, оскільки Ollama і vLLM по-різному планують prefill і decode, а параметр, який допомагає одному з них, може нічого не змінити в іншому. Спочатку виміряйте показники в обох фазах, а потім змініть лише один параметр.

FAQ

Чому перший токен генерується кілька секунд, а решта надходить швидко?

Очікування — це prefill, а потокова передача — decode. Під час prefill увесь prompt обробляється за один обчислювально обмежений прохід, перш ніж з’явиться будь-який результат, тому його тривалість зростає разом із довжиною prompt. Після цього decode генерує один токен за крок зі швидкістю, яку визначає пропускна здатність пам’яті. Вона майже не залежить від довжини prompt. Найчастіше причина — довгий system prompt у кожному запиті. Prefix caching усуває повторну обробку цієї частини.

Чи сповільнює довший prompt генерацію токенів за секунду?

Незначно, але з іншої причини, ніж TTFT. На кожному кроці decode зчитуються keys і values усіх попередніх токенів, тому більший KV cache означає більше байтів для зчитування на кожен токен. Для поширеної конфігурації з 8 billion параметрів cache становить приблизно 128 KiB на токен, тому для контексту з 8,000 токенів на кожному кроці обробляється приблизно 1 GB даних. Основний вплив довгого prompt усе одно припадає на TTFT, а не на швидкість потокової генерації.

Яка характеристика GPU дає змогу оцінити швидкість decode?

Пропускна здатність пам’яті. Поділіть заявлену пропускну здатність на розмір weights у пам’яті — так ви отримаєте теоретичну межу для одного потоку. Відеокарта з більшою обчислювальною потужністю, але з такою самою пропускною здатністю пам’яті, не забезпечить вищу швидкість потокової генерації. Саме тому квантування до 8 bits приблизно подвоює швидкість decode: кількість байтів, які зчитуються для кожного токена, зменшується вдвічі, а обчислення не змінюються.

Чому пропускна здатність зростає після додавання користувачів, але для кожного користувача система працює повільніше?

Одне зчитування weights обслуговує по одному токену для кожної послідовності в batch, тому загальна кількість токенів за секунду зростає разом із розміром batch. Водночас кожен окремий токен очікує спільного кроку, тому затримка для одного користувача також зростає. Відстежуйте p99 inter token latency, а не сукупну пропускну здатність, і перевіряйте vllm:num_requests_waiting, щоб визначити, чи запити стоять у черзі, а не обробляються.