Я смотрю на параметры запуска нашего инференс-сервера: флаг gpu_memory_utilization выставлен в 0.95, max_model_len зафиксирован на 8192 токенах, а в качестве движка крутится vLLM. Абсолютно стандартная картина для тех, кто только выводит локальные LLM в продакшен. И абсолютно мертвая конфигурация для тех условий, в которых мы работаем. В Morana Labs мы разворачиваем индустриальный ИИ на железе клиента, на edge-серверах в закрытых контурах без доступа к облачным API. Там нет возможности динамически докинуть еще десяток A100, когда трафик неожиданно прыгнет. И именно на таком объекте прошлой осенью мы усвоили жесткий урок. Ситуация, когда KV-cache съедает всю память GPU: как считать и тюнить под нагрузкой, чтобы не словить OOM, стала для нас не темой для отвлеченных дискуссий, а буквальным логом посмертного анализа рухнувшей системы.
Сервер прекрасно держал все синтетические тесты во время приемки. Но стоило начаться реальной пересменке на заводе, как двадцать инженеров практически одновременно загрузили в систему логи с контекстом под четыре тысячи токенов каждый. Latency улетел в стратосферу, время до первого токена пробило пятнадцать секунд, а затем консоль выплюнула классический CUDA Out Of Memory. Почему это произошло? Потому что мы на секунду доверились автоматике движка, вместо того чтобы взять калькулятор. Формула объема KV-кэша безжалостна: количество слоев умножить на количество голов внимания, умножить на размерность головы, умножить на два для ключей и значений, умножить на длину последовательности, размер батча и количество байт на параметр.
Берем Llama-3-8B. Тридцать два слоя, восемь KV-голов, размерность сто двадцать восемь. Умножаем это на два для ключей и значений, получаем шестьдесят пять тысяч пятьсот тридцать шесть параметров на каждый токен. В формате FP16 это сто двадцать восемь килобайт. Умножаем на хардкап контекста в 8192 токена и батч из двадцати одновременных запросов. Получаем больше двадцати гигабайт чистой видеопамяти только под кэш. А теперь вспоминаем, что сами веса модели занимают пятнадцать гигабайт, а мы сидим на одной карточке с двадцатью четырьмя гигабайтами на борту. Математика не сходилась изначально, это была бомба замедленного действия, заложенная в конфигурацию.
Вы думаете, можно просто покрутить настройки vLLM и физика отступит? Как бы не так. Параметр gpu_memory_utilization лишь говорит движку, сколько памяти зарезервировать на старте. Если поставить его слишком низким, модель даже не начнет собирать батчи. Если выкрутить в потолок, вы оставите слишком мало места для активаций и получите OOM на этапе prefill. Настоящая битва разворачивается между max_num_seqs и max_num_batched_tokens. Это ваш газ и тормоз в мире инференса.
Когда KV-cache съедает всю память GPU: как считать и тюнить под нагрузкой, чтобы не словить OOM
Маркетинг приучил всех верить в магию PagedAttention и prefix caching. Будем честны: PagedAttention не создает память из воздуха. Он гениально решает проблему фрагментации, динамически нарезая кэш на блоки, но если сырой объем вашего кэша превышает физический лимит кремния, PagedAttention просто очень аккуратно и без фрагментации уложит ваши данные прямо перед падением с ошибкой OOM. А кэширование префиксов? Отличная архитектурная находка, если у вас огромный общий системный промпт. В нашем случае каждый инженер кидал в модель уникальные куски телеметрии с оборудования. Процент попадания в префиксный кэш болтался около нуля, добавляя лишь вычислительный оверхед на проверку хэшей.
Мы начали жестко резать max_num_seqs, ограничивая максимальное количество одновременных запросов. Что происходит в этот момент? Вы спасаете память, но убиваете пропускную способность. Если позволить движку агрессивно батчить все подряд, время до первого токена, тот самый TTFT, улетает за пределы человеческого терпения. Пользователь думает, что всё зависло, обновляет страницу, и тем самым отправляет еще один запрос, удваивая нагрузку на вашу и без того задыхающуюся видеокарту. Это классический трейд-офф: агрессивный батчинг накручивает красивый throughput в отчетах, но рвет tail latency в клочья. Клиенту абсолютно плевать на среднюю пропускную способность системы, если его конкретный запрос висит десять секунд.
Квантизация кэша и цена компромисса в MLOps
От отчаяния мы пошли на радикальный шаг — включили квантизацию KV-кэша в FP8. Размер потребляемой памяти схлопнулся вдвое. Тесты снова загорелись зеленым, запросы полетели. Победа? Черта с два. Через день мы заметили, что модель начала галлюцинировать. Она путала конкретные значения давления в логах, если эти цифры находились в самом начале длинного контекста. Деградация точности при квантизации кэша — это не миф и не академическая погрешность. Когда языковой модели нужно извлечь точную числовую строку из глубины в шесть тысяч токенов, снижение точности представления ключей ломает механизм внимания. Мы чуть было не променяли шумное падение инфраструктуры на тихую, но фатальную поломку бизнес-логики.
Из этого пике мы выходили через жесткий пересмотр SLA с бизнесом. Мы не могли безопасно переваривать контексты по 8K для такого количества юзеров на имеющемся железе без потери качества. Пришлось урезать max_model_len в два раза и жестко лимитировать конкурентность на уровне балансировщика, а не только внутри vLLM. Да, длинные промпты пошли под нож. Да, часть запросов в пике стала отбиваться HTTP-статусом 429 Too Many Requests. Но это предсказуемое поведение системы. В production-ML инженерия заканчивается там, где начинается надежда на авось. Вы либо считаете емкость до байта и тюните сервинг под худший сценарий, либо готовитесь ночью поднимать упавшие контейнеры.