modelscope-classroom, учебник, раздел 6.5: Continuous Batching
Полный перевод раздела 6.5 «Учебника ModelScope по большим моделям»: почему статический батчинг теряет почти половину времени GPU, как работает планирование на уровне итераций, смешивание prefill и decode, chunked prefill, алгоритмы планирования, вытеснение и выгрузка KV-кэша, метрики и реализации в vLLM, TensorRT-LLM и SGLang.
Continuous Batching (непрерывный батчингнепрерывный батчингEN continuous batching (iteration-level scheduling)Состав батча пересматривается после каждого шага decode — закончившиеся запросы уходят, новые встают на их место. Идея системы Orca (2022), стандарт всех современных серверов.) — ключевая техника планирования в системах инференса LLM. Классический статический батчинг вынужден ждать, пока закончатся все запросы в батче, и только потом начинать новый — это приводит к серьёзной растрате ресурсов. Continuous Batching позволяет запросам динамически входить в батч и выходить из него, что сильно повышает пропускную способность системы.
Представьте досмотр в аэропорту. Классический статический батчинг похож на «пропуск партиями»: следующие 10 человек заходят, только когда вся предыдущая группа из 10 прошла досмотр. Если у первого — небольшой рюкзак и он прошёл за 30 секунд, а у восьмого — три чемодана и ему нужно 5 минут, ждут все. Continuous Batching похож на настоящую линию досмотра: ушёл один — на его место сразу встаёт следующий, и линия всегда загружена полностью.
graph TD
subgraph СТ["Статический батчинг"]
S1["Собрать B запросов"] --> S2["Сформировать батч"]
S2 --> S3["Decode всех запросов"]
S3 --> S4["Ждать самый медленный"]
S4 --> S5["Вернуть результаты"]
S5 --> S1
end
subgraph НБ["Continuous Batching"]
C1["Запрос пришёл"] --> C2["Динамически вошёл в батч"]
C2 --> C3["Сгенерировать токен"]
C3 --> C4{"Какой-то запрос завершён?"}
C4 -->|да| C5["Убрать завершённые"]
C5 --> C6["Добавить новые"]
C6 --> C3
C4 -->|нет| C3
end6.5.1 Проблемы статического батчинга
Заголовок раздела «6.5.1 Проблемы статического батчинга»Классический процесс
Заголовок раздела «Классический процесс»Процесс статического батчинга (Static Batching):
- Собрать запросов в батч.
- Сделать prefill всех запросов.
- Делать decode, пока не закончатся все запросы.
- Вернуть результаты, начать следующий батч.
Проблемы эффективности
Заголовок раздела «Проблемы эффективности»Потери на ожидании. Длины ответов в батче разные: короткие запросы заканчиваются раньше, но вынуждены ждать самый длинный — как на корпоративной эстафете, где самый быстрый бегун должен ждать на финише, пока соберутся все, чтобы начать следующий этап.
Пример:
Запрос 1: ответ 50 токенов, 5 сЗапрос 2: ответ 500 токенов, 50 сЗапрос 3: ответ 100 токенов, 10 с
Статический батчинг: ждём 50 с, у запросов 1 и 3 загрузка GPU крайне низкаяПотери на паддинге. Выравнивание длин требует паддинга, а заполненные позиции создают бесполезные вычисления внимания.
Всплески задержки. Границы батчей создают ожидание: задержка нового запроса зависит от самого медленного запроса в текущем батче.
Использование ресурсов
Заголовок раздела «Использование ресурсов»Пусть длины ответов в батче равномерно распределены на . Полезная загрузка:
где:
- , — минимальная и максимальная длина ответа в батче;
- длины ответов считаются равномерно распределёнными на ;
- «загрузка» — доля полезного времени вычислений GPU во всём времени; время, когда короткие запросы закончились и ждут, потрачено впустую.
Если , , загрузка всего — почти половина времени GPU уходит на ожидание.
6.5.2 Принцип Continuous Batching
Заголовок раздела «6.5.2 Принцип Continuous Batching»Основная идея
Заголовок раздела «Основная идея»Continuous Batching (ещё называют Iteration-Level Scheduling — планирование на уровне итераций) пересматривает состав батча после каждой итерации decode:
- завершённые запросы сразу удаляются;
- новые запросы сразу добавляются;
- размер батча меняется динамически.
Процесс планирования
Заголовок раздела «Процесс планирования»Итерация 1: [Req1, Req2, Req3] → генерируем токен → Req1 завершёнИтерация 2: [Req2, Req3, Req4] → добавили новый Req4, генерируем токенИтерация 3: [Req2, Req3, Req4] → генерируем токен → Req3 завершёнИтерация 4: [Req2, Req4, Req5] → добавили Req5, генерируем токен...Явных «границ батча» нет, запросы обрабатываются конвейером — как на линии досмотра в аэропорту, где нет деления на «первую партию» и «вторую партию», а поток пассажиров непрерывен.
Преимущества
Заголовок раздела «Преимущества»- Нет ожидания: запрос возвращается, как только завершён, не дожидаясь остальных в батче.
- Высокая загрузка: GPU всё время работает с полной нагрузкой.
- Низкая задержка: новый запрос сразу входит в батч, не дожидаясь конца текущего.
- Гибкое планирование: можно в первую очередь обслуживать срочные запросы.
6.5.3 Планирование prefill и decode
Заголовок раздела «6.5.3 Планирование prefill и decode»Смешанный батч
Заголовок раздела «Смешанный батч»Continuous Batching должен обрабатывать два вида запросов:
- запросы на prefill: только что пришли, нужно обработать промпт;
- запросы на decode: уже генерируют ответ.
Их вычислительные свойства различны: prefill упирается в вычисления (compute-boundограничено вычислениями (compute-bound)EN compute-boundОперация, в которой вычислитель загружен полностью, а память успевает подвозить данные. Ускоряется только более быстрым железом или меньшим числом операций.), decode — в память (memory-boundограничено памятью (memory-bound)EN memory-boundОперация, которая дольше ждёт данные из памяти, чем считает. Ускоряется, если читать меньше байт (квантование, GQA, MLA) или делать больше работы на каждый прочитанный байт (батчинг).).
Стратегии планирования
Заголовок раздела «Стратегии планирования»Последовательная: сначала закончить все prefill, потом decode.
- Просто, но может «заморить голодом» запросы на decode.
Чередующаяся: делать prefill раз в несколько итераций.
- Баланс между задержкой и пропускной способностью.
Разделённая: prefill и decode на разных GPU.
- Лучшая загрузка, но выше сложность.
Chunked Prefill и планирование
Заголовок раздела «Chunked Prefill и планирование»Вместе с Chunked Prefillprefill по частям (chunked prefill)EN chunked prefillДлинный промпт обрабатывается кусками, которые перемешиваются с шагами decode других запросов, чтобы те не замирали на время большого prefill. длинный prefill можно резать на куски и смешивать с запросами на decode:
Итерация 1: [Decode: Req1, Req2] + [Кусок prefill: Req3_part1]Итерация 2: [Decode: Req1, Req2] + [Кусок prefill: Req3_part2]Итерация 3: [Decode: Req1, Req2, Req3] → prefill Req3 завершён, он переходит в decodeТак длинный prefill не блокирует decode.
6.5.4 Трудности реализации
Заголовок раздела «6.5.4 Трудности реализации»Управление KV-кэшем
Заголовок раздела «Управление KV-кэшем»С Continuous Batching управлять KV-кэшемKV-кэшEN KV cache сложнее:
- запросы динамически входят и выходят, и KV-кэш нужно динамически выделять и освобождать;
- длины KV-кэша у разных запросов разные.
Paged AttentionPagedAttention (страничный KV-кэш)EN PagedAttentionKV-кэш хранится блоками фиксированного размера (например, по 16 токенов), а таблица блоков связывает логические позиции с физическими — как страницы виртуальной памяти в ОС. Основа vLLM. — идеальное решение: страничное управление естественно поддерживает динамическое выделение.
Последовательности разной длины
Заголовок раздела «Последовательности разной длины»Запросы в батче разной длины — как считать эффективно?
Паддинг: дополнить до самой длинной — просто, но расточительно.
Packed Attention: склеить несколько коротких последовательностей в одну длинную и разделить их маской внимания.
Ragged Tensor: использовать «рваные» тензоры неодинаковой длины и обойтись без паддинга.
vLLM и подобные системы используют специальные ядра для последовательностей разной длины, чтобы избежать расходов на паддинг.
Фрагментация памяти
Заголовок раздела «Фрагментация памяти»Частые входы и выходы запросов могут фрагментировать видеопамять. Страничный механизм Paged Attention эффективно смягчает эту проблему.
6.5.5 Алгоритмы планирования
Заголовок раздела «6.5.5 Алгоритмы планирования»First-Come-First-Served (FCFS)
Заголовок раздела «First-Come-First-Served (FCFS)»Самая простая стратегия: обрабатывать в порядке поступления.
- Честно, но может задерживать длинные запросы.
Shortest-Job-First (SJF)
Заголовок раздела «Shortest-Job-First (SJF)»В первую очередь — запросы, которые, по прогнозу, закончатся быстрее всего.
- Минимизирует среднюю задержку.
- Нужно предсказывать длину ответа (а прогноз неточен).
- Может «заморить голодом» длинные запросы.
Приоритетное планирование
Заголовок раздела «Приоритетное планирование»Планирование по приоритету запроса:
- платные пользователи > бесплатные;
- интерактивные > пакетные;
- срочные > обычные.
Справедливое планирование
Заголовок раздела «Справедливое планирование»Гарантирует каждому запросу справедливую долю ресурсов:
где «все ресурсы» — видеопамять и время вычислений GPU, а «число активных запросов» — сколько запросов сейчас обрабатывается. Запросам, уже получившим много ресурсов, понижают приоритет, чтобы каждый получил примерно одинаковое время обслуживания.
Планирование в vLLM
Заголовок раздела «Планирование в vLLM»vLLM использует стратегию FCFS + вытеснение:
- По умолчанию — FCFS.
- Когда не хватает видеопамяти, вытесняется последний вошедший запрос (приостанавливается и выгружается — swap out).
- Когда память освобождается, вытесненный запрос возобновляется.
6.5.6 Вытеснение (Preemption) и выгрузка (Swap)
Заголовок раздела «6.5.6 Вытеснение (Preemption) и выгрузка (Swap)»Зачем нужно вытеснение
Заголовок раздела «Зачем нужно вытеснение»Когда одновременных запросов слишком много и под KV-кэш не хватает видеопамяти, часть запросов нужно вытеснить (Preemption). Это как при внезапной толчее на досмотре: часть пассажиров приходится временно отправить обратно в зал ожидания, пока линия не освободится.
graph TD
A["Не хватает видеопамяти"] --> B{"Стратегия вытеснения"}
B --> C["Выбросить KV-кэш"]
B --> D["Выгрузить в память CPU"]
B --> E["Пересчитать"]
C --> F["Потом заново prefill"]
D --> G["Вернуть, когда память освободится"]
E --> H["Сохранить токены, потом заново prefill"]Стратегии вытеснения
Заголовок раздела «Стратегии вытеснения»Выбросить: удалить KV-кэш вытесненного запроса, потом заново сделать prefill.
- Просто, но теряется уже сделанная работа.
Выгрузить (Swap): перенести KV-кэш в память CPU, потом вернуть обратно — как сдать багаж в камеру хранения в зале ожидания и забрать, когда освободится место.
- Прогресс сохраняется, но растут расходы на ввод-вывод.
Пересчитать (Recompute): сохранить только промпт и уже сгенерированные токены, после вытеснения заново сделать prefill.
- Промежуточный вариант; подходит, когда prefill стоит недорого.
Механизм выгрузки в vLLM
Заголовок раздела «Механизм выгрузки в vLLM»vLLM поддерживает выгрузку KV-кэша между GPU и CPU:
- При нехватке видеопамяти выбирается запрос, вошедший последним.
- Его KV-кэш асинхронно копируется в память CPU.
- Блоки на GPU освобождаются.
- Когда память освободится, кэш асинхронно копируется обратно, и запрос возобновляется.
Paged Attention позволяет выгружать по блокам, что гибче.
6.5.7 Оптимизация производительности
Заголовок раздела «6.5.7 Оптимизация производительности»Выбор размера батча
Заголовок раздела «Выбор размера батча»При Continuous Batching фактический размер батча всё время меняется. Цели:
- максимально загрузить GPU;
- не выйти за пределы видеопамяти;
- сохранить разумную задержку.
Обычно задают максимальный размер батча, а планировщик подстраивает фактический.
Приоритет prefill
Заголовок раздела «Приоритет prefill»Prefill влияет на задержку запросов, которые уже в decode. Стратегии:
- немедленный prefill: новый запрос обрабатывается сразу — может вырасти задержка decode;
- отложенный prefill: накопить запросы и сделать prefill пачкой — меньше прерываний;
- chunked prefill: обрабатывать по кускам — влияние сглаживается.
Балансировка нагрузки
Заголовок раздела «Балансировка нагрузки»При развёртывании на нескольких GPU запросы нужно распределять равномерно:
- по кругу (round robin): просто, но не учитывает нагрузку;
- наименьшее число соединений: отдавать GPU, у которой сейчас меньше всего запросов;
- взвешенно: учитывать разную мощность GPU.
Метрики для мониторинга
Заголовок раздела «Метрики для мониторинга»- Глубина очереди: число ожидающих запросов — отражает нагрузку системы.
- Распределение задержки: P50, P95, P99.
- Пропускная способность: обработанных токенов в секунду.
- Доля вытеснений: доля вытесненных запросов — отражает нехватку ресурсов.
6.5.8 Реализации
Заголовок раздела «6.5.8 Реализации»vLLMvLLMEN vLLMОткрытый движок инференса LLM (UC Berkeley): PagedAttention, continuous batching, тензорный параллелизм, OpenAI-совместимый сервер (vllm serve, порт 8000). Стандарт для сервисов и генерации в RL. — эталонная реализация Continuous Batching + Paged Attention:
- Python API + ядра на C++/CUDA;
- поддерживает много моделей (LLaMA, Mistral, Qwen и др.);
- интеграция с Ray для распределённого инференса.
TensorRT-LLM
Заголовок раздела «TensorRT-LLM»TensorRT-LLM от NVIDIA даёт похожие возможности:
- сильно оптимизированные ядра CUDA;
- интеграция с Triton Inference Server;
- параллелизм на нескольких GPU.
SGLangSGLangEN SGLangДвижок инференса от LMSYS: RadixAttention (переиспользование общего префикса запросов), быстрый структурированный вывод, ранняя поддержка новых DeepSeek и Qwen; OpenAI-совместимый сервер на порту 30000. поверх Continuous Batching улучшает интерфейс программирования:
- RadixAttentionRadixAttentionEN RadixAttentionПрефикс-кэш SGLang — KV-кэш всех запросов хранится в префиксном (radix) дереве, общие начала переиспользуются автоматически, редко нужные ветви вытесняются по LRU.: более эффективный префикс-кэш;
- оптимизации структурированной генерации;
- поддержка управляющих конструкций (control flow).