Запустили LLM в продакшен - первые дни эйфории. Потом пользователи жалуются на долгий ответ. Вы смотрите в мониторинг, а TTFT (Time to First Token) вырос с 100 мс до 3 секунд. GPU жрет память как не в себя, а KV-кеш разрастается до десятков гигабайт. Знакомо?
В этой статье я расскажу, почему это происходит именно в 2026 году, когда модели стали еще больше, и как с этим бороться. Никакой воды - только реальные кейсы из продакшена Яндекса, vLLM и llama.cpp.
TTFT и KV-кеш: два врага хорошего UX
TTFT - это время до первого токена. Пока пользователь ждет первую букву ответа, модель:
- Догружает веса (если не в памяти).
- Вычисляет префилл (prefill) - обрабатывает весь промпт.
- Генерирует первый токен.
KV-кеш (Key-Value cache) - это память, в которой трансформер хранит ключи и значения внимания всех предыдущих токенов. Без него каждый шаг генерации пересчитывал бы весь контекст заново. Но у KV-кеша есть темная сторона: он растет линейно с длиной последовательности и квадратично по памяти.
В 2026 году LLM с длиной контекста 128K токенов уже не редкость. Умножаем на 80 слоев, 32 головы внимания, 2 байта на параметр - получаем десятки гигабайт на один запрос. А если запросов 100 одновременно? Привет, OOM.
Почему TTFT убивает производительность (и как это исправить)
Возьмем недавний пример из статьи про Kimi-K2.5 на vLLM. Модель с 105B параметров, контекст 16K токенов. Без оптимизации префилла TTFT взлетает до 10 секунд на одном GPU A100. Пациент скорее мертв, чем жив.
Как лечим:
1 Чанкированный префилл (Chunked Prefill)
Делим промпт на чанки по 512-1024 токена. Каждый чанк обрабатывается как отдельный запрос, но KV-кеш накапливается. Это позволяет перемежать префилл и декодинг, снижая пиковую нагрузку на память. vLLM это поддерживает из коробки:
# Конфиг для vLLM с chunked prefill
from vllm import LLM, SamplingParams
llm = LLM(
model="Qwen/Qwen3-32B-Instruct",
max_num_batched_tokens=8192,
max_num_seqs=256,
enable_chunked_prefill=True, # включает чанкирование
max_paddings=256
)
Результат: TTFT падает до 1.5 секунд при той же пропускной способности (смотрите бенчмарки Qwen3-32B).
2 Continuous Batching с динамическим планированием
В старых батчерных подходах вы ждали заполнения батча, потом обрабатывали все сразу. Continuous batching добавляет новые запросы в текущий батч по мере освобождения слотов. Планировщик GPU решает, какие последовательности выгружать, какие оставить в кеше.
Ключевой параметр - gpu_memory_utilization. Не ставьте 0.95 сразу. Начните с 0.85 и увеличивайте шагами по 0.02, пока не упретесь в OOM.
3 Splitwise – разделение префилла и декодинга
Идея: префилл (вычислительно тяжелый) выполняется на одних GPU, а декодинг (память-интенсивный) на других. Это позволяет подбирать конфигурацию под каждый этап. Продвинутый вариант - P/D separation. Пока поддерживается не всеми рантаймами, но vLLM уже экспериментирует.
KV-кеш: жадный пожиратель памяти
KV-кеш - главный источник проблем в длинных сессиях. После 10K токенов даже 7B модель может занять 8 ГБ VRAM под кеш на одну беседу. А если у вас многопользовательский чат?
Вот что можно сделать:
1 PagedAttention и vLLM
vLLM использует PagedAttention - разбивает KV-кеш на блоки фиксированного размера (page). Это устраняет фрагментацию памяти и позволяет использовать Virtual Memory на GPU. В статье про KV-кеш мы подробно разбирали, почему трансформеры так жульничают с памятью.
Настройка:
# Оптимизация страничного кеша
llm = LLM(
model="Qwen/Qwen3-32B-Instruct",
max_model_len=32768,
block_size=16, # размер страницы в токенах
swap_space=4, # GB для CPU swap
gpu_memory_utilization=0.88
)
2 Квантизация KV-кеша до 4 бит
Техника Delta-KV для llama.cpp позволяет сжимать KV-кеш до 4 бит почти без потерь на Llama 70B. Суть: хранить не точные значения, а их дельту относительно базового квантованного значения. Восстановление - на лету, с минимальным overhead.
В vLLM есть встроенная квантизация KV-кеша через kv_cache_dtype:
# Включаем 4-битный KV-кеш
llm = LLM(
model="Qwen/Qwen3-32B-Instruct",
kv_cache_dtype="fp8_e4m3", # или "fp8_e5m2"
# для 4-битного: kv_cache_dtype="int4"
)
Проверьте перплексию в своем сценарии. На моих тестах падение качества менее 0.5% при сжатии в 2 раза.
3 KV-оффлоадинг на CPU или NVMe
Когда GPU памяти не хватает, выгружайте старый KV-кеш на CPU или SSD. В статье про Hybrid KV Cache Manager на примере MiniMax-M2.5 показано, как настроить гибридное хранение: горячие страницы на GPU, холодные - на CPU или NVMe.
Важно: скорость оффлоадинга должна быть выше скорости генерации, иначе TTFT начнет расти. Используйте параллельную пересылку стримов.
4 Ветвящийся кеш (RadixAttention)
Если много запросов имеют общий префикс (system prompt, контекст диалога, RAG-документы), можно кешировать KV-кеш для этого префикса и переиспользовать. SGLang это умеет через RadixAttention. В vLLM реализована похожая фича - prefix_caching.
# Включаем кеширование префиксов
llm = LLM(
model="Qwen/Qwen3-32B-Instruct",
enable_prefix_caching=True
)
Эффект: при повторяющихся префиксах TTFT снижается на 90% для длинных контекстов.
Как НЕ надо делать: типичные грабли
Ошибка №1: Включить все оптимизации сразу и надеяться на магию. Начните с мониторинга. 5 ключевых метрик для стабильной работы self-hosted LLM - вот что нужно отслеживать в первую очередь.
Ошибка №2: Игнорировать температурный режим GPU. При перегреве частота падает, TTFT ползет вверх. Следите за clock speed в NVIDIA SMI.
Ошибка №3: Сжимать KV-кеш до 2 бит на чувствительных задачах. Расследование про Nemotron 3 Super показало, что разные рантаймы дают разное качество при квантизации - не копируйте настройки вслепую.
Ошибка №4: Держать все сессии в VRAM. Если пользователь не активен 30 секунд - выгружайте его KV-кеш. Проблема повторной обработки больших промптов в llama.cpp решается именно через политику вытеснения.
Пошаговый план настройки продакшена (checklist)
- Замерьте базовый TTFT на своей нагрузке. Используйте
prometheus+grafanaдля сбора метрик. - Включите continuous batching и установите
gpu_memory_utilization = 0.85. - Настройте PagedAttention: подберите
block_size(16 для долгих сессий, 32 для коротких). - Включите квантизацию KV-кеша - начните с FP8, проверьте перплексию.
- Добавьте кеш префиксов (prefix caching).
- Настройте оффлоадинг для холодных страниц.
- Оптимизируйте префилл: включите chunked prefill, если контекст > 4K.
- Мониторьте TTFT и memory usage - установите алерты на порог 2 секунды.
Если вы используете Yandex AI Studio, многие из этих оптимизаций уже реализованы на уровне платформы. Но если хотите полный контроль - берите vLLM или llama.cpp.
Что дальше? Неочевидный совет
В 2026 году появился интересный тренд - алгоритмы с сублинейной памятью (например, Mamba, RWKV, Hyena). Они не используют KV-кеш, а значит - не имеют проблем с его размером. Но их качество пока уступает трансформерам на сложных задачах.
Мой совет: не гонитесь за новыми архитектурами слепо. Лучше вложитесь в грамотную инфраструктуру для трансформеров - правильный планировщик GPU, аллокатор памяти и стриминг вывода. Одно только внедрение оптимизированного Top-K на CPU может ускорить семплинг в 20 раз. А если объединить все методы - пользователь не заметит, что за LLM стоит целый кластер.
FAQ по TTFT и KV-кешу
? Что делать, если TTFT растет со временем работы сервера?
Скорее всего, происходит утечка памяти KV-кеша. Проверьте, не кешируете ли вы старые сессии без TTL. Включите eviction policy (LRU) и поставьте лимит на количество активных последовательностей.
? Какой размер блока (block_size) выбрать для PagedAttention?
16 токенов - универсальный вариант. Если контексты короткие (до 2K) - ставьте 8, если длинные (64K+) - 32. Экспериментируйте с помощью A/B тестов.
? Нужен ли KV-оффлоадинг, если у меня H100 с 80 ГБ?
Зависит от модели. 70B модель с контекстом 32K уже занимает ~50 ГБ под веса + ~16 ГБ под KV-кеш на одну сессию. Если сессий больше 3 - оффлоадинг обязателен.
Подписывайтесь на блог, чтобы не пропустить разбор новых техник. В следующей статье расскажу, как мы объединили chunked prefill с custom attention kernel для ускорения в 3 раза на инференсе Mistral Large 3.