Что такое 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-инстанс — плохая идея. Причины:

  1. Отказоустойчивость: crash одного инстанса = падение всех tenants. Несколько реплик = потеря одной не останавливает сервис.
  2. Изоляция: разные tenants можно направить на разные реплики (например, interactive-tenants на одни, batch-tenants на другие), и их нагрузки не смешиваются.
  3. Масштабируемость: 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):

  1. Prefix-aware routing: направляет запрос с повторяющимся префиксом (системный промпт агента) на ту реплику, где этот префикс уже в KV-cache. Это подняло KV-cache hit rate с 12% до 34% — прямой прирост throughput без изменения железа.
  2. Load balancing по загрузке: распределяет запросы по репликам с учётом текущей загрузки (сколько последовательностей активно), а не просто round-robin.
  3. Отказоустойчивость: если реплика падает, балансировщик направляет трафик на остальные две. Потеря одной реплики = потеря ~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 SLAThroughputПриоритетИзоляция
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.

← FP8 квантизация Observability для vLLM →