Четыре свойства LLM-трафика

  • Запросы не однотипные: web ~50 мс · LLM 10 с – 10+ мин · агент ~1 мин. Длительность requests разбросана в 100× (до 100 000× с учётом prefill).
  • Стоимость ∝ токенам: prefill + decode; 128-token prompt → ~10 MB KV-кэша, 32K-token prompt → ~2.5 GB. Слот занят на минуты.
  • Кэш — состояние на ноде: поток запросов вытесняет KV-кэш → следующий ход сессии пересчитывает prefill.
  • Bursty: агент (pi.dev, Cursor, Kilo) создаёт 3–10 параллельных запросов на человека, а не 1.

Почему это ломает round-robin

  • RR распределяет число запросов, а не стоимость. Длинный запрос занимает слот на минуты, следующий по кругу падает на ту же ноду → очередь, пока другие простаивают.
  • Кэш протухает под потоком коротких запросов → следующий ход сессии пересчитывает prefill.
  • «Fairness by request count is not fairness by cost» (NVIDIA Dynamo / Ace The Cloud, 2026).

Цифры: token-weighted shortest-queue — P99 TTFT 4200 → 650 мс (6.5× лучше RR); llm-d (Tesla production) — до 3× throughput и TTFT в 2 раза ниже против RR (cache-aware routing).

Что видит балансер: 3 реплики, 12 секунд

R1: на реплику прилетает большой запрос (100K prefill + decode, 8 с) — слот занят весь отрезок, кэш сессии живёт — вытеснять его нечем. R2/R3: прилетает поток коротких запросов (1.5 с) — KV/prefix-кэш протухает между ними, и следующий ход сессии пересчитывает prefill заново.

Одна реплика «думает» минуты, на других кэш протухает — балансировать надо нагрузку и кэш, а не число запросов.

Сигналы балансировки

СигналИсточникПравило
LoadvLLM /metrics (3 с) + in-flight WLCСначала без очереди (no-wait), затем least-loaded
Sessionheader x-session-idpin: TTL 60 мин, rebind после 3 spill
PrefixMD5(system prompt)pin: TTL 5 мин, отпускается при перегрузе
Healthscrape + 5xxLIVE ⇄ PROBING ⇄ EJECTED, активный probe
  • Sticky: сессия привязана к реплике с тёплым кэшем — TTFT на турах 2+ падает в 4–5 раз (индустриальные замеры, не наш деплой). Session affinity экономит 70–90% prefill-вычислений на турах 2+ (10-туровый диалог, 8K контекст).
  • Load: забитая vLLM-реплика (waiting > 0) — вне роутинга; запросы уходят на свободные.
  • Cold start: новая сессия — на холодную реплику (least-loaded), не в чужую очередь.

Набор сигналов совпадает с индустриальной практикой — колесо не изобретали: llm-d (scoring: cache + prefill/decode + SLA + load), NVIDIA Dynamo (worker-reported KV-состояние), SGLang (router-predicted radix tree), Gateway API Inference Extension (KV cache % + queue + LoRA). «load + prefix/KV-locality + health» — де-факто стандарт 2026.

Что мы видели в референс-деплое

Наш Python-балансировщик роутит 4 реплики vLLM по этим сигналам (sticky, load, health, cold start). Подробности — в части 4 серии «Наш опыт».

Вывод

LLM-трафик — не HTTP-трафик: запросы различаются в 100× по стоимости, кэш — состояние на ноде, нагрузка bursty. Балансировать надо стоимость и локальность кэша, а не число соединений.

Смежные материалы: Балансировка LLM-трафика (часть 4 серии), Multi-tenant Serving.

← Подбор модели под железо: VRAM-бюджет Матрица моделей по размерным классам: одна задача — одна модель →