Как работает динамическое бэтчирование в TGI vs vLLM?
Краткий тезис
Динамическое бэтчирование позволяет инференс-серверам обрабатывать несколько запросов одновременно, эффективно используя GPU. TGI и vLLM решают эту задачу по-разному: TGI использует token-level scheduler с FIFO-очередью без вытеснения, а vLLM — iteration-level scheduler с поддержкой preemption (вытеснения). Ключевое различие — способность vLLM динамически перераспределять память между запросами, что даёт более высокую утилизацию GPU за счёт потенциального роста латентности для отдельных запросов.
1. Что такое динамическое бэтчирование и зачем оно нужно
Динамическое бэтчирование (batching|dynamic batching) — техника, при которой инференс-сервер (например, TGI или vLLM) собирает несколько входящих запросов в общий батч, выполняет их параллельно и возвращает результаты. В отличие от статического бэтчинга (все запросы должны быть собраны до начала инференса), динамическое бэтчирование позволяет добавлять новые запросы в текущий батч «на лету» или переставлять их, чтобы минимизировать простои GPU.
Зачем LLM-инференс — дорогая операция, требующая значительной памяти KV-кэша и вычислений. Одиночные запросы не полностью загружают GPU, а групповое выполнение кратно увеличивает пропускную способность (throughput) — количество обработанных запросов в секунду.
Проблема запросы разной длины (генерация от 10 до 2000 токенов) и времени поступления. Простейшая очередь «первым пришёл — первым обслужен» приводит к тому, что короткие запросы ждут завершения длинных, а пустые слоты в батче простаивают.
2. TGI (Text Generation Inference): FIFO + token-level scheduling
TGI от Hugging Face использует подход token-level scheduler. Логика выглядит так:
- Шаг 1 все входящие запросы попадают в FIFO-очередь («первый пришёл — первый обслужен»).
- Шаг 2 при запуске очередного шага декодирования (одна итерация генерации) scheduler выбирает из очереди столько запросов, сколько влезает в память GPU (с учётом длины их KV-кэша).
- Шаг 3 запросы, которые уже обрабатываются, остаются в батче до завершения или пока не будет заполнен их max_new_tokens.
- Шаг 4 preemption не поддерживается — если для нового запроса не хватает памяти, он просто отклоняется (возвращается ошибка 503 или ожидает в очереди, но не может вытеснить уже выполняющийся запрос).
Особенности TGI
- Простота легко понять и деплоить.
- Starvation (голодание) коротких запросов если в начале запускается длинный запрос, короткие будут ждать его завершения.
- Потеря пропускной способности если в батче есть свободное место, но все запросы в очереди уже обрабатываются, GPU может простаивать в конце генерации.
3. vLLM: iteration-level scheduler + preemption
vLLM использует более агрессивный iteration-level scheduler. Основные отличия:
- Каждый шаг декодирования пересматривает состав батча. Scheduler анализирует не только новые запросы, но и текущие. Если суммарная память KV-кэша превышает лимит, он может вытеснить один или несколько выполняемых запросов.
- Preemption (вытеснение) — ключевая механика. Вытесненный запрос не удаляется, его KV-кэш свапается на CPU-диск (swap to disk). Освободившееся место на GPU занимает новый или более приоритетный запрос.
- Вытесненный запрос позже возобновляется с контрольной точки (сохранённого KV-кэша), но его латентность растёт на время swap.
Детали preemption
- Preemption mode может быть swap (сохранить весь KV-кэш) или recompute (сбросить кэш и заново прогнать префикс). vLLM по умолчанию использует swap для больших кэшей, recompute — для коротких.
- Priority scheme vLLM поддерживает приоритеты. При нехватке памяти вытесняется запрос с наименьшим приоритетом или с наибольшим оставшимся временем.
- Гранулярность можно вытеснять не весь запрос, а только его часть (один шаг), но обычно вытесняется целый запрос.
4. Сравнительная таблица: TGI vs vLLM
| Характеристика | TGI | vLLM |
|---|---|---|
| Тип планировщика | Token-level (один раз на начало генерации) | Iteration-level (каждый шаг) |
| Очередь | FIFO | FIFO + priority (опционально) |
| Preemption (вытеснение) | Нет | Да (swap / recompute) |
| Работа с переполнением памяти | Отклонение запроса / ожидание | Вытеснение и своп на CPU |
| Утилизация GPU | Средняя (простои при неравномерной длине) | Высокая (батч максимально заполнен) |
| Латентность (p50) | Предсказуемая, но может быть высокой при ожидании | Ниже для коротких запросов, выше для вытесненных |
| Пропускная способность (throughput) | Ниже (особенно при high concurrency) | Выше (до 2–4x в бенчмарках) |
| Сложность деплоя | Низкая (один Docker‑образ, минимум конфигов) | Средняя (требуется настройка swap, max_model_len) |
| Поддержка prefix caching | Да (Shared Prefix Cache) | Да (Automatic Prefix Caching) |
| Поддержка LoRA‑адаптеров | Да (ограниченно) | Да (гибко, несколько LoRA в батче) |
5. Как preemption влияет на latency и как с этим работать
Preemption — компромисс между памятью и латентностью. Когда запрос вытесняется, его KV-кэш копируется на CPU (или диск), а затем обратно. Это добавляет overhead:
- Swap latency копирование tensors через PCI‑e bus. Для модели 7B и кэша 4096 токенов может составлять 50–300 мс.
- Recompute latency если модель recompute, то заново выполняется forward pass для префикса (без генерации новых токенов).
- Priority‑based scheduling сценарии, где latency критична (например, чат-агенты), могут иметь высокий приоритет и реже вытесняться.
- Dynamic swap threshold можно настроить max_num_batched_tokens так, чтобы вытеснения происходили реже, ценой снижения утилизации.
- Chunked prefill предзагрузка (prefill) разбивается на чанки, что позволяет вставлять декад-шаги других запросов между чанками — уменьшает вероятность вытеснения.
6. Когда выбирать TGI, а когда vLLM
Выбираем TGI, если:
- Требуется предсказуемая латентность (SLA с жёсткими P99).
- Нагрузка невысокая (concurrency < 10) или запросы примерно одинаковой длины.
- Команда не готова к сложной конфигурации vLLM.
- Используются очень длинные контексты (32k+), где swap может стать узким местом.
Выбираем vLLM, если:
- Высокая пропускная способность важнее, чем латентность «хвоста».
- Смешанная нагрузка (короткие и длинные запросы вперемешку).
- Используются LoRA‑адаптеры, prefix caching или сложные сценарии агентов (множество инференс-вызовов).
- Готовы тестировать конфигурации и мониторить swap‑активность.
7. Детали реализации: настройки и примеры конфигов
Пример конфигурации TGI (ENV или CLI):
docker run ghcr.io/huggingface/text-generation-inference:latest \
--model-id meta-llama/Llama-2-7b-chat-hf \
--num-shard 1 \
--max-total-tokens 4096 \
--max-batch-prefill-tokens 2048
Здесь max-batch-prefill-tokens ограничивает размер одного prefill‑шага.
Пример конфигурации vLLM (при запуске сервера):
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--max-model-len 4096 \
--max-num-batched-tokens 8192 \
--swap-space 16 # 16 GB CPU swap
--swap-space выделяет память на CPU для хранения вытесненных кэшей. Если этот параметр мал, preemption будет невозможен, и vLLM будет работать как TGI (отклонять запросы).
8. Влияние на Agentic RAG-системы
В архитектуре Agentic RAG агент делает много последовательных вызовов LLM: анализирует запрос, решает, какие документы искать, генерирует ответ. При этом:
- Запросы агента обычно короткие (до 1000 токенов), но их много.
- Критичное требование — низкая латентность каждого шага, чтобы агент работал интерактивно.
- vLLM с iteration-level scheduler и приоритетами для коротких запросов часто даёт лучший P50/P90, но может «выкинуть» длинный контекст (например, при загрузке документа) в swap.
Рекомендация для агентов используйте vLLM с настройкой --priority-scheduler (если поддерживается) или разделяйте длинные prefetch-запросы и короткие reasoning-запросы на отдельные инстансы.
Пет-проект для закрепления
Задача Сравнить пропускную способность и латентность TGI и vLLM при синтетической нагрузке с разной длиной запросов.
Инструменты
- Docker, k6 или locust для генерации нагрузки.
- TGI (официальный образ) и vLLM (pip install vllm + openai‑совместимый сервер).
- Jupyter + matplotlib для визуализации.
Шаги:
- Запустите оба сервера с одной и той же моделью (например,
Qwen2.5-7B-Instruct) на одном GPU. - Напишите скрипт на Python, который отправляет запросы с длиной prompt = 50..2000 токенов и max_tokens = 10..500 (распределение uniform).
- Сгенерируйте 1000 запросов, засеките время ответа для каждого.
- Повторите для разного уровня concurrency (1, 4, 8, 16).
- Вычислите метрики: средний throughput (req/s), P50, P95, P99 latency.
- Постройте гистограммы. Для vLLM дополнительно отслеживайте количество preemptions через логи (
/metricsendpoint).
Ожидаемый результат
- При низком concurrency TGI и vLLM показывают близкие результаты.
- При concurrency > 4 vLLM даёт на 30-100% больше throughput, но P99 может быть выше на 200-500 мс из-за вытеснений.
- TGI при переполнении памяти возвращает 503 ошибки, vLLM — успешно обрабатывает все запросы (если swap-space > 0).
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 849 | Оптимизации инференса: PagedAttention, FlashAttention |
| 850 | Сравнение серверов: TGI, vLLM, Triton Inference Server |
| 847 | Влияние batch size на latency и throughput |
| 835 | Архитектура Agentic RAG: требования к latency |
| 840 | Кэширование KV-кэша в агентах |
| 853 | Профилирование инференс-сервера в production |
Навигация
- Предыдущий: 847
- Следующий: 849
- Индекс: 00. Индекс разборов