Перейти к содержимому
5 мин чтения

Как chunked prefill меняет справедливость очереди

Chunked prefill меняет latency чатов и скорость обработки документов. Разбираем эксперимент, метрики GPU, очереди и выбор бюджета токенов.

Как chunked prefill меняет справедливость очереди

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

Chunked prefill решает именно эту проблему очереди. Он разбивает начальную обработку большого промпта на порции и даёт планировщику шанс обслужить decode уже открытых диалогов между этими порциями. Но размер порции нельзя назвать универсальной настройкой. Маленькая порция часто спасает интерактивность, крупная быстрее проталкивает файл, а слишком маленькая заставляет GPU и планировщик тратить время на собственную координацию.

В смешанном сервисе нужно оптимизировать не среднюю скорость и не красивую загрузку GPU. Нужно заранее решить, кто имеет право ждать: пользователь в коротком чате, импортёр PDF на сто страниц или оба в пределах разных, измеримых обещаний.

Prefill и decode конкурируют за разную работу GPU

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

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

Документация vLLM описывает этот обмен прямо: при включённом chunked prefill планировщик сначала размещает ожидающие decode-запросы, затем отдаёт оставшийся бюджет prefill. Если промпт не помещается в max_num_batched_tokens, сервер режет его на части. Авторы документации также указывают противоположные эффекты: меньший бюджет улучшает inter-token latency, а больший помогает time to first token для новых запросов.

Здесь часто смешивают две вещи. Размер чанка не равен размеру документа. В vLLM max_num_batched_tokens является бюджетом токенов на планируемую итерацию, куда могут попасть decode и кусок одного или нескольких prefill. Фактическая длина порции длинного запроса зависит от свободного остатка этого бюджета и текущего состава батча.

Справедливость нельзя измерить одной средней задержкой

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

Для чата полезны две метрики. TTFT, time to first token, показывает, сколько пользователь ждёт начала ответа. ITL, inter-token latency, показывает паузы между последующими токенами. Сервис может иметь хороший средний TTFT и при этом раздражать пользователей длинными зависаниями в середине ответа. Для потокового интерфейса P95 и P99 ITL обычно важнее среднего значения.

Для длинного файла смотрите на время до первого токена, полное время prefill и полезную скорость обработки входа:

prefill throughput = число входных токенов / время от постановки в очередь до готовности KV cache

Не подменяйте эту скорость токенами генерации в секунду. Файл может генерировать короткий итог, но требовать огромного prefill. Если вы меряете только output tokens per second, вы не увидите, что импорт длинных документов фактически остановился.

Наконец, считайте ожидание в очереди отдельно от времени работы модели. Когда TTFT вырос, это может означать три разные неисправности: запрос не попал в GPU из-за очереди, запрос долго проходил prefill или KV cache не поместился и сервер начал вытеснение либо preemption. Эти случаи требуют разных действий.

Маленький чанк защищает чат, но не бесплатен

Малый бюджет токенов ограничивает время, которое один запуск prefill способен удерживать GPU. Поэтому decode чаще получает место в следующей итерации, а ответ в чате идёт ровнее. При конкуренции с документами это обычно снижает хвост ITL.

Цена состоит из трёх частей. Во-первых, длинный запрос проходит больше циклов планирования. Во-вторых, сервер хуже использует крупные матричные операции, если в каждом запуске осталось слишком мало prefill-токенов. В-третьих, при постоянном потоке decode prefill может получать только крошки бюджета. Документ не исчезает из очереди, но его завершение растягивается.

Большой бюджет даёт обратную картину. Он может заметно улучшить TTFT нового большого запроса и скорость загрузки файла, потому что сервер обработает больше входных токенов за запуск. Но если один такой запуск занял ощутимое время, пользователи увидят скачок ITL. Для голосового ассистента или операторского чата это часто хуже, чем более медленный импорт документа в фоне.

Популярный совет «поставьте максимум, который влезает в память» неверен для смешанной нагрузки. Память отвечает на вопрос, поместится ли KV cache. Справедливость определяется тем, сколько работы один класс запросов может выполнить до того, как другой класс снова получит очередь.

Эксперимент должен сталкивать запросы, а не усреднять их

Проверяйте chunked prefill не одиночным запросом на пустом сервере, а столкновением двух потоков. Первый поток имитирует чаты: короткий контекст, короткий ответ, регулярные поступления. Второй поток подаёт длинные документы, причём часть из них должна быть заметно длиннее вашего обычного RAG-контекста.

Зафиксируйте модель, квантование, GPU, лимит контекста, параметры sampling и число одновременных клиентов. Менять в одном прогоне можно только бюджет токенов и связанные правила длинного prefill. Если вы одновременно меняете модель, cache policy и размер чанка, результаты ничего не объяснят.

Минимальная матрица теста выглядит так:

ПрогонБюджет токеновЧатовый потокДокументный потокЦель
Aмалыйпостоянныйпостоянныйзащитить ITL
Bсреднийпостоянныйпостоянныйнайти рабочий компромисс
Cбольшойпостоянныйпостоянныйпроверить скорость файлов
Dсреднийвсплесквсплескувидеть хвост очереди

Не берите одинаковые запросы для всех документов. Реальная очередь имеет тяжёлый хвост: много умеренных контекстов и несколько очень длинных. Сделайте хотя бы три группы, например короткий запрос для чата, типичный RAG-контекст и большой документ. Содержимое можно заполнить повторяющимся нейтральным текстом, если токенизатор даёт нужную длину. Для измерения планировщика смысл текста не нужен, нужна контролируемая длина в токенах.

Сначала проведите базовый прогон с отключённым либо максимально крупным дроблением, если это позволяет ваш движок. Он покажет, как выглядит head-of-line blocking без возможности отдать очередь decode. Потом сравните его с несколькими бюджетами. Не гонитесь за одним победителем: вам нужна граница, после которой чат перестаёт соблюдать SLO.

Логируйте события запроса, а не только сводку сервера

Не переписывайте чатовый клиент
Сохраняйте OpenAI-совместимый код, меняя только base_url для доступа к нужным моделям.

Нужны timestamps на стороне клиента и сервера. Клиент должен записывать момент отправки, первый полученный токен, каждый следующий токен и завершение ответа. Серверные метрики должны содержать очередь, число активных sequence, использование KV cache и длительность итерации scheduler.

Ниже форма записи, которую удобно сохранять как JSON Lines. Она не зависит от конкретного SDK и позволяет потом пересчитать TTFT, ITL и полное время обработки.

{
  "request_id": "chat-00421",
  "class": "chat",
  "prompt_tokens": 420,
  "requested_output_tokens": 160,
  "sent_at_ms": 1730000000000,
  "first_token_at_ms": 1730000000840,
  "finished_at_ms": 1730000006420,
  "output_tokens": 147,
  "inter_token_ms_p95": 54
}

Для документа поменяйте class на document и сохраняйте prefill_ready_at_ms, если движок или обвязка умеют его отдать. Если нет, не притворяйтесь, что TTFT равен чистому prefill. В отчёте назовите метрику честно: «время до первого выходного токена документа». Она включает очередь, prefill и минимальную генерацию.

Сводная таблица должна содержать не меньше следующих строк для каждого прогона:

  • P50, P95 и P99 TTFT для чатов.
  • P50, P95 и P99 ITL для чатов.
  • P50 и P95 полного времени для документов.
  • Входные токены документов в секунду.
  • Долю времени, когда очередь документов не пуста.

Последняя строка важна. Если GPU выглядит занятой почти всё время, а очередь документов растёт, вы не нашли высокую загрузку. Вы нашли режим, в котором интерактивные запросы систематически отбирают остаточный бюджет у фоновой работы.

Нагрузка GPU показывает причину, а не качество сервиса

Средний GPU utilization полезен как диагностический сигнал, но плох как итоговая метрика. Можно держать GPU почти полностью занятой длинным prefill и при этом разрушить чат. Можно снизить среднюю загрузку из-за мелких чанков, но удержать обещанную задержку для пользователей. Выбирать приходится по цене продукта, а не по самой высокой цифре в панели мониторинга.

Разделите наблюдение хотя бы на четыре графика: utilization, память GPU, KV cache usage и длительность одной scheduler iteration. Если при уменьшении бюджета ITL улучшается, а длительность итерации падает, это ожидаемая картина. Если одновременно резко растёт очередь документов, вы выбрали слишком малую порцию для текущей интенсивности чатов.

Следите за памятью отдельно от вычислений. Chunked prefill не уменьшает итоговый KV cache длинного запроса. После завершения prefill документ всё ещё занимает память, пока его генерация не закончится или пока движок не освободит sequence. Если большой контекст приводит к частым preemption, уменьшение чанка может сгладить ITL, но не устранит дефицит памяти.

Есть и менее очевидный случай: GPU utilisation снижается, а latency ухудшается. Обычно так выглядит слишком мелкая гранулярность при малом числе активных запросов. В каждом раунде мало полезной работы, батчи не наполняются, а планировщик чаще передаёт управление. В такой ситуации добавление конкурирующих запросов не лечит сервис, оно только маскирует плохой параметр.

Ограничение длинных prefill важнее простого FCFS

Один API для разных нагрузок
Маршрутизируйте чатовые и документные запросы к 500+ моделям через один совместимый эндпоинт.

FCFS кажется честным, потому что запросы идут в порядке прихода. В смешанной очереди это слабая форма честности. Один документ, пришедший на миллисекунду раньше, получает право задержать сотни коротких запросов, даже если они требуют на порядки меньше вычислений.

vLLM даёт для chunked prefill отдельные параметры: порог long_prefill_token_threshold, максимальное число частично обработанных prefill и максимальное число одновременно обрабатываемых длинных prefill. В документации прямо сказано, что значение max_long_partial_prefills ниже max_num_partial_prefills позволяет коротким промптам в некоторых случаях обгонять длинные и улучшать latency.

Практическая политика для общего пула обычно выглядит так:

  1. Определите порог «длинного» промпта по распределению реальных входов, а не по размеру контекстного окна модели.
  2. Ограничьте число одновременных длинных partial prefill одним в первом тесте.
  3. Оставьте возможность нескольким коротким prefill идти параллельно, если память и модель это допускают.
  4. Проверьте, не голодают ли документы при высокой постоянной чатовой нагрузке.
  5. Если голодают, выделите им квоту времени, отдельную очередь или отдельный пул, а не просто увеличивайте чанк до максимума.

Это не обещание равного времени завершения. Это правило, которое не позволяет одной тяжёлой категории бесконтрольно захватывать вычисления. Недавняя работа Fairness-Aware and Latency-Controllable Scheduling for Chunked-Prefill LLM Serving приходит к тому же выводу: статический бюджет и жёсткий FCFS дают непредсказуемый хвост задержки и starvation, поэтому приоритет должен учитывать накопленное ожидание и оставшийся объём prefill.

Один пример показывает, где ломается наивная настройка

Представьте один GPU-пул. В нём уже идут 30 чатов: каждый выдаёт короткий поток ответа. Затем приходит документ с большим промптом. При крупном бюджете сервер ставит большую часть prefill документа в ближайшие итерации. Decode имеет приоритет при следующем планировании, но пользователи уже увидели растянутую текущую итерацию. Их ITL растёт, хотя очередь decode формально не нарушила порядок.

Теперь вы резко снижаете бюджет. Чаты начинают выдавать токены ровнее. Но документ получает небольшой остаток после decode. Если новые чаты приходят непрерывно, остаток почти всегда мал, и документ может ждать минуты. На дашборде чат выглядит отлично, а операция загрузки файла превращается в скрытую бесконечную очередь.

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

Не путайте это с разделением prefill и decode по разным GPU. Раздельные пулы снимают прямую конкуренцию фаз, но создают новую стоимость маршрутизации и передачи KV cache. Исследование P/D-Serve рассматривает именно такое разделение как способ масштабировать разные фазы, а не как автоматическую замену политике очередей. На одном общем пуле грамотный chunked prefill часто остаётся более простым первым шагом.

Не переносите чужой размер чанка в свой кластер

Меняйте маршрут без миграции
Отделяйте выбор модели от кода приложения и меняйте маршрут через единый API-шлюз.

Число 2048, 8192 или 16384 ничего не говорит без модели, GPU, длины ответов и состава нагрузки. В актуальной документации vLLM приведены именно ориентиры: меньшие значения, например 2048, дают лучшую ITL, а для максимальной пропускной способности на крупных GPU и небольших моделях рекомендуются значения выше 8192. Это направление поиска, а не готовая производственная конфигурация.

Сначала выберите ограничение по продукту. Например, чат не должен превышать свой P95 ITL при одновременной обработке документов, а P95 полного времени документа должен оставаться приемлемым для фоновой операции. Затем найдите самый большой бюджет, который держит чатовый SLO. Такой выбор обычно даёт документам больше полезной пропускной способности, чем бездумно малый чанк.

Если вы обслуживаете разные модели через единый API, проводите тест отдельно для каждой важной пары «модель плюс GPU». AI Router позволяет оставлять прикладной OpenAI-совместимый вызов прежним при маршрутизации к нужной модели, но параметры очереди и результаты latency всё равно принадлежат конкретному inference-движку и железу.

После выбора бюджета поставьте регрессионный тест в релизный процесс. Добавление нового системного промпта, включение tool calling или смена модели меняют длину входов. Настройка, которая была разумна для вчерашнего распределения токенов, быстро становится источником длинных пауз, если никто не смотрит на хвосты.

Настройка готова только после проверки голодания

Chunked prefill стоит включать ради контролируемой конкуренции, а не ради самого переключателя. Хороший результат выглядит прозаично: короткие чаты держат TTFT и ITL под нагрузкой, документы продолжают продвигаться, а GPU не получает постоянных провалов из-за слишком мелких итераций.

Проверьте худший режим, а не только типичный. Запустите длительный поток чатов, добавьте очередь больших файлов и посмотрите на самый старый документ. Если его возраст растёт без верхней границы, ваша политика защищает чат, но не является справедливой. Исправляйте правило очереди, число одновременных длинных prefill или архитектуру пулов. Размер чанка сам по себе этого не исправит.

Часто задаваемые вопросы

Что такое chunked prefill в LLM-сервере?

Chunked prefill делит обработку длинного входного контекста на части, которые планировщик может вставлять между другими работами. Он не делает саму модель быстрее и не уменьшает число токенов в документе. Его задача состоит в том, чтобы один большой запрос не занимал GPU непрерывно, пока короткие чаты ждут первого токена.

Нужен ли chunked prefill каждому LLM-приложению?

Обычно он помогает, если на одном пуле GPU одновременно живут чаты, RAG-запросы и загрузка больших файлов. При исключительно пакетной обработке длинных документов крупный бюджет токенов часто даст больше пропускной способности. Решение зависит от распределения длин промптов и SLO, а не от того, включена ли функция по умолчанию.

Какие метрики лучше всего показывают влияние длинных документов на чат?

В первую очередь смотрите на TTFT коротких запросов во время загрузки длинных документов. Затем смотрите на P95 и P99 inter-token latency уже начавшихся ответов. Средняя задержка почти всегда скрывает очередь, которую видят реальные пользователи.

Как выбрать размер чанка для chunked prefill?

Маленький бюджет обычно защищает плавность генерации в чатах, потому что prefill реже отнимает у decode большой непрерывный отрезок работы. Но длинные документы проходят больше раундов планирования, а GPU может терять эффективность на слишком мелких порциях. Увеличивайте бюджет, пока выигрываете в TTFT больших запросов без нарушения SLO чата.

Чем chunked prefill отличается от приоритетной очереди?

Это разные механизмы. Chunked prefill определяет, можно ли прервать длинную обработку и сколько токенов попадёт в один запуск планировщика. Приоритет задаёт, какой запрос планировщик выберет раньше, когда есть конкуренция. Приоритет без ограниченной порции prefill всё ещё может плохо защищать уже идущий decode.

Когда chunked prefill снижает общую производительность?

Он может стать хуже, когда служба почти целиком обрабатывает большие документы и не имеет чувствительных к задержке чатов. Слишком мелкие части добавляют накладные расходы планировщика и могут снизить производительность prefill. Он также не исправляет нехватку KV cache, медленную токенизацию или неверно выбранную модель.

Заменяет ли chunked prefill разделение prefill и decode по разным GPU?

Отдельные GPU-пулы для prefill и decode устраняют основную конкуренцию фаз, но добавляют маршрутизацию и передачу KV cache между узлами. Chunked prefill остаётся полезен внутри общего пула или как запасной режим при перекосе нагрузки. Не начинайте с раздельной архитектуры, если обычное ограничение бюджета и очереди уже держат SLO.

Можно ли менять max_num_batched_tokens без перезапуска сервиса?

Да, но сначала измерьте результат на конкретной модели и железе. В vLLM параметры планировщика можно передать через API движка, а в логах приложения сохранить фактическую конфигурацию запуска. Не переносите число из чужого бенчмарка: размер модели, длина ответов и параллелизм меняют точку компромисса.

Как chunked prefill работает с изображениями и PDF?

Для таких входов нужны отдельные правила. Включите их в эксперимент отдельно, потому что обработка мультимодальных embeddings может занимать бюджет иначе, чем текст. В vLLM есть настройка, запрещающая частично планировать мультимодальный вход, и её изменение влияет на честность очереди.

С чего начать настройку chunked prefill в продакшене?

Практичный стартовый режим для смешанной нагрузки: один длинный prefill одновременно, явный порог длинного промпта и бюджет, который вы проверили под реальным чатом. AI Router позволяет сохранить OpenAI-совместимый клиент при смене маршрута к выбранной модели, поэтому сам эксперимент можно провести без переписывания прикладного вызова. После этого закрепляйте не одно число, а допустимые границы для TTFT, ITL и скорости обработки файлов.