Что такое Multi-tenant Serving
Multi-tenant serving — архитектура, где несколько независимых клиентов (tenants) делят один или несколько vLLM-инстансов. Это стандартный режим для внутренних платформ (когда разные команды/продукты гоняют через общий LLM-сервис) и для внешних API (когда разные пользователи делят инфраструктуру провайдера).
Ключевые проблемы
Три вещи, которые нужно решить, чтобы tenants не деградировали друг друга:
- Fair scheduling: каждый tenant получает справедливую долю ресурсов. Один tenant, шлющий 1000 запросов в секунду, не должен «топить» остальных.
- Resource isolation: один tenant не может деградировать сервис для других. Его длинные запросы, гигантские KV-cache и ошибки не должны бить по остальным.
- Priority management: критические запросы (например, interactive-чат против batch-аналитики) обрабатываются в первую очередь.
Подходы к реализации
Request-level scheduling
Грубый контроль на уровне запроса. vLLM лимитирует, сколько запросов одновременно в батче:
--max-num-seqs— максимальное количество последовательностей в батче.--max-num-batched-tokens— максимальная сумма токенов в итерации.
Это даёт базовый потолок, но не fairness между tenants: если один tenant заполняет весь max-num-seqs, остальные ждут.
Token-level scheduling
Более тонкий контроль на уровне токенов. Scheduler распределелает токено-бюджет между запросами так, чтобы ни один tenant не монополизировал итерации. Это основа для fair scheduling: каждый tenant получает квоту токенов в единицу времени.
Priority management
vLLM поддерживает приоритеты запросов (через API-параметры и scheduler-логику). Критические запросы получают доступ к ресурсам раньше. В production-настройках это обычно реализуется на уровне балансировщика, а не внутри vLLM: балансировщик знает, какой tenant какой приоритет, и направляет запросы на соответствующие реплики/очереди.
Почему несколько реплик лучше одного инстанса
Для настоящего multi-tenant с изоляцией один большой vLLM-инстанс — плохая идея. Причины:
- Отказоустойчивость: crash одного инстанса = падение всех tenants. Несколько реплик = потеря одной не останавливает сервис.
- Изоляция: разные tenants можно направить на разные реплики (например, interactive-tenants на одни, batch-tenants на другие), и их нагрузки не смешиваются.
- Масштабируемость: adding replica = adding capacity, без перенастройки одного инстанса.
Именно поэтому в референс-деплое мы держим 3 реплики (каждая 2×RTX 3090, TP=2), а не один инстанс на 6 GPU.
Что мы видели в референс-деплое
Наша конфигурация: 3 реплики vLLM (каждая 2×RTX 3090, TP=2, max-num-seqs 2) + собственный балансировщик на ~400 строк Go.
Балансировщик решает три задачи, которые NGINX не решает (подробнее — в части 4):
- Prefix-aware routing: направляет запрос с повторяющимся префиксом (системный промпт агента) на ту реплику, где этот префикс уже в KV-cache. Это подняло KV-cache hit rate с 12% до 34% — прямой прирост throughput без изменения железа.
- Load balancing по загрузке: распределяет запросы по репликам с учётом текущей загрузки (сколько последовательностей активно), а не просто round-robin.
- Отказоустойчивость: если реплика падает, балансировщик направляет трафик на остальные две. Потеря одной реплики = потеря ~33% capacity, но сервис не падает.
Каждая реплика — отдельный «tenant-домен»: мы можем направить interactive-нагрузку на одни реплики, batch-нагрузку — на другие, и их KV-cache не смешивается. max-num-seqs 2 на реплику — это и есть наша единица изоляции: 2 параллельные последовательности на реплику, 3 реплики = 6 последовательностей суммарно.
Rate limiting и квоты
Приоритеты решают, кто обслуживается первым, но не сколько может потребить один tenant. Для настоящего fair sharing нужны квоты:
- Token budget per tenant — ограничение на число токенов, которые tenant может сгенерировать в единицу времени (например, 100K токенов/мин). Превышение → запрос идёт в очередь или отклоняется с 429.
- Request rate limit — ограничение на число запросов в секунду на tenant (например, 5 rps). Защита от «шторма» запросов.
- Context budget — ограничение на суммарный размер контекста, который tenant может удерживать в KV-cache. Защита от одного tenant, который «закладывает» всю память длинным контекстом.
Эти квоты обычно реализуются на уровне балансировщика, а не внутри vLLM: балансировщик знает tenant ID каждого запроса и может применять per-tenant лимиты, прежде чем направить запрос на реплику. vLLM сам по себе не знает о tenants — он видит только запросы.
Практическая конфигурация: три типа нагрузки
Типичный production-сервис имеет три типа нагрузки с разными требованиями:
| Тип нагрузки | TTFT SLA | Throughput | Приоритет | Изоляция |
|---|---|---|---|---|
| Interactive-чат | < 2s | средний | высокий | отдельная реплика |
| Agent (длинный контекст) | < 5s | низкий (последовательный) | средний | отдельная реплика |
| Batch-аналитика | не критично | высокий (ночной) | низкий | оставшиеся реплики |
В такой схеме:
- Interactive-реплика — получает interactive-запросы, оптимизирована под низкий TTFT (маленький
max-num-seqs, быстрый prefill). - Agent-реплика — получает агентные запросы с длинным контекстом (большой
max-model-len,max-num-seqs 2). - Batch-реплика — принимает batch-нагрузку в фоновое время, не конкурирует с interactive.
Это и есть «разведение tenants по репликам»: каждый тип нагрузки имеет свою реплику и свои параметры, и они не мешают друг другу.
Вывод
Multi-tenant serving — это не «один vLLM на всех», а несколько реплик + умный балансировщик + приоритеты + квоты. Fair scheduling и изоляция достигаются разведением tenants по репликам, а отказоустойчивость — тем, что потеря одной реплики не роняет сервис. Для production-инференса с несколькими типами нагрузки это единственный разумный путь.
Полный разбор балансировщика — в части 4 серии «Наш опыт». Смежные материалы: Continuous Batching, Tensor Parallelism, Observability.