Inference / Performance
Почему длинный prompt тормозит генерацию: расчёт стоимости prefill и decode

Длинный входной документ расходует вычислительный бюджет до того, как модель начнёт выдавать первый токен. Чтобы оценить эффект до нагрузочного теста, разделите запрос на две разные фазы — prefill и decode — и считайте их отдельно.
Короткий ответ
В фазе prefill сервер обрабатывает все входные токены и строит KV-кэш. В фазе decode он последовательно генерирует выходные токены, используя уже построенный кэш. Длинный prompt увеличивает:
- время до первого токена (TTFT);
- занятость GPU до начала генерации;
- объём KV-кэша и давление на память;
- вероятность того, что decode одного запроса будет ждать prefill другого.
Для грубой оценки полезно считать не «стоимость запроса» целиком, а две величины:
Cprefill = Nin / Rprefill
Cdecode = Nout / Rdecode
Trequest ≈ Cprefill + Cdecode
Здесь Nin — число входных токенов, Nout — число сгенерированных токенов, а R — измеренная производительность конкретной конфигурации в токенах в секунду.
Почему один показатель tokens/s вводит в заблуждение
Число tokens/s обычно смешивает два режима с разной вычислительной структурой. Prefill хорошо векторизуется: GPU обрабатывает большой блок токенов параллельно. Decode, напротив, автогрессивен: следующий токен зависит от предыдущего, а при батчинге растёт стоимость чтения KV-кэшей.
Поэтому сервер может показать высокую скорость обработки длинного входа и одновременно иметь плохой time-to-first-token или низкую скорость выдачи при большом числе конкурентных запросов. Для пользователя это выглядит как «модель долго думает», хотя часть времени уже потрачена на входной документ.
Расчётная модель
1. Время prefill
Пусть:
L— длина входа в токенах;rp— наблюдаемая скорость prefill, токенов/с;α— коэффициент накладных расходов планировщика, копирования и очереди.
Тогда:
Tp = α · L / rp
Если запрос обрабатывается в батче, используйте не только скорость одного запроса: фактический rp зависит от размера батча, длины последовательностей и того, как сервер смешивает prefill с decode.
2. Время decode
Пусть G — длина ответа в токенах, а rd — скорость decode для выбранной конкуренции:
Td = G / rd
Полное время до последнего токена в простом приближении:
Te2e = Tqueue + Tp + Td
Для первого токена decode почти не добавляет полной стоимости: обычно учитывают prefill, планирование и вычисление первого шага генерации. Поэтому TTFT особенно чувствителен к L.
3. Пропускная способность
Если сервер последовательно занят одним запросом, верхняя оценка по запросам в секунду:
Q ≈ 1 / (Tp + Td)
А по токенам можно считать отдельно:
Throughputin ≈ L · Q
Throughputout ≈ G · Q
Это не закон производительности GPU, а модель очереди. Она полезна для сравнения вариантов при одинаковых условиях. Реальный сервер может выиграть за счёт continuous batching или проиграть из-за памяти, фрагментации и конфликтов фаз.
Пример для H100 и Llama 3.1 70B
Ниже — не измерение и не обещание производительности, а воспроизводимый числовой пример. Значения rp, rd и α нужно заменить результатами собственного бенчмарка.
| Параметр | Значение | Комментарий |
|---|---|---|
| GPU | H100 | Конкретный вариант памяти и число GPU не заданы |
| Модель | Llama 3.1 70B | Квантование, параллелизм и runtime нужно указать отдельно |
rp |
8 000 токенов/с | Условное число для примера |
rd |
80 токенов/с | Условное число для одного активного decode-потока |
α |
1,15 | Условный запас на накладные расходы |
Рассмотрим запрос с L = 32 000 входных токенов и ответом G = 800 токенов:
Tp = 1,15 · 32 000 / 8 000 = 4,6 с
Td = 800 / 80 = 10 с
Te2e ≈ 14,6 с
Для входа в 4 000 токенов при том же ответе:
Tp = 1,15 · 4 000 / 8 000 = 0,575 с
Te2e ≈ 10,575 с
В этом конкретном примере длинный документ добавил около 4 секунд к времени запроса и почти в восемь раз увеличил время prefill. Если ответы короткие, доля prefill становится ещё заметнее. Если ответы длинные, доминирует decode, но длинный вход всё равно съедает ёмкость очереди и KV-кэша.
Как учитывать KV-кэш
KV-кэш хранит промежуточные ключи и значения attention для уже обработанных токенов. Его размер зависит от длины контекста, числа слоёв, числа KV-heads и точности хранения. Упрощённо:
MemoryKV ≈ L · layers · 2 · KV_heads · head_dim · bytes_per_element
Множитель 2 отражает ключи и значения. Формула опускает детали упаковки, выравнивания и реализации runtime, поэтому используйте её как оценку порядка величины.
Для длинных документов важен не только один запрос. При одновременной обработке B последовательностей:
MemoryKV,total ≈ Σ MemoryKV,i, i = 1…B
Когда KV-кэш приближается к доступной памяти, сервер уменьшает размер батча, чаще вытесняет запросы или начинает дольше ждать свободного слота. В этот момент линейная модель времени перестаёт быть точной: задержка растёт ступенчато.
Chunked prefill: как не блокировать decode
Chunked prefill разбивает длинный вход на части. Вместо одного блока из L токенов планировщик обрабатывает чанки размером C, перемежая их с decode-шагами других запросов.
chunks = ceil(L / C)
Tp ≈ Σ α · Ci / rp
В идеализированной модели сумма остаётся примерно той же. Выигрыш появляется в очереди: decode небольшого ответа может получить GPU между чанками, а не ждать окончания всего prefill.
| Свойство | Большой prefill без разбиения | Chunked prefill |
|---|---|---|
| TTFT текущего длинного запроса | Может быть меньше из-за меньшего числа переключений | Может вырасти из-за пауз между чанками |
| Задержка уже генерируемых запросов | Риск блокировки на время большого prefill | Обычно легче ограничить паузу |
| Управление очередью | Проще | Требует настройки размера чанка и политики планирования |
| Общее число вычислений | Базовая величина | Не обязано уменьшаться |
Размер чанка выбирают по SLO. Если важнее p99 inter-token latency для коротких ответов, начните с меньшего чанка и проверьте влияние на TTFT длинных запросов. Если важнее максимальный throughput документов, слишком мелкие чанки могут добавить лишние переключения.
Раздельные prefill- и decode-пулы
При disaggregated serving prefill и decode выполняются на разных пулах GPU. Prefill-пул быстро обрабатывает вход и передаёт состояние, а decode-пул продолжает генерацию. Это не делает вычисления бесплатными, но разделяет конкурирующие профили нагрузки.
Для двух пулов полезна следующая оценка:
Te2e ≈ Tqueue,p + Tp + Ttransfer
+ Tqueue,d + Td
Новая статья расходов — передача состояния и координация. Её нельзя игнорировать: длинный контекст означает большой объём данных, который может пройти через сетевую или внутрисистемную связь.
Преимущество пулов появляется, когда профили нагрузки устойчиво различаются. Например, входы длинные, ответы короткие — тогда prefill-пул можно масштабировать по TTFT, а decode-пул — по числу одновременных генераций. Если нагрузка непредсказуема или transfer дорог, единый пул с continuous batching может оказаться проще и эффективнее.
Воспроизводимый способ получить свои коэффициенты
Сначала зафиксируйте конфигурацию: точную модель, формат весов, число GPU, tensor/pipeline parallelism, максимальный контекст, размер батча, runtime и параметры scheduler. Без этого значения скорости нельзя переносить между серверами.
Шаг 1. Подготовьте матрицу входов
Используйте несколько длин, например 1k, 4k, 8k, 16k и 32k токенов, а длину ответа зафиксируйте на одном или нескольких уровнях. Важно измерять именно токены, а не символы.
Шаг 2. Снимите prefill
Запустите только измерительный прогон и соберите TTFT и число обработанных входных токенов. Не публикуйте секретные документы: используйте синтетический текст или локальный безопасный набор.
# Безопасный локальный шаблон: не вставляйте реальные секреты
python - <<'PY'
for n in [1000, 4000, 8000, 16000, 32000]:
print(f"prompt_tokens={n}")
PY
Команда лишь печатает план эксперимента и ничего не отправляет в сеть. Конкретный вызов inference-сервера зависит от используемого API и поэтому должен быть добавлен отдельно, без включения ключей доступа в shell history или логи.
Шаг 3. Снимите decode
Зафиксируйте входной контекст и измерьте скорость выдачи при разных значениях concurrency. Записывайте среднее и p50/p95/p99, а не только один лучший прогон.
Шаг 4. Повторите для chunked prefill
Меняйте только размер чанка. Для каждого значения сравните TTFT длинных запросов, inter-token latency коротких запросов, GPU utilization и пиковое потребление памяти.
Шаг 5. Подставьте измеренные значения
Возьмите устойчивый участок графика, а не случайный пик:
prefill_cost = input_tokens / measured_prefill_tps
decode_cost = output_tokens / measured_decode_tps
request_time = queue_time + prefill_cost + decode_cost
Если скорость зависит от длины контекста, используйте таблицу или кусочно-линейную модель вместо одной константы.
Проверка результата
Расчёт считается полезным, если он объясняет порядок величин и помогает выбрать эксперимент. Проверьте его по четырём направлениям:
- Сравните предсказанный TTFT с измеренным на тех же длинах входа.
- Проверьте, что увеличение числа входных токенов не скрыто в токенизаторе или шаблоне чата.
- Сверьте прогноз KV-кэша с фактическим пиковым потреблением памяти.
- Проверьте p95/p99 при смешанной нагрузке: короткий prompt + длинный prompt + активный decode.
Если одиночный запрос совпадает с моделью, а p99 под нагрузкой — нет, проблема обычно находится в очереди, планировщике, batching или памяти, а не в самой формуле.
Типовые ошибки
- Считать prompt в символах. Стоимость определяется токенами конкретного токенизатора.
- Использовать decode throughput для оценки TTFT. Это разные фазы и разные режимы загрузки GPU.
- Сравнивать разные форматы весов. Квантование меняет память и скорость.
- Игнорировать шаблон чата. Системные инструкции и служебные токены входят в
Nin. - Умножать одиночную скорость на число пользователей. При конкуренции меняются batching, KV-кэш и скорость decode.
- Считать chunked prefill бесплатным. Он меняет планирование и задержки, но не обязан уменьшать объём работы.
- Смешивать TTFT и end-to-end latency. Для длинного ответа доминировать может decode.
- Не учитывать передачу состояния между пулами. В раздельной архитектуре это отдельная задержка и потребление канала.
Ограничения модели
Формулы выше не заменяют нагрузочный тест. Они предполагают стабильные скорости и не моделируют полностью:
- динамическое continuous batching;
- конфликты за GPU и память;
- фрагментацию KV-кэша;
- спекулятивную декодировку;
- кэширование префиксов;
- сетевые задержки и сериализацию;
- различия между ядрами attention и реализациями runtime.
Особенно осторожно переносите пример на другой H100-конфиг или другую версию runtime. Название GPU и модели не определяет итоговую скорость без параметров параллелизма, точности и scheduler.
Итог
Ответ на вопрос «Почему длинный prompt тормозит генерацию: расчёт стоимости prefill и decode» начинается с раздельного учёта фаз. Сначала оцените Tp = L / rp, затем Td = G / rd, добавьте очередь и проверьте память KV-кэша.
Если длинные входы блокируют короткие ответы, исследуйте chunked prefill. Если prefill и decode имеют устойчиво разные профили нагрузки, рассмотрите раздельные пулы и включите стоимость передачи состояния. Во всех случаях заменяйте условные коэффициенты измеренными значениями конкретной H100-конфигурации и Llama 3.1 70B.
Дополнительные материалы: практические guides и глоссарий терминов.