Когда запросы к большой языковой модели приходят несколько раз в день, GPU простаивает, но оплачивать ноду приходится круглосуточно. Естественное желание — освобождать карту между запросами: удалять поды с моделью, когда нагрузки нет, и поднимать их снова по требованию. Для обычного веб-приложения такая схема работает с задержкой в несколько секунд. Для LLM всё сложнее: после запуска пода нужно загрузить веса и прогреть движок, поэтому первый запрос может ждать ответа минутами. В июле команда H Company сообщила, что новая реплика их крупнейших моделей выходила в готовность до 27 минут. Они используют vLLM на GPU-нодах Kubernetes и добавляют реплики по мере роста нагрузки от агентов. Автоскейлер быстро увеличивает число подов, но дальше начинаются скачивание образа, загрузка весов, компиляция и прогрев движка — и эти этапы настройками масштабирования не ускорить.
Я ожидал, что основное время уходит на загрузку весов, и решил посмотреть публичные замеры. У Tensorfuse запуск Llama 3.1 8B на L40S занял 294 секунды, из которых на скачивание модели и перенос весов в GPU ушло только 94. Остальные 200 секунд потребовались на компиляцию, захват CUDA graphs, инициализацию движка и импорты. Значительную часть этой работы новый под повторяет при каждом старте, хотя результаты можно сохранить на ноде. В статье разберём, как настроить запуск LLM с нуля и сократить холодный старт: откуда KEDA получает сигнал о нагрузке, пока подов нет, что стоит заранее закэшировать и какие таймауты позволят первому запросу дождаться модели. На публичных замерах посмотрим, сколько времени дают эти оптимизации, а затем перейдём к манифестам KEDA HTTP Add-on, DaemonSet для предзагрузки и настройкам vLLM. Это поможет платформенным инженерам оценить, подходит ли такой режим их нагрузке или лучше оставить тёплую реплику.
Почему холодный старт LLM такой долгий
В отличие от лёгких веб-сервисов, LLM-под не может начать обслуживать запросы сразу после запуска контейнера. Нужно выполнить несколько последовательных этапов:
- Скачивание образа и весов модели. Если образ и веса не закэшированы на ноде, они скачиваются из реестра или хранилища. Для моделей в десятки гигабайт это может занять минуты.
- Загрузка весов в GPU. Перенос тензоров из оперативной памяти в видеопамять. На примере Llama 3.1 8B это заняло 94 секунды из 294.
- Компиляция и захват CUDA graphs. Движок (например, vLLM) компилирует ядра под конкретную конфигурацию и записывает графы вычислений. Это ускоряет последующие инференсы, но требует времени при старте.
- Инициализация движка и импорты. Загрузка Python-модулей, настройка планировщика, прогрев internal-структур.
Большая часть этих шагов повторяется при каждом запуске пода, хотя результаты можно сохранить и переиспользовать. Именно на это стоит направить оптимизацию.
Что кэшировать, чтобы не платить дважды
Самый очевидный шаг — убедиться, что образ контейнера и веса модели уже присутствуют на ноде. В Kubernetes для этого можно использовать DaemonSet, который заранее скачивает модель на каждую GPU-ноду, или локальный кэш образов. Если веса хранятся в объектном хранилище, настройте предзагрузку в постоянный том (PVC) с типом ReadOnlyMany или ReadWriteMany, чтобы поды не скачивали их заново.
Второй уровень — кэширование результатов компиляции. vLLM и подобные движки позволяют указать директорию для кэша (например, VLLM_CACHE_DIR). Если эта директория находится на локальном SSD и сохраняется между запусками пода (через hostPath или PVC), компиляция и захват CUDA graphs могут выполняться быстрее или пропускаться. Однако стоит проверить, поддерживает ли ваша версия движка переиспользование кэша между разными подами. На публичных замерах Tensorfuse такое кэширование не тестировалось, поэтому выигрыш нужно измерять самостоятельно.
Третий уровень — предварительный прогрев. Некоторые движки позволяют выполнить пробный инференс при старте, чтобы инициализировать все структуры. Это не сокращает общее время старта, но делает первый реальный запрос быстрее. Если первый запрос и так ждёт, лучше не тратить время на прогрев, а сразу переходить к обслуживанию.
Как KEDA узнаёт о нагрузке, когда подов нет
KEDA HTTP Add-on умеет масштабировать от нуля на основе HTTP-запросов. Он ставит перед сервисом прокси, который принимает входящие запросы и, если подов нет, инициирует масштабирование, а сам запрос ставит в очередь. Когда под готов, прокси перенаправляет запрос. Важно настроить таймауты: время ожидания в очереди должно быть больше, чем холодный старт. Иначе первый запрос получит ошибку 504, не дождавшись модели.
В манифесте KEDA HTTP Add-on можно задать параметр scaledObject с указанием minReplicaCount: 0 и maxReplicaCount, а также cooldownPeriod — время после последнего запроса, через которое под удаляется. Для LLM имеет смысл увеличить cooldownPeriod до десятков минут, чтобы не запускать холодный старт при редких, но повторяющихся запросах. Также проверьте, что прокси KEDA поддерживает достаточный таймаут (например, timeout: 600s).
Если нагрузка не HTTP, а, например, очередь сообщений, можно использовать KEDA-скейлеры для Kafka, RabbitMQ и других систем. Принцип тот же: сигнал о наличии работы запускает масштабирование, а потребитель ждёт готовности пода.
Практические шаги и типичные ошибки
Чтобы сократить холодный старт и не получить разочарование, действуйте по шагам:
- Измерьте текущий холодный старт. Запустите под с нуля и замерьте время до первого успешного ответа. Разбейте его на этапы: скачивание, загрузка весов, компиляция, инициализация.
- Обеспечьте локальное присутствие образа и весов. Используйте DaemonSet для предзагрузки или локальный кэш образов на нодах.
- Настройте кэш компиляции. Укажите директорию на быстром диске, сохраняемую между запусками. Проверьте, что движок её использует.
- Увеличьте таймауты в KEDA HTTP Add-on. Убедитесь, что очередь запросов ждёт дольше, чем длится холодный старт.
- Настройте cooldownPeriod. Не удаляйте поды слишком быстро, если запросы повторяются с интервалом в несколько минут.
- Рассмотрите гибридный подход. Если холодный старт остаётся слишком долгим, оставьте одну тёплую реплику (minReplicaCount: 1) для базовой нагрузки, а остальные добавляйте по требованию.
Типичные ошибки: недостаточный таймаут в прокси (запросы падают с ошибкой), отсутствие кэша весов на ноде (каждый старт скачивает модель заново), игнорирование cooldownPeriod (под удаляется сразу после запроса, и следующий запрос снова ждёт), а также попытка ускорить компиляцию настройками автоскейлера — это не работает.
В итоге, scale-to-zero для LLM возможен, но требует тщательной настройки кэширования и таймаутов. Публичные замеры показывают, что даже при оптимизациях холодный старт может занимать минуты. Если ваш сервис чувствителен к задержкам, возможно, стоит держать хотя бы одну тёплую реплику. В противном случае комбинируйте предзагрузку, кэш компиляции и правильные таймауты, чтобы первый запрос дожидался модели.