Счёт за GPU-узлы почти всегда растёт быстрее, чем нагрузка на модель. Карта дорогая, работает урывками, а платите вы за календарное время её существования. Ниже — конфигурация NodePool под GPU inference, выбор сигнала для скейлинга, анатомия холодного старта и методика расчёта, которая покажет, где экономия реальна.
Почему GPU-нода простаивает, а счёт растёт
Инференс-сервис редко нагружен равномерно: у внутреннего ассистента пик приходится на рабочие часы, у сервиса распознавания документов — на конец месяца. Карта занята ровно столько, сколько идут запросы, а оплачивается всё время, пока узел числится в кластере. Разрыв между этими величинами и есть та сумма, за которую идёт борьба.
Три уровня масштабирования и почему Pending-поды — не проблема HPA
Масштабирование в Kubernetes работает на трёх независимых уровнях, и путаница между ними — самая частая причина, по которой «автоскейлинг настроен, а не работает».
- Количество реплик. HPA или KEDA: смотрят на метрику и меняют число подов в Deployment.
- Размер одного пода. VPA подбирает requests и limits. Для GPU-подов почти бесполезен:
nvidia.com/gpu— целочисленный ресурс, а память и процессор определяются размером весов. - Количество и типы узлов. Cluster Autoscaler или Karpenter. Именно этот уровень отвечает за деньги: под без узла ничего не стоит, узел без подов стоит полную ставку.
Классическая картина: HPA поднял реплики с двух до шести, четыре пода встали в Pending с 12 Insufficient nvidia.com/gpu. HPA свою работу сделал — он не умеет создавать узлы. Дальше слово за узловым автоскейлером, и на этом стыке копится задержка: планировщик отметил под непланируемым, автоскейлер запросил машину, машина загрузилась, поставила драйверы, скачала образ. Минуты, в течение которых пользователи получают 503. Обратная ситуация не менее дорогая: пик прошёл, HPA вернул две реплики, четыре узла остались с нулевой нагрузкой и продолжают начисляться в счёте.
Karpenter против Cluster Autoscaler: где разница действительно в деньгах
Cluster Autoscaler работает с заранее заданными группами узлов. Логика предсказуемая, но у неё есть цена — дискретность (если в группе машины с четырьмя картами, а поду нужна одна, вы платите за четыре), комбинаторный взрыв групп и слабая упаковка.
Karpenter группами не оперирует. Контроллер читает требования непланируемых подов — ресурсы, nodeSelector, tolerations, зоны — и напрямую заказывает машину нужного размера. Дальше работает consolidation: контроллер проверяет, можно ли разместить текущие поды дешевле, и заменяет узлы.
|
Свойство |
Cluster Autoscaler |
Karpenter |
|---|---|---|
|
Источник конфигурации |
Группы узлов, заданные заранее |
NodePool с диапазоном требований |
|
Выбор типа машины |
Из фиксированного набора групп |
Подбирается под требования подов |
|
Упаковка подов (bin packing) |
Только при планировании |
Постоянная переупаковка через consolidation |
|
Смена типа машины на лету |
Нет |
Да, замена узла на более дешёвый |
|
Прерываемые инстансы |
Через отдельную группу |
Через |
|
Поддержка на площадках РФ |
Практически везде |
Зависит от провайдера: нужен поставщик ресурсов |
Где выигрыша не будет: на однородном парке подбирать не из чего, при ровной круглосуточной нагрузке consolidation нечего оптимизировать, а без поставщика ресурсов для вашего облака трудозатраты съедят экономию. Отдельная сложность — одновременная работа Karpenter и Cluster Autoscaler на одних подах: оба реагируют, кластер получает вдвое больше узлов, после чего оба начинают их удалять. Разводите нагрузки по taint'ам жёстко.
NodePool под GPU: рабочая конфигурация
NodePool описывает, какие узлы допустимо создавать, NodeClass — специфику облака (образ, сеть, диск). Конфиги с apiVersion: karpenter.sh/v1alpha5 и объектами Provisioner, AWSNodeTemplate, ttlSecondsAfterEmpty сегодня не применятся: вместо Provisioner — NodePool, вместо TTL-полей — секция disruption, вместо spec.provider — nodeClassRef.
Манифест NodePool и NodeClass с фильтрацией по GPU
Ключевая идея: разрешить только машины с ускорителями, пометить их taint'ом и ограничить пул по количеству карт, а не узлов.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu-inference
spec:
template:
metadata:
labels:
workload: inference
spec:
nodeClassRef:
group: karpenter.k8s.local
kind: CloudNodeClass
name: gpu-default
taints:
- key: nvidia.com/gpu
value: "true"
effect: NoSchedule
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand", "spot"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["gpu-t4-8-32", "gpu-a100-16-128"]
- key: topology.kubernetes.io/zone
operator: In
values: ["ru-1a", "ru-1b"]
expireAfter: 168h
limits:
nvidia.com/gpu: "12"
cpu: "192"
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 15m
weight: 10
taintsна уровне шаблона узла. Без него в дорогой пул уедет любой под без ограничений — сборщик логов, тестовый Job. Ответная сторона — tolerations в манифесте инференса плюсnodeSelectorна меткуworkload: inference.limitsв единицах GPU. Лимит «4 узла» превратится в 16 карт, если Karpenter возьмёт машины с четырьмя картами. Лимит вnvidia.com/gpu— прямой потолок расходов.capacity-typeс прерываемыми инстансами. Прерываемая машина дешевле, но её отбирают с коротким уведомлением, а перезапуск GPU-реплики занимает минуты. Рабочая схема — минимальный слой обычных узлов и прерываемые сверху: два NodePool с разнымweight.expireAfter. Долгоживущие GPU-узлы копят рассинхронизацию с эталонным образом.
В Deployment инференса обязательны три вещи: запрос ровно того числа карт, которое используется, tolerations под taint пула и topologySpreadConstraints.
resources:
limits:
nvidia.com/gpu: 1
requests:
cpu: "4"
memory: 24Gi
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: llm-serving
Оптимальная по упаковке раскладка и отказоустойчивая раскладка — разные вещи. Karpenter охотно разместит четыре реплики на одной машине с четырьмя картами, и потеря этой машины даст стопроцентный отказ сервиса.
Почему свежая нода «готова», но GPU на ней ещё нет
Узел переходит в Ready, когда отчитался kubelet, — но ресурс nvidia.com/gpu появляется в status.allocatable только после цепочки GPU-оператора: драйвер, контейнерная среда с поддержкой ускорителей, device plugin, регистрация устройств. Между этими моментами проходит от полминуты до нескольких минут.
В этом окне планировщик видит готовый узел без GPU и держит под в Pending, а Karpenter видит непланируемый под и может заказать ещё один узел. Consolidation это позже исправляет, но вы уже заплатили за лишнюю карту и лишний холодный старт. Меры: startupTaints в NodePool — узел создаётся с временным taint'ом, который снимает GPU-оператор после разметки устройств, и пока taint на месте, Karpenter не считает узел пригодной ёмкостью; образ с предустановленным драйвером, собственный или провайдерский, указанный в NodeClass, — разница в стадии подготовки в минутах.
По какому сигналу масштабировать инференс
Сигнал определяет и качество сервиса, и счёт, а выбор метрики для GPU-инференса не очевиден.
Очередь запросов и TTFT вместо утилизации GPU
Почему не CPU: у inference-пода процессор занят токенизацией и работой HTTP-сервера. При полной загрузке карты CPU может показывать 15%, а на потоке коротких запросов — подскочить без нагрузки на ускоритель.
Почему обманчива утилизация GPU: DCGM_FI_DEV_GPU_UTIL отражает, выполнялось ли на карте хоть что-то в интервале измерения, а не то, насколько плотно она загружена. vLLM и Triton объединяют запросы в пакеты непрерывно, поэтому и при одном активном запросе, и при трёх десятках метрика может держаться у верхней границы.
Что работает:
- Глубина очереди ожидающих запросов (у vLLM —
vllm:num_requests_waiting). Очередь стабильно ненулевая — нужна реплика. - Время до первого токена (TTFT). Метрика, которую чувствует пользователь, деградирует раньше остальных при перегрузке.
- Число конкурентных запросов на реплику. Нагрузочным тестом измеряете, при какой параллельности TTFT ещё укладывается в SLA, и держите этот порог.
Устойчивее всего связка: основной сигнал — конкурентность или глубина очереди, TTFT — страховочный порог для резкого масштабирования вверх.
Кастомные метрики в HPA и KEDA: от PromQL до масштабирования до нуля
Метрики сервера собирает Prometheus через ServiceMonitor, метрики карт — DCGM exporter. Дальше два пути.
Путь первый — prometheus-adapter. Компонент публикует запрос PromQL как кастомную метрику Kubernetes, после чего HPA работает с ней как со штатной.
rules:
- seriesQuery: 'vllm:num_requests_waiting{namespace!="",pod!=""}'
resources:
overrides:
namespace: {resource: namespace}
pod: {resource: pod}
name:
as: "vllm_requests_waiting"
metricsQuery: 'avg_over_time(vllm:num_requests_waiting{<<.LabelMatchers>>}[2m])'
avg_over_time с окном в две минуты здесь не украшение: мгновенное значение очереди шумит, а каждое ложное срабатывание на GPU стоит холодного старта. В самом HPA критично настроить поведение — вверх быстро, вниз с большой задержкой.
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 2
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 900
policies:
- type: Pods
value: 1
periodSeconds: 300
Асимметрия намеренная: лишняя реплика в течение пятнадцати минут стоит доли GPU-часа, а преждевременно остановленная — нового холодного старта в несколько минут.
Путь второй — KEDA. Даёт то, чего HPA не умеет: масштабирование до нуля. Модель, к которой обращаются несколько раз в день, между обращениями не должна занимать карту вообще.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: llm-serving
spec:
scaleTargetRef:
name: llm-serving
minReplicaCount: 0
maxReplicaCount: 6
cooldownPeriod: 600
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
query: sum(rate(nginx_ingress_controller_requests{service="llm-serving"}[2m]))
threshold: "0.1"
Ноль реплик означает, что Karpenter снимет и узел. Плата — первый запрос после простоя ждёт полный холодный старт. Для внутренней аналитической модели схема работает хорошо, для клиентского API — нет. Промежуточный вариант: клиент кладёт задачу в брокер, KEDA поднимает обработчик по глубине очереди, ответ забирается позже.
Холодный старт и консолидация: две цены автоскейлинга
Считать нужно обе цены автоматики: задержку появления новой реплики и риск потерять работающую.
Из чего складывается время до первого ответа новой реплики
Значения ниже — порядок величин для типовой конфигурации: узел с одной картой, модель на 7–14 миллиардов параметров, сервер vLLM, образ около 12 ГБ.
|
Стадия |
Типовая длительность |
Сокращается? |
|---|---|---|
|
Реакция автоскейлера на Pending-под |
5–20 с |
Частично — интервалом опроса |
|
Создание и загрузка виртуальной машины |
60–150 с |
Нет, определяется облаком |
|
Регистрация узла, старт kubelet |
20–40 с |
Слабо |
|
Драйверы и разметка устройств device plugin |
30–180 с |
Да — образ с драйвером |
|
Загрузка образа контейнера (12 ГБ) |
90–300 с |
Да — локальное зеркало реестра |
|
Загрузка весов модели в VRAM |
40–180 с |
Да — кэш весов на томе |
|
Прогрев: компиляция ядер, первый прогон |
20–90 с |
Частично — прогон при старте |
|
Итого до первого ответа |
≈ 4,5–13 мин |
— |
Реестр контейнеров в том же регионе срезает загрузку образа в разы; веса на сетевом томе облегчают образ; собственный образ узла закрывает стадию подготовки ускорителя. Не сокращается никак время создания ВМ и загрузка весов в память карты.
Даже после всех оптимизаций реалистичный минимум — около трёх минут. Значит, нужен минимальный резерв реплик. minReplicas ставится не по средней нагрузке, а так, чтобы существующие реплики выдержали характерный рост за время холодного старта: при плавном профиле хватает одной запасной реплики, при скачках в разы — двух-трёх.
Отдельная задача — обучение моделей, конкурирующее с инференсом за карты. Держать под него постоянный узел невыгодно. Здесь помогает облачная ML-платформа Cloud4U — GPU-сервер под конкретный цикл обучения без закупки железа: обучение нейросетей идёт до восьми раз быстрее, чем на процессорных мощностях, а инференс-кластер остаётся не тронутым тяжёлыми учебными задачами.
Настройки disruption и PDB, которые защищают работающий инференс
Consolidation приносит основную экономию и создаёт основной риск. Для процессорной нагрузки выселение пода стоит секунды, для GPU-инференса — полной перезагрузки весов на новом узле.
consolidationPolicy: WhenEmptyдля узлов с инференсом.WhenEmptyOrUnderutilizedразрешает переносить работающие поды ради упаковки — на GPU почти всегда неоправданно.consolidateAfter. 30–60 секунд провоцируют колебания: узел удалён, через две минуты нагрузка вернулась, платим холодный старт. Разумный ориентир — 10–20 минут.- PodDisruptionBudget на каждый inference-Deployment. Без него добровольное выселение может увести все реплики разом.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: llm-serving-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: llm-serving
minAvailable — в абсолютном числе, а не в процентах: при maxUnavailable: 25% и двух живых репликах округление разрешит выселить половину мощности сервиса.
Karpenter умеет ограничивать собственную активность по расписанию и по доле узлов:
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 20m
budgets:
- nodes: "1"
- nodes: "0"
schedule: "0 8 * * mon-fri"
duration: 11h
Первое правило разрешает трогать не более одного узла за раз, второе запрещает добровольные операции в рабочие часы буднего дня. Плюс прерываемые инстансы требуют обработчика уведомлений о скором изъятии, иначе реплика исчезнет без замены.
Сколько это экономит и когда не окупается
Расчёт экономии: переменные, формула и таблица сценариев
Переменные: C — стоимость GPU-часа (карта уровня A100 обходится в несколько раз дороже T4; конкретная ставка зависит от площадки и типа инстанса и остаётся в расчёте множителем C); N_peak — карт на пике, 6; N_base — постоянный резерв, 2; H_peak — часов в месяц под полным пулом, 180 из 730; T_cold — холодный старт, 0,1 ч; K — подъёмов узлов в месяц, 60.
Постоянный пул: C × N_peak × 730.
Автоскейлинг: C × N_base × 730 + C × (N_peak − N_base) × H_peak + C × (N_peak − N_base) × T_cold × K.
Сократим C и посчитаем в GPU-часах — так экономия видна независимо от ставки за карту.
|
Показатель (месяц, 730 ч) |
Постоянный пул |
Автоскейлинг |
|---|---|---|
|
Базовые карты (2 шт. круглосуточно) |
1460 GPU-ч |
1460 GPU-ч |
|
Пиковые карты (4 шт.) |
2920 GPU-ч |
720 GPU-ч |
|
Накладные расходы холодных стартов |
0 |
24 GPU-ч |
|
Итого GPU-времени в месяц |
4380 GPU-ч |
2204 GPU-ч |
|
Доля от постоянного пула |
100% |
≈ 50% |
На эластичном слое — пиковых картах — экономия видна ярче всего: 720 GPU-часов против 2920, срезается около трёх четвертей. По всему GPU-времени автоскейлинг обходится примерно вдвое дешевле постоянного пула, а накладные расходы холодных стартов — 24 GPU-часа из 2204, порядка процента месячного счёта, — на фоне этой экономии теряются.
К GPU-времени добавляются постоянные операционные статьи: мониторинг и сбор метрик нужны в обоих вариантах примерно одинаково, а автоскейлинг требует ещё порядка восьми часов инженера в месяц на сопровождение автоматики. Рядом с сэкономленными тысячами GPU-часов эти надбавки невелики, но в ноль экономию они не обращают — держите их в расчёте.
Внедрение считается отдельно: 60–80 часов инженера, разово. На фоне экономии около половины GPU-счёта в месяц (≈ 2176 GPU-часов при этом профиле) настройка окупается в первый же месяц. Оценки ориентировочны, итог сдвинется на ±25%. Экономия пропорциональна доле простоя: если пиковых карт всего одна-две сверх базы, сэкономленное GPU-время сжимается в разы — до нескольких сотен часов, — и на этом фоне сопровождение и риски перестают оправдываться. Наш ориентир: автоскейлинг GPU начинает уверенно окупаться от четырёх-пяти карт в пуле при доле простоя выше 40%.
Когда автоскейлинг GPU не подходит и что делать при дефиците карт
- Жёсткий SLA на задержку. Если отклик гарантирован в пределах секунд, минуты холодного старта неприемлемы: автоскейлинг работает только «сверх» гарантированного пула.
- Большие модели с долгой загрузкой весов. Для модели на 70+ миллиардов параметров загрузка весов может занять больше времени, чем весь остальной холодный старт.
- Дефицит карт и квоты. Karpenter заказывает узел, а облако отвечает отказом: нужного типа машин в зоне нет.
Что помогает при дефиците: несколько типов карт в requirements — пул, который умеет взять и A100, и что-то из младшей линейки, переживёт дефицит одного типа; несколько зон доступности; PriorityClass для инференса выше, чем для обучения; разделение одной карты между репликами. MIG — аппаратное деление карты на изолированные экземпляры с собственной памятью, доступное только на отдельных профессиональных ускорителях (A100, H100); time-slicing — программное разделение по времени, работает на любых картах, но без изоляции памяти. Для небольших моделей MIG позволяет разместить несколько реплик на одну карту.
Отдельный слой ограничений — данные. Если инференс работает с персональными или платёжными данными, узлы должны создаваться внутри аттестованного контура: список зон в NodePool ограничивается площадками нужного класса защищённости, а nodeClassRef указывает на образ и сеть внутри этого контура.
Главные выводы
Экономия появляется из связки трёх настроенных вещей: пул узлов, который берёт ровно нужные карты и не пускает к ним посторонние поды; сигнал масштабирования по очереди и TTFT, а не по загрузке процессора или обманчивой утилизации карты; ограничители консолидации. Karpenter выигрывает у Cluster Autoscaler там, где парк карт разнороден, а нагрузка неравномерна.
Перед внедрением честно посчитайте профиль: доля простоя, число карт сверх базового резерва, длительность холодного старта после оптимизаций. От четырёх-пяти карт в пуле и простоя выше 40% масштабирование ML-нагрузок окупается за первый же месяц. Две вещи не решаются конфигурацией: минимальный резерв реплик нужен всегда, а при физическом дефиците карт помогает заранее продуманный набор типов машин, зон и приоритетов плюс разделение карты через MIG.
