Деградация продового LLM-инференса: KV-cache, OOM и рост p99
Продовый инференс больших языковых моделей редко выходит из строя мгновенно: деградация накапливается незаметно, пока не приводит к отказам. Разбираем четыре ключевых механизма — фрагментацию KV-кэша, нехватку памяти, блокировку очереди и расхождение метрик — а также способы их диагностики.
Технологический евангелист VK Cloud Стас Погоржельский опубликовал материал, в котором детально разобрал, почему демонстрационные запуски LLM всегда выглядят быстрыми, а реальная эксплуатация через неделю начинает деградировать. По его наблюдениям, инференс редко ломается сразу и явно: обычно система постепенно ухудшает показатели, и критическое состояние обнаруживается только после инцидента. В статье выделены четыре основных источника проблем: фрагментация KV-кэша, OOM при длинном контексте, head-of-line blocking внутри батча и расхождение метрик p50 и p99. Каждый механизм сопровождается описанием внутреннего поведения системы, сценарием воспроизведения и набором метрик, которые сигнализируют о приближении сбоя, а скрипты для нагрузочного тестирования собраны в открытом репозитории-харнесе.
Ключевой тезис материала — продовый инференс упирается в память сильнее, чем в вычисления. Префилл, обрабатывающий промпт, нагружает вычислительные ядра, тогда как декодирование, генерирующее токены по одному, постоянно читает и пишет память, упираясь в её пропускную способность. На длинной дистанции деградация возникает именно из-за декодирования в сочетании с растущим KV-cache. Исследование UC Berkeley подтверждает: по мере роста контекста основной источник трафика памяти смещается с весов модели на KV-cache, и в проде последний часто превышает по объёму сами веса. На примере модели на 13B параметров на A100 40GB веса занимают около 26 Гб, оставляя примерно 14 Гб под KV-cache — при расходе около 1 Мб на токен это всего около 14 тысяч токенов на карту, что ограничивает батч до 28 запросов при длине последовательности 512 и до 7 при 2048.
Сложность добавляет архитектура внимания: у старых моделей с multi-head attention KV-cache для 70B и контекста 8K достигает примерно 20 Гб на запрос, а батч на 32 запроса требует около 640 Гб. У современных 70B-моделей вроде Llama-3 с 8 KV-головами вместо 64 потребление падает примерно в 8 раз — до 2,6 Гб на запрос при 8K и около 10 Гб при 32K. Пропускную способность сервиса определяет число последовательностей, одновременно помещающихся в память под KV-cache, а скорость самой модели вторична. Классическая реализация KV-cache резервирует непрерывный кусок памяти под максимальную длину последовательности, но большинство запросов до максимума не доживают: при генерации 100 токенов с max_len 2048 около 95% слотов простаивают. Авторы vLLM замерили на реальных нагрузках потери 60–80% памяти на фрагментацию и избыточное резервирование.
Для решения этой проблемы существует PagedAttention, который выделяет память блоками по требованию, аналогично виртуальной памяти в ОС, что снижает потери до менее чем 4% и увеличивает throughput в 2–4 раза при том же уровне латентности по сравнению с FasterTransformer и Orca, а против HuggingFace Transformers прирост достигает 24-кратного. Однако на практике paged attention скорее смещает проблему: со временем проявляется внешняя фрагментация пулов блоков и начинает сказываться механизм вытеснения. В TensorRT-LLM KV-cache организован иерархически — блок, пул (primary на GPU и secondary на CPU) и менеджер состояний (Created, Updated, Removed, Stored), и при постоянных аллокациях и вытеснениях доступная ёмкость начинает колебаться. Именно в динамике занятости пула часто скрывается первый источник деградации: код остаётся неизменным, но поведение системы со временем ухудшается. Для диагностики vLLM предоставляет метрику занятости кэша, которую следует отслеживать во времени под растущим числом уникальных префиксов, а скрипт kv_usage_probe.py из харнеса автоматизирует этот процесс.
Второй механизм — OOM при работе с длинным контекстом. Даже если суммарный объём KV-cache укладывается в лимиты, отдельные запросы с очень длинным контекстом могут исчерпать доступную память, особенно при пиковых нагрузках, когда пул блоков уже заполнен. Это приводит к аварийному завершению запроса, который ранее проходил без проблем. Третий механизм — head-of-line blocking внутри батча: если один запрос в батче генерирует очень длинный ответ или застревает на декодировании, остальные запросы вынуждены ждать, что резко увеличивает p99. Четвёртый механизм — расхождение метрик p50 и p99: средняя задержка может оставаться приемлемой, но хвостовые задержки растут из-за накопления очередей и фрагментации, и это не всегда заметно при усреднённом мониторинге.
Для российского рынка этот материал особенно актуален, поскольку многие компании активно внедряют LLM-инференс в продуктовые среды, сталкиваясь с неожиданными деградациями. Публикация VK Cloud — одна из немногих практических работ на русском, которая системно описывает проблемы эксплуатации, а не только архитектурные подходы. В отличие от общих рекомендаций по оптимизации, автор предлагает конкретные метрики и скрипты для воспроизведения проблем, что позволяет инженерам заранее выявлять узкие места. Однако открытым остаётся вопрос о применимости этих методов к другим фреймворкам, таким как TensorRT-LLM или CTranslate2, и о том, насколько универсальны пороги алертов, предложенные в харнесе.
В перспективе можно ожидать дальнейшего развития подходов к управлению KV-cache, включая дезагрегированные решения, которые VK Cloud уже описывала ранее в контексте Kubernetes. Тем не менее, как показывает практика, даже современные системы с PagedAttention требуют постоянного мониторинга и тонкой настройки под конкретную нагрузку. Инженерам стоит обратить внимание на метрики занятости кэша, динамику p99 и частоту OOM, а также регулярно прогонять нагрузочные тесты с реалистичными сценариями, включая длинные контексты и пиковые всплески. Пока же деградация продового инференса остаётся серьёзным вызовом, и материалы, подобные этому, помогают сообществу лучше понимать и предотвращать такие инциденты.