Transformers documentation
Архитектура непрерывного пакетирования
Архитектура непрерывного пакетирования
Традиционная пакетная обработка (batching) обрабатывает фиксированную группу запросов одновременно и ожидает завершения самого медленного запроса перед началом обработки следующей группы. Из-за этого GPU простаивает между пакетами.
При непрерывном батчинге на каждом шаге генерации планировщик проверяет завершённые запросы и сразу заменяет их ожидающими. Короткие запросы исключаются из батча сразу после завершения, и GPU остаётся загруженным всё время. В результате значительно возрастает пропускная способность и снижается средняя задержка.
Жизненный цикл запроса
Запрос проходит через четыре состояния от момента отправки до завершения.
WAIT IN QUEUE LOAD PROMPT INTO KV STREAM OUTPUT DONE
┌────────────────┐ ┌─────────────────────┐ ┌─────────────────┐ ┌────────┐
│ PENDING │───▶│ PREFILLING │───▶│ DECODING │───▶│FINISHED│
│ ○ ○ ○ · · · │ │ ████████░░░░░░░░░░ │ │ → → → → · · │ │ ✓ │
└────────────────┘ └─────────────────────┘ └─────────────────┘ └────────┘
others ahead prompt chunks +1 token/step- Pending — запрос поставлен в очередь и ожидает планирования.
- Prefilling — токены промпта обрабатываются в прямом проходе.
- Декодирование — выходные токены генерируются по одному.
- Завершено — генерация завершена, результат доступен.
Запрос переводится из состояния pending в prefilling, когда планировщик находит достаточный бюджет токенов и место в кэше для принятия запроса. Если промпт слишком длинный, чтобы поместиться в один шаг, планировщик распределяет его по нескольким проходам вперед (chunk prefill).
Поблочный prefill
Обработка длинного промпта за один шаг обходится дорого, так как блокирует генерацию для других запросов. Chunked prefill решает эту проблему, разбивая большие префиллы на несколько шагов.
Blocking (long prefill blocks the batch) Step: 1 2 3 4 Req A [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] ← one huge prefill Req B ················ wait ················ Req C ················ wait ················ Chunked (same prompt split and interleave with decode) Step: 1 2 3 4 Req A [▓▓ pre] [▓▓ pre] [▓▓ pre] [→ dec] Req B [→ dec] [→ dec] [▓▓ pre] [→ dec] Req C [→ dec] · idle · [→ dec] · idle ·
Если промпт нового запроса превышает доступный бюджет токенов (заданный параметром max_batch_tokens в ContinuousBatchingConfig), планировщик обрабатывает столько токенов, сколько возможно, и откладывает остальные. На последующих шагах он продолжает с того места, где остановился, чередуя работу по prefill с текущими шагами decode. Это позволяет поддерживать продуктивность батча и сокращает время до первого токена для других запросов.
Планировщик
Планировщик определяет, какие запросы попадают в каждый проход вперед, на основе двух бюджетов и ограничения на количество запросов.
- Бюджет токенов — максимальное количество токенов запроса, обрабатываемых за один прямой проход, задается параметром
max_batch_tokensв ContinuousBatchingConfig. - Бюджет кэша — общее количество страниц KV, которые можно прочитать за один проход, ограниченное общим размером кэша.
- Ограничение на количество запросов — максимальное число запросов в одном проходе, задаваемое параметром
max_requests_per_batch. Тензор логитов масштабируется в зависимости от размера словаря, поэтому это ограничение задаёт пиковое потребление памяти для моделей с большим словарём.
FIFO
FIFOScheduler — планировщик по умолчанию. Он заполняет пакет в порядке приоритета. Сначала обрабатываются активные запросы декодирования, затем активные запросы префилла, а после — новые ожидающие запросы в порядке их поступления. Это максимизирует пропускную способность, поскольку существующие запросы обрабатываются до подключения новых.
PrefillFirst
PrefillFirstScheduler завершает операции chunked prefill перед возобновлением decode. Это снижает фрагментацию для рабочих нагрузок, в которых преобладают длинные промпты.
Установите планировщик в ContinuousBatchingConfig.
cb_config = ContinuousBatchingConfig(scheduler_type="prefill_first")Управление памятью
Кэш KV разбит на страницы, чтобы запросы разной длины могли использовать общий фиксированный пул памяти без фрагментации. Общий пул блоков памяти, реализованный как CachePool, предоставляет эти блоки каждому аллокатору типа внимания, который делит их на блоки и страницы.
Shared pool (first two chunks reserved for padding / never allocated)
chunk N [ ======================================== ]
| |
full attn: [ FULL BLOCK 0 | FULL BLOCK 1 | … ]
[ page L0 | page L0 | … ] (one page per full layer)
sliding: [ ----------- SLIDING BLOCK 0 ----------- ]
[ page L1 | page L2 | page L3 | … ] (one page per sliding layer)
page = page_size tokens of KV for one layer
block = one page per layer of that attention type
chunk = pool unit sized so full and sliding blocks tile the same bytesНепрерывная пакетная обработка поддерживает только два типа внимания: full_attention (FullAttentionCacheAllocator) и sliding_attention (SlidingAttentionCacheAllocator). Гибридные модели получают по одному аллокатору каждого типа. Тип внимания определяется моделью (layer_types / sliding_window), а не ContinuousBatchingConfig.
Слои со скользящим окном сохраняют только фиксированное окно последних токенов, перерабатывая небольшой набор страниц. Они не поддерживают таблицы блоков ядра или шаринг блоков.
Кэш резервирует два чанка перед чанками данных для выравнивания операций чтения и записи, которые запросы никогда не используют. Эти чанки никогда не выделяются для запросов.
Размер кэша
Менеджер определяет количество блоков, которые помещаются в память, при запуске на основе свободного объёма памяти GPU. Он учитывает тензоры KV, маски внимания, активации и служебные индексы, а затем настраивает размер пула так, чтобы он укладывался в max_memory_percent доступной памяти. Если параметр не задан, используется значение по умолчанию, зависящее от характеристик оборудования и параметров непрерывного батчинга. В режиме MPS для кэша выделяется меньшее пространство, так как CPU и GPU разделяют память, что позволяет избежать подкачки. num_blocks задаёт ёмкость в виде целого числа блоков модели по page_size токенов, если вы указываете его явно.
Вы можете зафиксировать значения в ContinuousBatchingConfig.
cb_config = ContinuousBatchingConfig(
page_size=256,
num_blocks=4096,
max_batch_tokens=512,
max_memory_percent=0.8,
)Поступление
Прежде чем запрос будет добавлен в пакет, планировщик проверяет, что для каждого аллокатора типа внимания имеется достаточная свободная ёмкость. Если у какого-либо аллокатора ресурсов окажется недостаточно, запрос отклоняется, и ничего не выделяется.
Чтобы избежать заполнения кэша до состояния, когда offloading становится единственным вариантом, планировщик применяет safety_margin. Как только свободная ёмкость кэша опускается ниже safety_margin, новые prefill-запросы удерживаются, и продолжаются только активные decode-операции. Планировщик FIFO по умолчанию использует значение 0.15 (15% ёмкости резервируется), тогда как prefill_first по умолчанию имеет значение 0.0. Настройте safety_margin в ContinuousBatchingConfig, где значение 0 отключает резерв.
Кэширование префиксов
Когда два запроса имеют общий префикс промпта, они могут использовать общие блоки, хранящие KV для этого префикса. Каждый завершённый блок хешируется по содержимому. Последующий запрос с совпадающим префиксом переиспользует блок и пропускает prefill для соответствующих токенов. Общие блоки учитываются по счётчику ссылок и возвращаются в пул свободных блоков только после завершения всех запросов, которые их используют.
allow_block_sharing по умолчанию имеет значение True. Префиксное разделение активно только в том случае, если каждый аллокатор поддерживает разделение, поэтому его используют только модели с полным вниманием. Чисто скользящие и гибридные модели отключают его. Установите allow_block_sharing=False для рабочих нагрузок с короткими запросами и длинными генерациями, где накладные расходы на учёт превышают выгоду.
Эвикция
Освобождённые блоки не обнуляются и остаются в состоянии «инициализированного» с сохранением их содержимого и хэша, чтобы последующее совпадение префикса могло их переиспользовать. Когда пул неинициализированных блоков исчерпывается, менеджер лениво понижает самый недавний инициализированный блок до неинициализированного, удаляя его хэш. Кэш префиксов представляет собой слой best-effort, который аллокатор отбрасывает, чтобы продолжать обслуживать новые запросы.
Мягкий сброс
Мягкий сброс — это резервный путь для offloading. Если менеджер не может скопировать KV-кэш запроса в пул обмена на CPU, потому что offloading на CPU отключён или пул заполнен, он выполняет мягкий сброс запроса. Он добавляет сгенерированные токены запроса к его исходному промпту, освобождает его блоки кэша и возвращает запрос в очередь. Раздел Offloading описывает, какие запросы выбираются для этого.
Когда в кэше снова появляется свободное место, запрос возобновляется с того места, где он был прерван, а история генерации закодирована в промпте.
CUDA-графы
Каждый шаг генерации связан с накладными расходами на CPU: от формирования батча до запуска ядер GPU и чтения результатов. Графы CUDA устраняют эти накладные расходы, записывая полную последовательность выполнения на GPU один раз и воспроизводя её для батчей с совпадающими формами.
Поскольку формы батчей изменяются на каждом шаге, система непрерывного батчинга обрабатывает это с помощью дополнения (padding) и кэширования.
- Длины запросов дополняются до ближайшего кратного
q_padding_interval_size. - Длины KV дополняются нулями до ближайшего кратного
kv_padding_interval_size. - Записанные графы хранятся в кэше.
Если форма батча с заполнением совпадает с формой кэшированного графа, граф воспроизводится без какого-либо диспетчеризации на CPU. Новые формы вызывают захват нового графа.
CUDA graphs требуют статического потока управления и несовместимы с attention masks. По умолчанию они обнаруживаются автоматически и отключаются, если условия не выполняются.
Асинхронная пакетная обработка
Асинхронная пакетная обработка использует две пары буферов ввода-вывода и два потока CUDA для перекрытия работы CPU и GPU.
Sequential CPU ── [prep N] ·········idle········· [prep N+1] ·········idle········· GPU ·········idle········· [compute N] ·········idle········· [compute N+1] Async CPU ── [prep N] [prep N+1] [prep N+2] ── GPU ── ········ [compute N] [compute N+1] ──
Пока GPU вычисляет батч N, CPU готовит батч N+1. Однако перекрытие этих двух процессов требует примерно вдвое большего объема VRAM.
Асинхронное батчинг требует активации CUDA-графов, поскольку воспроизведение графа обеспечивает стабильные адреса тензоров, необходимые для корректного перекрытия потоков.
Выгрузка
Запросы, генерирующие длинные выводы, могут расти до тех пор, пока в кэше KV не останется места для выделения следующего блока. Вместо того чтобы завершаться с ошибкой, менеджер возвращает активные запросы в очередь ожидания, освобождая блоки для оставшихся запросов.
Offloading выполняется по требованию. Планировщик сообщает о заблокированных запросах, то есть активных запросах, которым не удалось выделить необходимые блоки, а также о количестве блоков, требуемых каждым из них. Менеджер выгружает минимальное количество запросов, необходимое для освобождения достаточного числа блоков для покрытия этого спроса. Выгрузка начинается с самых новых запросов, поэтому запросы, уже запланированные в текущей партии, сохраняют свой кэш, а последний оставшийся активный запрос никогда не выгружается.
Две ситуации запускают выгрузку.
- Пакет запланирован, но некоторые активные запросы застряли. Менеджер выгружает достаточно застрявших запросов, чтобы следующий пакет мог выделить блоки для остальных.
- Кэш заполнен, и ни одна партия не может быть запланирована. Менеджер выгружает запросы и повторяет планирование в цикле в пределах того же шага, пока партия не влезет или пока не станет невозможно выгрузить ни один запрос. Он больше не ждёт следующего прямого прохода для восстановления.
Отгруженный запрос проходит по одному из двух путей. Если cpu_offload_space больше 0.0, менеджер копирует блоки KV-кэша запроса в предварительно выделенный закреплённый буфер CPU, объединяя все запросы, которые помещаются в пул обмена, в один перевод. После того как в кэше GPU освобождается место, менеджер копирует блоки обратно на GPU и возобновляет запрос без повторного вычисления его промпта и сгенерированных токенов. Запросы, которые не помещаются в пул, а также все отгруженные запросы при отключённой отгрузке на CPU, переходят на мягкий сброс.
Дальнейшие шаги
- Статья Continuous batching blog post рассматривает кэширование KV, chunked prefill и динамическое планирование с приведением цифр бенчмарков производительности.
- Примеры использования приведены в документации Continuous batching.