Continuous Batching
Непрерывная пакетная обработка перестраивает пакет на сервере LLM на каждом шаге декодирования: завершённые запросы удаляются, новые добавляются, а GPU остаются загруженными.
Непрерывное пакетирование, также называемое планированием на уровне итераций или пакетированием запросов в полёте, — это метод планирования для обслуживания генеративных моделей, при котором сервер пересобирает пакет на каждом шаге декодирования. Завершившаяся последовательность сразу покидает пакет и возвращает результат, а ожидающий запрос занимает её место на следующем шаге. Кён-ин Ю и его коллеги из Сеульского национального университета и FriendliAI представили этот подход в Orca на OSDI 2022 и сообщили о пропускной способности в 36,9 раза выше, чем у NVIDIA FasterTransformer на GPT-3 175B при той же задержке. Название «непрерывное пакетирование» распространилось в 2023 году, когда его стали использовать vLLM и Hugging Face Text Generation Inference; теперь это планировщик по умолчанию в движках обслуживания, лежащих в основе больших языковых моделей (LLM).
Почему пакетирование на уровне запросов неэффективно использует GPU#
Генерация текста выдаёт один токен за прямой проход, а запросам нужно очень разное количество проходов: ответ «да» или «нет» заканчивается через несколько токенов, тогда как файл с кодом может содержать тысячи токенов. При статическом пакетировании группа запросов остаётся вместе, пока не завершится самый длинный запрос. Слоты, чьи последовательности закончились раньше, простаивают, а новые запросы ждут, пока освободится вся группа, из-за чего растёт задержка вывода каждого короткого запроса, стоящего в очереди за длинным.
Динамическое пакетирование — схема на уровне запросов, используемая в таких серверах, как NVIDIA Triton: сервер собирает запросы, поступившие за короткий промежуток времени, и обрабатывает их одним пакетом до завершения. Такой подход подходит моделям с фиксированной стоимостью обработки каждого входа, например классификатору изображений или детектору объектов, поскольку каждый вход обрабатывается за один прямой проход. Режим predict в Ultralytics YOLO формирует пакеты запросов из входных данных YOLO26: файл .streams со ссылками на камеры запускается как один пакет, содержащий самый свежий кадр из каждого потока, поэтому 8 потоков обрабатываются с размером пакета 8, а аргумент batch объединяет кадры из видео, каталога или списка изображений .txt в один прямой проход.
Как работает планирование на уровне итераций#
Планировщик выполняет один шаг декодирования для каждой активной последовательности, а затем проверяет, достигла ли она токена конца последовательности или ограничения по длине. Завершённые последовательности возвращаются, а выделенная им память освобождается; запросы из очереди допускаются к обработке, если помещаются в кеш KV. Orca обрабатывает (выполняет префилл) промпт нового запроса на том же шаге, на котором декодирует остальные последовательности; современные движки делают то же самое с помощью блочного префилла.
Для смешивания последовательностей разной длины потребовалась ещё одна идея — избирательное пакетирование, как её назвала Orca. Операции, которые обрабатывают каждый токен независимо, например большие матричные умножения в каждом слое трансформера, выполняются сразу для всех токенов всех последовательностей. Механизм внимания работает отдельно для каждой последовательности, поскольку длина контекста у них различается.
В этой симуляции те же восемь запросов планируются на GPU, который декодирует по четыре последовательности за раз:
import heapq
output_lengths = [5, 50, 8, 12, 40, 3, 30, 7] # tokens each queued request will generate
slots = 4 # sequences the GPU decodes together at each step
# Static batching: each group of 4 runs until its longest request finishes
static_steps = sum(max(output_lengths[i : i + slots]) for i in range(0, len(output_lengths), slots))
# Continuous batching: the next request takes the first slot that frees up
slot_free_at = [0] * slots
for length in output_lengths:
start = heapq.heappop(slot_free_at)
heapq.heappush(slot_free_at, start + length)
continuous_steps = max(slot_free_at)
for name, steps in [("static", static_steps), ("continuous", continuous_steps)]:
print(f"{name}: {steps} decode steps, slots busy {sum(output_lengths) / (steps * slots):.0%}")Для статического пакетирования требуется 90 шагов декодирования, а слоты заняты 43% времени. Непрерывное пакетирование завершается за 50 шагов — столько же, сколько занимает самый длинный запрос; слоты при этом заняты 78% времени.
Ограничения и сопутствующие методы#
Увеличение текущего пакета повышает общую пропускную способность, но каждый шаг занимает больше времени, поэтому растёт время обработки одного выходного токена. Поэтому движки ограничивают число параллельных последовательностей и количество токенов, обрабатываемых за один шаг; в vLLM для этого предусмотрены max_num_seqs и max_num_batched_tokens. Кроме того, непрерывное пакетирование сочетают с несколькими смежными методами:
- PagedAttention: Выделяет память кеша KV небольшими блоками: завершившаяся последовательность сразу освобождает память, а новая занимает только необходимый ей объём.
- Блочный префилл: Разбивает длинные промпты на фрагменты, чтобы префилл нового запроса не останавливал декодирование остальных.
- Спекулятивное декодирование: Даёт наибольший выигрыш при небольшом размере пакета, поскольку большие непрерывные пакеты уже задействуют большую часть вычислительных ресурсов GPU.










