Оптимизация инференса LLM: шесть шагов к снижению стоимости GPU без замены оборудования
Высокая загрузка видеокарт в продакшене часто маскирует реальные проблемы эффективности. Повторный расчёт одинаковых промптов, неэффективный кэш и неправильные параметры сервинга могут сводить на нет преимущества новой архитектуры. На примере стека vLLM и Kubernetes рассмотрена методика оптимизации инференса больших языковых моделей: от диагностики узких мест до внедрения префиксного кэширования. Цель — снижение стоимости GPU-часов и улучшение SLA без закупки дополнительного железа.
# Почему инференс LLM становится дорогим и как снизить расходы на GPU
В продакшене сложная картина: команды за вечер успешно разворачивают открытые модели на vLLM, демо проходит блестяще, но через месяц финансовая служба сообщает, что аренда GPU обходится как зарплата трёх разработчиков. Пользователи ждут ответы по 15–20 секунд. Инженеры открывают nvidia-smi и видят утилизацию на 100%. Вывод логичен: нужны новые карты.
Однако часто проблема не в количестве графических ускорителей, а в том, что инфраструктура вынуждена пересчитывать одни и те же длинные запросы много раз. Ниже представлен план из шести шагов, который позволяет найти узкие места, убрать дублирующую работу и доказать эффект цифрами.
*Важно:* Эта оптимизация не требует смены чекпойнтов моделей или закупки нового парка карт. Меняются только конфигурация сервинга и точность весов.
1. Базовая линия и поиск узкого места
Прежде чем оптимизировать, необходимо снять базовую линию. Оптимизировать вслепую невозможно и опасно для бюджета.
Главная экономическая метрика — GPU-часы на миллион выходных токенов при соблюдении SLA. Считается она так: все сгенерированные токены делятся на все GPU-часы за интервал. Перевод в деньги производится через внутреннюю ставку часа GPU (амортизация, электричество и т.д.).
nvidia-smi часто вводит в заблуждение. Показатель GPU-Util демонстрирует долю времени, когда на карте выполнялось хоть какое-то вычисление, но не показывает, насколько карта загружена полезной работой. Для оценки насыщения эффективнее использовать метрики профилирования DCGM: * DCGM_FI_PROF_DRAM_ACTIVE — загрузка памяти. * DCGM_FI_PROF_SM_ACTIVE — активность вычислительных блоков.
Если после оптимизации улучшится только время до первого токена (TTFT), а пропускная способность не вырастет, токен не подешевеет. Экономия начинается, когда освобождённое время GPU позволяет обслужить больше запросов.
Быстрый индикатор: Отношение входных к выходным токенам. Если на один выходной приходится десятки входных, а hit rate кэша низок, деньги утекают на повторный *prefill*. Подтверждается это анализом фазовых метрик vLLM: если доминирует время *prefill*, помогут шаги с кэшированием. Если доминирует *decode*, нагрузка упирается в генерацию, и нужно работать с памятью и батчингом.
2. Расчёт ёмкости KV-кэша
KV-кэш растёт линейно с длиной последовательности и количеством одновременных запросов. Для Llama 3 70B на токен приходится около 320 Кб кэша. Последовательность в 4096 токенов занимает примерно 1,25 ГБ памяти.
Важно корректно интерпретировать gpu-memory-utilization. Это бюджет памяти, выделяемый экземпляру vLLM целиком (под веса, активации, служебные структуры и KV-кэш), а не доля памяти, занятая исключительно кэшем. Скрипт оценки должен учитывать количество слоев, голову внимания и тип квантования.
Если ёмкость заполнена, а запаса нет, не стоит спешить с покупкой карт. Сначала необходимо настроить движок под свою нагрузку и реализовать кэширование.
3. Настройка движка vLLM под нагрузку
Дефолтные настройки часто съедают память впустую. В vLLM необходимо настроить следующие параметры в соответствии с реальным трафиком:
* Continuous batching: Новые запросы встают в батч, как только освобождаются ресурсы. Это повышает утилизацию GPU. * `--tensor-parallel-size`: Для H100 связка на одной ноде (NVLink) позволяет использовать оба GPU для одной модели. * Квантование весов (`--quantization=fp8`): Онлайн-квантование BF16 -> FP8 вдвое сокращает память под веса, освобождая место для KV-кэша и конкурентности. Это не влияет на качество генерации при правильных настройках. * KV-кэш (`--kv-cache-dtype=fp8`): Использование FP8 для кэша вдвое увеличивает его ёмкость по сравнению с FP16. * `--max-model-len`: Лимит должен соответствовать реальному контракту API, а не максимуму контекстного окна модели. Избыточный лимит забивает кэш. * Параметры планировщика (`--max-num-seqs`, `--max-num-batched-tokens`): Их нужно подбирать нагрузочным тестом, реагируя на симптомы (рост очереди, рост времени на токен).
4. Префиксное кэширование (Prefix Caching)
Prefix caching переиспользует KV-блоки только для совпадающего начала последовательности токенов, ускоряя этап *prefill*. Эффективность критически зависит от того, насколько стабильно выглядит начало запроса.
Частая ошибка: Включение кэша без изменения логики формирования промпта. Если в системный промпт автоматически вставляется текущая дата и время, префикс меняется каждую минуту, и кэш не работает. Всё, что одинаково для всех пользователей, должно стоять в начале. Всё, что уникально (имя пользователя, дата), — в конце.
Безопасность: Кэширование между разными пользователями может создавать канал утечки информации. В vLLM это решается параметром запроса cache_salt, который изолирует кэш для разных тенантов.
5. Балансировщик и маршрутизация
Обычный Kubernetes Service распределяет запросы случайно, не зная о содержимом кэшей конкретной реплики. Запрос с тем же префиксом может попасть на под, который его не видел, что приводит к двойному расчёту.
Для кластеров важно использовать специализированные решения для маршрутизации (например, llm-d в режиме precise prefix-cache routing), которые знают, где лежит нужный фрагмент кэша, и направляют туда новый запрос. Это позволяет избежать повторной генерации в распределённых системах.
Заключение
Оптимизация инференса — это замкнутый контур: замер — изменение — повторный замер. Без него любая оптимизация превращается в спор мнений. Начинать нужно с базовых настроек памяти и планировщика, а уже затем переходить к сложным техникам вроде кэширования и смены балансировщика. Только так можно обосновать необходимость новых карт или доказать, что текущая инфраструктура способна справляться с нагрузкой при правильной настройке.