Как вы обеспечиваете низкую задержку (<500ms) для LLM?
Краткий тезис
latency|Низкая задержка LLM-сервиса достигается сочетанием нескольких техник: стриминг токенов, выбор модели меньшего размера (1B–3B вместо 70B) для простых задач, кэширование частых запросов (Redis), batching нескольких запросов в один проход, а также использование специализированных инференс-движков (vLLM) с PagedAttention и Flash Attention 2. Ключевой ориентир: комбинация стриминга, меньшей модели и кэша позволяет уложиться в 200 мс для типовых запросов.
1. Термин: Задержка (Latency)
Задержка (latency) — время от отправки запроса пользователем до получения первого или последнего токена ответа. Для LLM часто различают:
- Time to First Token (TTFT) — время до начала генерации (включает препроцессинг, KV-cache инициализацию, первый forward pass). Должен быть ≤ 100–200 мс.
- Time per Output Token (TPOT) — время генерации одного следующего токена. Для маленьких моделей (1–3B) может быть 2–5 мс/токен, для больших (70B) — 20–50 мс/токен.
Цель <500 мс обычно означает, что пользователь видит первый токен быстро, а затем получает поток токенов (стриминг), не дожидаясь полного ответа.
2. Почему низкая задержка критична
- UX Если ответ дольше 500 мс, пользователь воспринимает сервис как «тормозящий». Для чат-ботов и ассистентов каждая миллисекунда влияет на удержание.
- Real-time сценарии Голосовые ассистенты, live-транскрибация — требуют TTFT < 200 мс.
- Экономика Медленный сервис требует больше ресурсов на ожидание (keep-alive соединения, параллельные запросы). Высокая задержка может привести к таймаутам и loss запросов.
3. Streaming (потоковая передача)
Streaming — сервер отправляет каждый сгенерированный токен сразу, как только он готов, вместо того чтобы ждать полный ответ. Снижает perceived latency (воспринимаемую задержку): пользователь начинает читать через 100–300 мс, хотя полный ответ может генерироваться 2–3 секунды.
Как работает
- LLM генерирует первый токен → сразу отправляется клиенту (WebSocket, Server-Sent Events — SSE).
- Последующие токены отправляются по мере генерации.
- Клиент отображает их инкрементально.
Реализация на Python (FastAPI + vLLM):
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from vllm import LLM, SamplingParams
app = FastAPI()
llm = LLM(model="meta-llama/Llama-3.2-3B-Instruct")
async def stream_response(prompt: str):
sampling_params = SamplingParams(max_tokens=256, stream=True)
async for output in llm.generate_async(prompt, sampling_params):
yield output.text
@app.post("/chat")
async def chat(prompt: str):
return StreamingResponse(stream_response(prompt), media_type="text/plain")
Streaming обязателен для любого production-сервиса LLM, если не требуется полный ответ сразу (например, для дальнейшей обработки).
4. Выбор меньшей модели
Меньшая модель (1B–3B параметров) даёт задержку в 5–10 раз ниже, чем модель 70B, при близком качестве на простых задачах (суммаризация, фактологический ответ, извлечение).
| Размер модели | Примеры | TTFT (1 токен) | TPOT | Latency 100 токенов |
|---|---|---|---|---|
| 1-3B | TinyLlama, Qwen2.5-1.5B, Phi-3.5-mini | 30-80 мс | 2-5 мс | ~250-600 мс |
| 7-8B | Llama 3.1-8B, Mistral 7B | 80-200 мс | 5-12 мс | 600-1400 мс |
| 70B+ | Llama 3.1-70B, DeepSeek-V3 | 300-1000 мс | 20-50 мс | 2-6 с |
Когда использовать меньшие модели
- Специализированная задача (только классификация, только суммаризация).
- Большинство пользовательских запросов не требует «интеллекта» гигантской модели.
- Допустимо использовать router (маршрутизатор) — для сложных запросов отправлять на 70B, для простых — на 3B.
Практический паттерн
if complexity_score(prompt) < 0.7:
model = "tinyllama-1.1b"
else:
model = "llama-70b"
5. Batch inference (пакетный инференс)
Batch inference — объединение нескольких независимых запросов в один forward pass на GPU. GPU лучше утилизирована при обработке батча, что уменьшает задержку на один запрос за счёт амортизации накладных расходов.
- Dynamic batching: сервер собирает запросы в течение небольшого таймаута (например, 50 мс) и отправляет батч на инференс.
- Continuous batching (vLLM): позволяет добавлять новые запросы и завершать старые во время прохода — ещё более эффективная загрузка.
Задержка при батче TTFT для каждого запроса в батче увеличивается незначительно (в пределах 10-20%), а пропускная способность (throughput) растёт в 2-4 раза. Для низкой задержки критичен маленький батч (до 4-8 запросов) — большее замедлит TTFT.
6. Кэширование (Redis)
Кэширование — хранение результатов частых (шаблонных) запросов в быстром хранилище, например Redis in-memory database. Позволяет вернуть ответ за 1-5 мс без вызова LLM.
Какие запросы кэшировать
- Повторяющиеся вопросы (FAQ, справка).
- Запросы с одинаковыми контекстами (системные промпты, приветствия).
- Результаты, которые одинаковы для многих пользователей (документация, описание продуктов).
Реализация
import hashlib, redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def get_cached_response(prompt: str) -> str | None:
key = hashlib.sha256(prompt.encode()).hexdigest()
return r.get(f"llm_cache:{key}")
def set_cached_response(prompt: str, response: str, ttl=3600):
key = hashlib.sha256(prompt.encode()).hexdigest()
r.setex(f"llm_cache:{key}", ttl, response)
Политика ключей TTL (time-to-live) — 15-60 минут; кэшировать только для статических промптов; если запрос содержит персонализированные данные (имя пользователя) — не кэшировать.
7. Специфика железа и оптимизация инференса
7.1 vLLM — Production-инференс-движок
vLLM — библиотека для быстрого инференса LLM. Основные фичи для снижения задержки:
- PagedAttention — эффективное управление KV-кэшем (деление на страницы). Снижает фрагментацию памяти, позволяет поддерживать большие батчи и уменьшает паузы на освобождение памяти.
- Flash Attention 2 — техника, ускоряющая attention в 2-3 раза за счёт лучшего использования кэша GPU и избежания записи промежуточных матриц.
- Continuous batching — динамическое добавление/завершение запросов без остановки.
Результат на одной GPU (A100 80GB) vLLM с Llama 3.1-8B даёт TTFT 80 мс и TPOT 6-8 мс, что укладывается в 500 мс для 50-токенового ответа.
7.2 Quantization (квантование)
Quantization — снижение точности весов модели (например, FP16 → INT8, INT4). Уменьшает размер модели и время forward pass.
| Тип квантования | Размер (на параметр) | Скорость (token/s) | Потеря качества |
|---|---|---|---|
| FP16 | 2 байта | 100% (база) | 0% |
| INT8 (GPTQ) | 1 байт | ~120-150% | ~0.5% |
| INT4 (AWQ) | 0.5 байта | ~150-200% | ~1-2% |
Для low-latency часто используют INT4 (AWQ) — модель помещается в VRAM с запасом, скорость выше, а качество приемлемо.
7.3 Speculative Decoding (спекулятивное декодирование)
Speculative decoding — приём, когда маленькая «драфт-модель» (например, 0.5B) генерирует несколько токенов, а большая модель проверяет их. Если драфт-модель верна, принимается сразу несколько токенов за один forward pass большой модели. Это ускоряет генерацию в 2-3 раза без изменения качества.
8. Асинхронный стек (FastAPI + asyncio + очередь)
Асинхронный стек (FastAPI + asyncio + очередь задач) позволяет не блокировать I/O и эффективно управлять соединениями.
- FastAPI — асинхронный фреймворк, способный обрабатывать тысячи одновременных соединений.
- asyncio — библиотека для конкурентного выполнения. LLM-инференс сам может быть CPU/GPU-интенсивным, но асинхронные паузы (загрузка данных, отправка ответов) не блокируют ядра.
- Очередь задач (например, Celery или Redis Queue) — когда запросы приходят быстрее, чем их может обработать GPU, ставим их в очередь и обрабатываем с приоритетами. Для low-latency лучше использовать встроенную очередь с asyncio (asyncio.Queue), чтобы не добавлять сетевые задержки.
Примерная архитектура
Client → FastAPI → asyncio.Queue → Worker (vLLM) → StreamingResponse
Worker запускается в отдельном процессе/потоке с одной GPU. FastAPI принимает запросы, кладёт в очередь, worker забирает и генерирует стрим.
9. Дополнительные техники
- Prompt caching — кэширование KV-кэша для одинаковых префиксов (system prompt, несколько первых токенов). vLLM поддерживает KV-cache sharing между запросами.
- Prefix caching — если system prompt не меняется, вычисляем его в начале сессии и переиспользуем.
- Input compression — сокращение длины промпта: удаление стоп-слов, перефразирование короткими фразами (с помощью LLM меньшего размера).
- Edge deployment — маленькие модели (1B) могут работать на CPU с ONNX Runtime или на мобильных чипах, убирая задержки сети.
Пет-проект для закрепления
Задача построить микросервис чат-бота на LLM с гарантированной задержкой < 500 мс для 95% запросов.
Инструменты Python, FastAPI, vLLM, Redis, Locust (для нагрузочного тестирования), Docker.
Шаги:
- Установить vLLM, загрузить модель Qwen2.5-1.5B (INT4 AWQ).
- Написать FastAPI-приложение с эндпоинтом
/chat, который принимает prompt и возвращает токены через SSE. - Внедрить кэш Redis: перед генерацией проверять хеш промпта, при находке отдавать кэш.
- Настроить continuous batching в vLLM (параметр
--max-num-batched-tokens= 4096,--max-num-seqs= 32). - Запустить Locust для стресс-теста: 50 одновременных пользователей, случайные промпты (50-200 символов). Измерить TTFT, TPOT, полное время ответа.
- Оптимизировать: если latency > 500 мс для части запросов — уменьшить max_num_seqs, увеличить размер батча, добавить speculative decoding (если поддерживается).
Ожидаемый результат
- TTFT: < 100 мс
- TPOT: < 3 мс/токен
- 95-й перцентиль полного времени ответа (на 200 токенов): < 500 мс
- Пропускная способность: > 100 requests/min на одной A100.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 61 | Как вы деплоите LLM в production? |
| 62 | Что такое model serving и какие есть инструменты? |
| 63 | Как вы реализуете batch inference для LLM? |
| 65 | Как вы тестируете latency и throughput LLM-сервиса? |
| 66 | Какие стратегии кэширования вы используете для LLM? |
| 67 | Что такое квантование моделей и как оно влияет на latency? |
Навигация
- Предыдущий: 63
- Следующий: 65
- Индекс: 00. Индекс разборов