Глава 7 — Продвинутые стратегии батчинга
Седьмой пост поглавного разбора LLM Primer VI: Scaling AI Systems. Глава, показывающая, что батчинг — не оптимизация, а несущее движение, делающее bandwidth-bound декодинг экономически возможным, и что batch — это глагол, а не существительное.
Зачем эта глава существует
Один шаг декодинга читает десятки гигабайт весов из HBM, чтобы выдать один токен. Если GPU в любом случае стримит эти веса, он может выдать токен для многих запросов на том же шаге почти без предельной цены. Батчинг поэтому не оптимизация в обычном смысле — это единственный способ сделать декодинг экономически возможным на железе, ограниченном пропускной способностью памяти. Но очевидный способ батчить рушится при встрече с реальным миром, потому что запросы разной длины и завершаются в разное время. Глава 7 идёт от наивной схемы, которая почти работает, к планировщику уровня итераций, который продакшен-движки действительно запускают, и называет долг, который тот планировщик создаёт для главы 8, чтобы её оплатить.
7.1 Статический батчинг терпит поражение от проблемы первого-завершившегося
Статический батчинг — то, что все пишут первым: собирать запросы, пока не наберётся размер батча B или не выйдет тайм-аут, запустить prefill на всём батче на длину S_max, запустить цикл декодинга, пока все последовательности не выдадут EOS. Две цены катастрофичны в комбинации. Паддинг — 50-токенный промпт в одном батче с 4 000-токенным платит за 80-кратный prefill, чем нужен. Первый-завершившийся — цикл декодинга идёт, пока каждая последовательность не завершится, поэтому батч с одним 20-токенным запросом и одним 2 000-токенным проводит 99 процентов времени с 31 слотом, производящим токены на выброс. Средняя пропускная выглядит приемлемо, а хвостовая задержка определяется тем, какой длинный запрос батчу случайно попался. Динамический батчинг (переменный тайм-аут, разрешать поздним прибывающим в ожидающий батч) смягчает ожидание, но не меняет фундаментальную схему; он наследует ту же проблему первого-завершившегося, как только начинается декодинг.
7.2 Continuous batching планирует на итерацию, а не на запрос
Continuous batching — планирование уровня итераций Orca, in-flight batching NVIDIA, «центральный трюк» vLLM и TGI — это смена переменной. Вместо фиксирования батча на всю длительность генерации планировщик пересматривает состав батча на каждом шаге декодинга. После каждого шага: завершившиеся последовательности освобождают свои слоты и свой KV; новые последовательности из очереди входят в освободившиеся слоты; совместный шаг декодинга идёт на новом активном множестве. Два свойства декодинга LLM делают это возможным. Каждый шаг декодинга структурно — одна и та же операция: ядру всё равно, какая последовательность в слоте 7, а какая в слоте 11. И каждая последовательность несёт свой KV-кэш независимо, поэтому её память просто становится доступной, когда она уходит. Единицей справедливости становится итерация, а не запрос: 20-токенный запрос завершается примерно через 20 итераций независимо от того, кто ещё в батче, а 2 000-токенный занимает один слот столько, сколько ему нужно, не удерживая других в заложниках. На реальном длиннохвостом трафике — смеси коротких чат-обменов и длинных RAG-генераций — утилизация GPU на 70B-модели поднимается с типичных 10–20 процентов статического батчинга до 60–80 процентов, а p99-задержка резко ужимается.
7.3 Chunked prefill объединяет prefill с декодингом на одном чипе
Continuous batching оставляет напряжение. Новым прибывшим нужен prefill, ограниченный вычислениями и предпочитающий длинные последовательности на проход. Активным нужен декодинг, ограниченный пропускной и предпочитающий широкие батчи. Запуск полного prefill на свежий 4 000-токенный промпт в том же прямом проходе, что и шаг декодинга для тридцати идущих последовательностей, либо задерживает идущие декодинги (плохой TTFT для существующих), либо задерживает prefill новичка (плохой TTFT для нового). Chunked prefill режет prefill на куски — скажем, по 512 токенов — и перемежает эти куски с шагами декодинга активных последовательностей. Один прямой проход теперь несёт часть работы prefill новой сессии и часть работы декодинга уже идущих одновременно, и две фазы делят один и тот же поток весов. Вычислительный характер prefill поглощает арифметику; спрос декодинга на пропускную амортизируется по большему числу полезной работы на байт. Две фазы перестают быть антагонистичными на одном чипе. Оставшийся случай — трафика столько, что они всё равно дерутся, — это постановка для дизагрегированного serving-а в главе 12.
Что подготавливает глава 7
Continuous batching делает своё дело и, делая его, разоблачает KV-кэш как связывающее ограничение параллельности. Каждая активная последовательность несёт свой KV, размером пропорционально её текущей длине; последовательности входят и выходят на каждом шаге; движок не может заранее знать, насколько длинной окажется каждая из них. Наивная разметка «один слэб на слот» отдаёт обратно большую часть выигрыша батчинга. Глава 8 переносит операционно-системное решение сюда: разделить кэш на маленькие физические блоки, отвязать их от логических позиций токенов через таблицу страниц и дать политике вытеснения возвращать или делить блоки между последовательностями. PagedAttention — это движение, делающее проблему KV в continuous batching разрешимой.
Дальше — Глава 8: KV-кэш следующего поколения. PagedAttention, вытеснение H2O, InfiniGen и экономика префиксного кэширования.