Проклятие TTFT: когда очередь съедает время жизни пользователя
Знаете, что бесит больше всего, когда ваша LLM под нагрузкой? Не то, что она медленно генерирует ответ целиком, а то, что первый токен приходит через вечность. Пользователь нажал «Отправить» — и видит спиннер. Через пять секунд он ушёл к конкуренту. Time-to-First-Token — это не метрика, это приговор для UX. Особенно под пиковой нагрузкой, когда очередь запросов растёт, а GPU начинает захлёбываться.
Continuous batching в vLLM спасает throughput, но не даёт волшебной пилюли для TTFT. Когда все слоты заняты, новый запрос ждёт завершения хотя бы одного префилла. И чем длиннее системный промпт и больше max_tokens у текущих запросов — тем дольше ждать.
Типичная ситуация: вы задеплоили Qwen 2.5-72B с sys_prompt в 4K токенов и max_tokens=4096. Под нагрузкой 100 RPS средний TTFT взлетает до 12 секунд. Паника, роллбэки, тикет «всё пропало».
Лень как добродетель: что такое динамическая лень?
Решение до безобразия простое и больное: если серверу тяжело, дайте ему поблажку. Сократите max_tokens для новых запросов, урежьте системный промпт до минимума. Да, качество ответа может чуть просесть, но пользователь получит первый токен за 2 секунды, а не за 12. Это и есть динамическая лень — сознательное снижение «качества» ради сохранения отзывчивости.
Вручную подкручивать параметры под нагрузкой — занятие для мазохистов. Нужен автомат. И тут на сцену выходит LazyGate — open-source ASGI-прокси, который анализирует состояние vLLM и на лету подменяет параметры запроса.
Заглянем под капот LazyGate
LazyGate — это шлюз на FastAPI, который сидит между клиентом и vLLM. Он перехватывает /v1/chat/completions, проверяет метрики, принимает решение о «лени», модифицирует тело запроса и отправляет дальше. Всё это с минимальным оверхедом — обычно менее 1 мс.
У него три состояния:
- Normal — живи как обычно, никаких изменений.
- Degraded — нагрузка растёт. Урезаем max_tokens на 30%, системный промпт заменяем на сокращённую версию (если настроено).
- Emergency — всё плохо. max_tokens падает до 256 (или другого минимума), промпт выкидывается вообще, остаётся только «You are a helpful assistant».
Переходы между состояниями происходят на основе метрик. По умолчанию LazyGate опрашивает /metrics vLLM и смотрит на vllm:num_requests_waiting и gpu_cache_usage_perc. Можно добавить свои — через плагины.
Кстати, если вы ещё не решили, какой движок использовать, прочитайте наше сравнение vLLM и SGLang. LazyGate заточен именно под vLLM, хотя при желании его можно натянуть на SGLang через OpenTelemetry-метрики.
Руки в код: настройка LazyGate
Переходим к самому вкусному. Предположим, у вас уже есть запущенный инстанс vLLM (версия 0.8.x или новее). Если нет — почитайте гайд по масштабированию для понимания, сколько GPU вам нужно.
1 Установка LazyGate
Проще всего через pip:
pip install lazygate
# или через Docker:
docker pull lazygate/lazygate:latest
docker run -p 8080:8080 lazygate/lazygate:latest
Рекомендую Docker — проще управлять конфигом и версиями.
2 Базовая конфигурация
Создайте файл lazygate.yaml:
# lazygate.yaml
upstream:
url: "http://vllm:8000"
health_endpoint: "/health"
states:
normal:
max_tokens_factor: 1.0 # не трогаем max_tokens
prompt_template: null # используем оригинальный системный промпт
conditions:
waiting_requests: < 5
degraded:
max_tokens_factor: 0.7 # сокращаем на 30%
prompt_template: "Ты помощник. Отвечай коротко и по делу."
conditions:
waiting_requests: >= 5
waiting_requests: < 20
emergency:
max_tokens_factor: 0.1 # грубо, но эффективно
prompt_template: "You are a helpful assistant."
conditions:
waiting_requests: >= 20
metrics:
source: "vllm" # или "otel"
polling_interval: 2 # секунды
server:
host: "0.0.0.0"
port: 8080
max_tokens_factor умножается на оригинальный max_tokens из запроса клиента. Если клиент запросил 4096, в degraded станет 2867, в emergency — 409. prompt_template полностью заменяет содержимое системного сообщения. Оригинальный промпт игнорируется.
Важно! Замена системного промпта может кардинально изменить поведение модели. Обязательно протестируйте на синтетической нагрузке, прежде чем вкатывать в прод. Почитайте про баги с контекстом, чтобы не попасть впросак.
3 Интеграция с vLLM
Теперь направьте клиентские запросы на LazyGate вместо vLLM. Если ваш клиент уже стучится на порт 8000, просто перенаправьте DNS или используйте reverse proxy. LazyGate сам форвардит запросы и возвращает ответы потоково (streaming тоже поддерживается).
Проверьте метрики: откройте http://localhost:8080/metrics — LazyGate отдаёт свои метрики и проксирует метрики vLLM.
4 Мониторинг и калибровка порогов
Запустите нагрузочное тестирование. Например, с помощью locust или bombardier. Смотрите на TTFT при разных значениях waiting_requests.
Для сбора метрик и алертов рекомендую использовать наш партнёрский сервис мониторинга GPU — он из коробки показывает TTFT, состояние очереди и загрузку кэша. Но можно обойтись и связкой Prometheus + Grafana.
Подберите пороги так, чтобы emergency включался не раньше, чем загрузка кэша превысит 90%. Иначе вы будете резать качество на пустом месте. Подробнее про кэш и его оптимизацию — в статье про KV-offloading.
Грабли, на которые я наступил
Хорошая идея, но есть нюансы. Вот три самые частые ошибки:
- Резкое переключение состояний. Если между normal и degraded порог в 1 запрос, состояние будет дёргаться каждую секунду. Добавьте гистерезис: например, для выхода из degraded требуется
waiting_requests < 3, а для входа —>= 5. - Замена промпта ухудшает качество сильнее, чем кажется. Особенно для моделей, которые полагаются на длинный system prompt для следования инструкциям. Протестируйте на метриках качества (Rouge, BERTScore).
- Забыли про streaming. LazyGate поддерживает SSE, но если клиент не обрабатывает чанки, то первый токен всё равно придёт только после полной генерации. Убедитесь, что клиент умеет читать поток.
Кстати, если у вас уже настроен continuous batching, динамическая лень с ним отлично сочетается. Подробнее о механизме — читайте в этой статье.
Вопросы, которые вы постесняетесь задать
Можно ли использовать LazyGate с SGLang или TensorRT-LLM?
Да, если они отдают метрики через OpenTelemetry или Prometheus. LazyGate умеет читать кастомные метрики через плагины. Но из коробки — только vLLM.
Как проверить, что LazyGate вообще работает?
Посмотрите на заголовок X-Lazy-State в ответе — он показывает текущее состояние (normal/degraded/emergency).
Что делать, если после настройки TTFT не уменьшился?
Вероятно, пороги States не совпали с реальной загрузкой. Увеличьте polling_interval до 1 секунды и проверьте, попадают ли метрики в нужные диапазоны. Или проблема в самом vLLM — посмотрите, не перегружен ли KV-кэш (оффлоадинг может помочь).
Не повлияет ли сокращение max_tokens на throughput?
Парадоксально, но может даже повысить: меньше токенов на генерацию — быстрее освобождаются слоты. Но за счёт качества ответа. Экспериментируйте.
Динамическая лень — не серебряная пуля, но в ситуациях, когда на первом месте отзывчивость, а не идеальный ответ, LazyGate реально выручает. Попробуйте на нагрузочном классе, настройте пороги под свою модель — и TTFT перестанет быть вашей головной болью.
Если у вас остались вопросы — пишите в комментариях. А если уже внедрили LazyGate, расскажите, какие пороги сработали лучше всего. Мой опыт: для Mistral Large 2 emergency при waiting_requests > 10 с max_tokens_factor 0.15 даёт TTFT ниже 2 секунд даже при 50 RPS.