SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Разница между prefill и decode в LLM

Фаза prefill ограничена вычислениями и влияет на время до первого токена. Фаза decode лимитирована пропускной способностью памяти. Разделяйте их при замере производительности GPU.

Различие между prefill и decode в одном абзаце

Различие между prefill и decode — это ключевой фактор, объясняющий большинство вопросов о задержках при работе с self-hosted LLM (large language model). Фаза prefill считывает весь промпт за один проход и ограничена вычислительной мощностью. Фаза decode генерирует ответ по одному токену за раз и ограничена пропускной способностью памяти. Время до получения первого токена (time to first token) определяется фазой prefill. Количество токенов в секунду (tokens per second) определяется фазой decode.

Обе фазы выполняются на одном и том же GPU (graphics processing unit), используют одни и те же веса и работают в рамках одного процесса, поэтому их естественно рассматривать как единую нагрузку. Однако по факту они ведут себя как две разные программы, совместно использующие одно устройство. Если разделить эти понятия, длинный список запутанных результатов перестанет вызывать вопросы.

Почему этап prefill ограничен вычислительной мощностью?

На этапе prefill весь промпт проходит через каждый слой нейросети за один проход. Промпт из 2,000 токенов создает нагрузку в 2,000 строк для каждого матричного умножения, поэтому GPU выполняет большой объем вычислений на каждый байт загруженных весов. Это соотношение — количество арифметических операций на байт переданных данных — называется арифметической интенсивностью, и у prefill она высока. Устройство работает близко к пределу вычислительной мощности, в то время как шина памяти имеет запас пропускной способности.

Prefill формирует два результата: KV cache (тензоры ключей и значений) для каждого токена промпта и первый выходной токен. Данные не поступают пользователю до завершения этого прохода, поэтому время выполнения prefill и время до получения первого токена (TTFT) практически идентичны.

Стоимость prefill растет вместе с длиной промпта. Линейная часть затрат приходится на матричные вычисления в каждом слое. Квадратичная часть связана с механизмом attention, где каждый токен взаимодействует с каждым предыдущим, что становится заметно при работе с длинным контекстом. Поэтому удвоение длины промпта как минимум удваивает TTFT.

Вы можете проверить это за одну минуту. Отправьте на сервер промпт из 200 токенов, а затем из 2,000 токенов, запрашивая одинаковое количество выходных токенов в обоих случаях. TTFT значительно возрастет. Скорость потоковой передачи после получения первого токена при этом почти не изменится.

Почему пропускная способность памяти ограничивает скорость декодирования?

Процесс декодирования генерирует один токен за шаг. Чтобы создать этот единственный токен, GPU должен считать из памяти каждый вес модели, выполнить с ним несколько операций и выгрузить его. Арифметическая интенсивность близка к 1, поэтому вычислительные блоки большую часть времени простаивают в ожидании данных.

Декодирование работает медленно, так как для каждого токена требуется считывание всей модели из памяти. Шина памяти определяет темп работы, а вычислительные блоки остаются незагруженными.

Это означает, что теоретический предел скорости декодирования одного потока можно рассчитать на бумаге. Разделите пропускную способность памяти на объем данных, занимаемый весами модели.

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-битной точности). Это теоретический расчет, а не результат бенчмарка. Ваша измеренная скорость будет ниже этого значения. Понимание того, насколько именно она ниже, полезно: это позволяет определить, нужно ли оптимизировать стек обслуживания или менять оборудование.

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

Именно поэтому квантование является самым эффективным способом увеличения скорости декодирования. Если сохранить ту же модель в 8 битах вместо 16, объем данных, считываемых для генерации одного токена, сокращается вдвое, а теоретический предел скорости примерно удваивается. Вы не добавляли вычислительных мощностей, вы просто снизили нагрузку на память.

Как измерить каждую фазу на собственном сервере?

Ollama возвращает разделение данных в теле ответа. Запросите генерацию без потоковой передачи (non-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}'

Используйте тег модели, которую вы уже загрузили; это покажет команда ollama list. prompt_eval_count и prompt_eval_duration относятся к фазе prefill: количество токенов в промпте и время, затраченное на их обработку. eval_count и eval_duration относятся к фазе decode. Длительность указана в наносекундах, поэтому скорость decode составляет eval_count / eval_duration * 1e9, а скорость prefill — prompt_eval_count / prompt_eval_duration * 1e9. Ожидайте, что скорость prefill будет значительно выше скорости decode для одного и того же запроса. Этот разрыв и объясняет всё, описанное здесь.

Для сервера, совместимого с OpenAI, например 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 для оценки нагрузки на кэш. Эти пять имен метрик составляют основу всей панели мониторинга.

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

Почему длинный системный промпт увеличивает задержку первого токена, но не влияет на скорость потоковой передачи?

Потому что системный промпт — это исключительно этап предварительного заполнения (prefill). Он обрабатывается один раз, в том же проходе, что и остальная часть промпта, до появления первого токена. После этого прохода он существует только в виде записей в KV-кэше, которые считываются при декодировании наряду с остальными данными. Таким образом, системный промпт объемом 3000 токенов увеличивает TTFT (время до первого токена) для каждого запроса, практически не меняя количество токенов в секунду.

Практически, но не совсем. Эти дополнительные записи в KV-кэше считываются на каждом шаге декодирования, поэтому очень длинный промпт всё же немного замедляет процесс. Следующий раздел посвящен этому вопросу.

Решение заключается в том, чтобы прекратить повторное вычисление одного и того же префикса. Сервер с поддержкой кэширования префиксов сохраняет KV-кэш общего префикса и повторно использует его, поэтому второй запрос с тем же системным промптом полностью пропускает этап prefill для этой части. vLLM называет это автоматическим кэшированием префиксов; проверьте vllm serve --help для вашей версии, так как настройки по умолчанию менялись в разных релизах. Этот KV-кэш в памяти GPU отличается от кэша промптов, за который вы платите провайдеру API, и разницу между KV-кэшем и кэшем промптов стоит изучить перед тем, как приступать к настройке любого из них.

Почему декодирование замедляется по мере заполнения контекста?

Существует две причины, обе связаны с KV cache.

Первая — пропускная способность. На каждом шаге декодирования механизм внимания считывает ключи и значения для всех предыдущих токенов. Веса создают фиксированную нагрузку на каждый токен. KV cache — растущую. Вы можете вычислить его размер на основе config.json модели: количество байт на токен равно 2, умноженному на num_hidden_layers, на num_key_value_heads, на размерность головы (hidden_size, деленное на num_attention_heads) и на количество байт на элемент. Множитель 2 учитывает один ключ и одно значение.

Для стандартной архитектуры с 8 миллиардами параметров, 32 слоями, 8 головами ключей и значений при использовании GQA (grouped query attention), размерности головы 128 и точности 16 бит, это составляет 2 x 32 x 8 x 128 x 2 = 131,072 байта, примерно 128 KiB на токен. Таким образом, диалог на 8,000 токенов занимает около 1 GB KV cache на каждый запрос.

Вторая причина — емкость памяти. Этот 1 GB — память, которая не может быть использована для хранения весов или контекста другого пользователя. Сервер определяет размер пула KV один раз при запуске, в vLLM через --gpu-memory-utilization, и когда пул заполняется, новые запросы встают в очередь. Рост vllm:num_requests_waiting при значении vllm:kv_cache_usage_perc, близком к 1, является точным признаком этого состояния. Некоторые стеки вытесняют выполняющийся запрос и пересчитывают его кэш позже вместо постановки в очередь, что пользователь воспринимает как зависание в середине генерации.

Длинный контекст обходится вам дважды: больше работы по предварительному заполнению (prefill) в начале и больше операций чтения из памяти на каждый токен для остальной части ответа.

Почему пакетная обработка повышает пропускную способность, но увеличивает задержку в «хвосте» распределения?

Поскольку процесс декодирования ограничен пропускной способностью памяти, дополнительные запросы практически не требуют вычислительных ресурсов. Одно чтение весов позволяет сгенерировать токен для каждой последовательности в пакете, поэтому общая пропускная способность растёт почти линейно вместе с размером пакета, пока не закончится место в KV-кэше или пока пакет не станет настолько большим, что снова упрётся в вычислительную мощность. Continuous batching пересобирает пакет на каждом шаге: завершённый запрос уходит, а запрос из очереди присоединяется, не дожидаясь остальных.

Расплата наступает в виде задержек в высоких процентилях. Теперь генерация следующего токена для каждого пользователя ожидает завершения самой медленной части общего шага. В результате p50 (медиана) остаётся приемлемой, а p99 (самый медленный 1 запрос из 100) значительно возрастает. Пользователи замечают именно p99, так как это ощущается как пауза в середине предложения.

Эта проблема усугубляется фазой prefill. Длинный промпт, поступивший в процессе генерации, занимает устройство на один длительный шаг, из-за чего у всех пользователей, получающих ответ в реальном времени, возникает задержка. Метод chunked prefill устраняет большую часть этих задержек: он разбивает длинный промпт на части и подмешивает каждую часть в пакеты декодирования. По состоянию на август 2026 года движок vLLM V1 делает это по умолчанию и позволяет управлять балансом через --max-num-batched-tokens. В документации по настройке vLLM этот компромисс описан прямо: меньшие значения (около 2048) обеспечивают лучшую задержку между токенами (ITL), так как меньше операций prefill прерывают декодирование, а большие значения улучшают TTFT, так как в один пакет помещается больше токенов prefill. Этот единственный флаг определяет баланс между prefill и декодированием, представленный в виде числа, которое можно изменять. Вопрос о том, в какой момент p99 перестаёт быть приемлемым, зависит от ёмкости системы, и в статье сколько одновременных пользователей может обслуживать одна self-hosted LLM этот вопрос подробно разбирается с использованием тех же метрик.

Почему более мощный GPU иногда не дает прироста производительности?

Потому что «более мощный» обычно означает больше вычислительных ресурсов, а для декодирования вычислительная мощность не является критическим фактором.

Сравните две строки в таблице выше. Для A100 80GB заявленная пропускная способность составляет 2039 ГБ/с, а для L40S — 864 ГБ/с; предел декодирования меняется пропорционально: 127 токенов в секунду против 54. RTX 4090 — очень быстрая карта по большинству показателей, и её 1008 ГБ/с обеспечивают предел в 63. Независимо от других различий между картами, скорость декодирования одного потока напрямую зависит от пропускной способности памяти, указанной в спецификации.

Таким образом, есть два способа ускорить декодирование: считывать меньше байтов на токен (квантование весов или использование меньшей модели) либо увеличить пропускную способность памяти. Для этапа prefill ситуация обратная. Он требует вычислительных мощностей, поэтому более быстрая карта действительно сокращает TTFT при длинных запросах. Если проблема в том, что первый токен генерируется четыре секунды, более производительное оборудование может её решить. Если же жалоба на то, что текст выводится медленно, замена оборудования, скорее всего, не поможет.

Стоит ли запускать prefill и decode на разных воркерах?

Крупные стеки обслуживания делают именно так; этот метод называется дезагрегацией prefill и decode. Один пул воркеров выполняет только prefill, второй — только decode, а KV cache, созданный первым пулом, передается второму через высокоскоростное соединение. Это работает, так как для этих фаз требуются разные аппаратные ресурсы и разные стратегии планирования. Prefill требует вычислительной мощности и обработки больших пакетов токенов. Decode требует пропускной способности памяти и поддержки множества одновременных последовательностей. Разделение позволяет масштабировать каждый пул независимо и предотвращает ситуацию, когда один огромный промпт блокирует все активные потоки.

На одном VPS (virtual private server) с одним GPU это почти никогда не имеет смысла. Вы будете делить одно устройство само с собой и превратите передачу указателя в сетевую передачу гигабайтов кэша. Этот метод становится эффективным, когда у вас достаточно ускорителей, чтобы выделить целые машины под каждую фазу, и достаточно стабильный трафик, чтобы загрузить оба пула. При меньших масштабах chunked prefill обеспечивает практически такую же изоляцию с помощью одного флага.

Что изменить, если показатели неудовлетворительны

Если TTFT слишком высокий:

  • Сократите промпт. Стоимость префилла зависит от количества токенов в промпте, а системный промпт оплачивается при каждом запросе.
  • Включите кэширование префиксов (prefix caching), чтобы повторяющийся префикс вычислялся один раз, а не каждый раз заново.
  • Увеличьте --max-num-batched-tokens, чтобы на каждом шаге выполнялось больше работы по префиллу.
  • Проверьте очередь, прежде чем винить модель. Значение vllm:num_requests_waiting выше нуля означает, что запрос еще не был запущен, что указывает на нехватку ресурсов.

Если количество токенов в секунду слишком низкое:

  • Используйте квантование весов. Меньше байт на вес — меньше байт считывается для генерации одного токена.
  • Сравните заявленную пропускную способность памяти вашей карты с таблицей выше и оцените, насколько вы близки к пределу.
  • Уменьшите --max-num-batched-tokens, чтобы префиллы реже прерывали процесс декодирования.
  • Проверьте длину контекста. Диалог, выросший до тысяч токенов, требует считывания гораздо большего KV-кэша на каждом шаге.

Среда выполнения (runtime) здесь также имеет значение, поскольку Ollama и vLLM планируют префилл и декодирование по-разному, и настройка, помогающая одному, может быть бесполезна для другого. Сначала проведите измерения для обеих фаз, а затем изменяйте только один параметр за раз.

FAQ

Почему первый токен генерируется секунды, а остальное выводится быстро?

Ожидание вызвано этапом prefill, а потоковый вывод — этапом decode. На этапе prefill вся подсказка (prompt) обрабатывается за один вычислительный проход до появления первого результата, поэтому затраты времени растут пропорционально длине подсказки. На этапе decode модель генерирует по одному токену за шаг со скоростью, ограниченной пропускной способностью памяти, что почти не зависит от длины подсказки. Частая причина задержки — длинный системный промпт в каждом запросе. Использование кэширования префиксов (prefix caching) позволяет исключить повторную обработку этой части.

Замедляет ли длинная подсказка генерацию токенов в секунду?

Незначительно, и по другой причине, чем TTFT. На каждом шаге decode модель считывает ключи и значения (KV) всех предыдущих токенов, поэтому больший объем KV-кэша означает больше байтов, считываемых для каждого токена. Для типичной архитектуры с 8 миллиардами параметров кэш составляет около 128 KiB на токен, поэтому контекст из 8,000 токенов — это примерно 1 GB данных, к которым происходит обращение на каждом шаге. Основное влияние длинной подсказки всё ещё сказывается на TTFT, а не на скорости потоковой передачи.

Какая характеристика GPU определяет скорость decode?

Пропускная способность памяти. Разделите заявленную пропускную способность на размер весов в памяти, и вы получите теоретический предел для одного потока. Карта с большей вычислительной мощностью, но такой же пропускной способностью не будет генерировать токены быстрее. Именно поэтому квантование до 8 бит примерно удваивает скорость decode: оно вдвое сокращает количество байтов, считываемых для каждого токена, не затрагивая вычислительные операции.

Почему общая пропускная способность растёт при добавлении пользователей, но каждый пользователь ощущает замедление?

Одно чтение весов позволяет сгенерировать токен для каждой последовательности в пакете (batch), поэтому общее количество токенов в секунду растёт вместе с размером пакета. Однако каждый отдельный токен теперь ожидает завершения общего шага, поэтому задержка для пользователя одновременно увеличивается. Отслеживайте p99 inter-token latency, а не совокупный показатель пропускной способности, и проверяйте vllm:num_requests_waiting, чтобы понять, стоят ли запросы в очереди или они уже выполняются.