GPU в Kubernetes стоят дорого. Очень дорого. И когда команда специалистов по данным грызётся за GPU-ресурсы, как покупатели на распродаже, — очевидно, что-то сломано.
Именно с такой проблемой я столкнулся несколько лет назад. Решение было реализовано тогда же, и хотя с тех пор прошло немало времени, я решил, что сейчас самое время поделиться этим опытом.
В нашем кластере k8s было 8 GPU NVIDIA A100, и наши специалисты по данным были… недовольны. Небольшие задачи обучения моделей часами ждали доступа к GPU, пока крупные задачи распределённого обучения монополизировали целые GPU при 30%-й утилизации.
Виновник — гранулярность выделения GPU (GPU allocation granularity). Kubernetes воспринимает GPU как атомарные ресурсы: либо ты получаешь целый GPU, либо не получаешь ничего. Промежуточного варианта нет.
Нужно было это исправить. И решение оказалось совсем не тем, чего я ожидал.
1 | Проблема: GPU-голодание против GPU-расточительства
Наш кластер выглядел так:
Типичный будний день:
-
3 крупных задачи глубокого обучения (PyTorch DDP) — каждая запрашивает целый A100
-
12 небольших задач инференса и дообучения — зависли в состоянии Pending
-
Фактическая утилизация GPU на работающих задачах: 35–45%
-
Время ожидания для небольших задач: 2–4 часа
Парадокс: вычислительные ресурсы GPU простаивали в то время, как задачи ждали в очереди.
Стандартные решения не работали:
-
Time-slicing (разделение GPU по времени от NVIDIA): вносило задержки и непредсказуемую производительность для ML-нагрузок
-
vGPU (VMware/NVIDIA vGPU): требовало корпоративного лицензирования и сложной настройки
-
Дробные запросы GPU (fractional GPU requests): не поддерживаются в k8s нативно без пользовательских планировщиков
Нам было нужно решение, которое даст:
-
Точное выделение GPU
-
Изоляцию производительности
-
Нативную интеграцию с Kubernetes
-
Нулевые изменения в рабочих процессах специалистов по данным
2 | Понимание MIG: архитектура
MIG (Multi-Instance GPU) позволяет разбить один GPU A100 или H100 на до 7 изолированных экземпляров, каждый из которых обладает:
-
Выделенными SM (Streaming Multiprocessors, потоковыми мультипроцессорами)
-
Выделенными срезами памяти
-
Выделенной пропускной способностью памяти
-
Аппаратной изоляцией (без проблемы «шумного соседа»)
Представьте это как пространства имён (namespaces) в Kubernetes, только реализованные на уровне железа GPU.
Профили MIG на A100–80GB:
1g.10gb → 1 экземпляр GPU, 10 ГБ памяти (возможно 7 экземпляров)
2g.20gb → 2 экземпляра GPU, 20 ГБ памяти (возможно 3 экземпляра)
3g.40gb → 3 экземпляра GPU, 40 ГБ памяти (возможно 2 экземпляра)
7g.80gb → Полный GPU, 80 ГБ памяти (1 экземпляр = весь GPU)
Загвоздка в том, что конфигурация MIG статична. GPU приходится разбивать заранее, а изменение разделения требует:
-
Остановки всех рабочих нагрузок
-
Перенастройки GPU
-
Перезапуска всего
Для рабочего кластера это совершенно непрактично.
3 | Решение: оператор динамического разбиения MIG
Я разработал пользовательский оператор Kubernetes, который динамически перенастраивает MIG-профили в зависимости от потребностей рабочих нагрузок. Вот как он работает:
Обзор архитектуры
Расширение планировщика Pod (Pod Scheduler Extender)
↓
Анализирует GPU-запросы ожидающих подов
↓
MIG-контроллер определяет оптимальное разбиение
↓
Проверяет необходимость перенастройки
↓
(Если нужна) Вытесняет поды → Перенастраивает MIG → Перепланирует
↓
Плагин GPU-устройств публикует новые MIG-устройства
↓
Поды планируются с соответствующими MIG-экземплярами
Ключевые компоненты
1. Определение пользовательского ресурса (CRD)
apiVersion: gpu.company.io/v1
kind: GPUPartitionPolicy
metadata:
name: ml-workload-policy
spec:
mode: "dynamic" # или "static"
optimizationInterval: "5m"
profiles:
- name: small-inference
migProfile: "1g.10gb"
priority: 1
- name: medium-training
migProfile: "3g.40gb"
priority: 2
- name: large-training
migProfile: "7g.80gb"
priority: 3
rules:
- if: "pending_pods > 5 AND requested_memory < 15GB"
then: "maximize 1g.10gb instances"
- if: "pending_pods > 0 AND requested_memory > 40GB"
then: "create 7g.80gb instances"
2. Логика MIG-контроллера
Контроллер запускается каждые 5 минут и выполняет следующее:
-
Сканирует ожидающие поды с GPU-запросами
-
Анализирует запрошенный объём памяти и вычислительные требования
-
Вычисляет оптимальную схему разбиения MIG
-
Сравнивает её с текущей конфигурацией
-
Инициирует перенастройку, если прирост эффективности превышает 20%
3. Стратегия перенастройки без простоев
Это была самая сложная часть. Нельзя просто «выдернуть» GPU из-под работающих нагрузок. Вот применённая стратегия:
def reconfigure_mig_safely(node_name, target_profiles):
# 1. Пометить узел taint'ом, чтобы блокировать новое планирование
kubectl_taint(node_name, "gpu-reconfiguring:NoSchedule")
# 2. Дождаться завершения работающих подов (с таймаутом)
wait_for_pod_completion(node_name, timeout="30m")
# 3. Плавно вытеснить оставшиеся поды
kubectl_drain(node_name, grace_period="10m")
# 4. Перенастроить MIG на GPU
nvidia_smi_mig_configure(target_profiles)
# 5. Перезапустить под daemonset плагина NVIDIA-устройств
restart_gpu_plugin(node_name)
# 6. Снять cordon с узла
kubectl_uncordon(node_name)
# 7. Убрать taint
kubectl_untaint(node_name, "gpu-reconfiguring")
4. Умные аннотации планирования
Специалисты по данным могут подсказывать свои требования через аннотации:
apiVersion: v1
kind: Pod
metadata:
name: bert-finetuning
annotations:
gpu.company.io/profile-preference: "1g.10gb,2g.20gb"
gpu.company.io/max-wait-time: "15m"
spec:
containers:
- name: training
image: pytorch:latest
resources:
limits:
nvidia.com/gpu: 1
Расширение планировщика читает эти аннотации и принимает взвешенные решения о размещении.
4 | Пример реализации
Вот фактические YAML-манифесты, которые мы развернули:
Метка GPU-узла
kubectl label nodes gpu-node-1 gpu.company.io/mig-capable=true
kubectl label nodes gpu-node-1 gpu.company.io/gpu-model=A100-80GB
Развёртывание MIG-контроллера
apiVersion: apps/v1
kind: Deployment
metadata:
name: mig-controller
namespace: gpu-system
spec:
replicas: 1
selector:
matchLabels:
app: mig-controller
template:
metadata:
labels:
app: mig-controller
spec:
serviceAccountName: mig-controller
containers:
- name: controller
image: company/mig-controller:v1.2.0
env:
- name: RECONFIGURATION_THRESHOLD
value: "20" # % прироста эффективности для запуска перенастройки
- name: CHECK_INTERVAL
value: "5m"
- name: POD_COMPLETION_TIMEOUT
value: "30m"
volumeMounts:
- name: nvidia-driver
mountPath: /usr/local/nvidia
volumes:
- name: nvidia-driver
hostPath:
path: /usr/local/nvidia
Пример развёртывания рабочей нагрузки
apiVersion: batch/v1
kind: Job
metadata:
name: gpt-inference-job
spec:
template:
metadata:
annotations:
gpu.company.io/profile-preference: "1g.10gb"
spec:
containers:
- name: inference
image: huggingface/transformers:latest
command: ["python", "inference.py"]
resources:
limits:
nvidia.com/mig-1g.10gb: 1
restartPolicy: Never
5 | Результаты в цифрах
После развёртывания решения:
Утилизация GPU:
-
До: 35–45% в среднем
-
После: 78–85% в среднем
-
Улучшение: +90% к утилизации
Время ожидания задач:
-
Небольшие задачи (< 15 ГБ): 2–4 часа → < 5 минут
-
Средние задачи (15–40 ГБ): 45 минут → < 10 минут
-
Крупные задачи (> 40 ГБ): без изменений (~5 минут)
Финансовый эффект:
-
Отложена покупка 4 дополнительных GPU A100
-
Экономия CapEx — более $120 тыс. за 6 месяцев
-
Снижение расходов на облачные GPU при пиковой нагрузке на 60%
Пользовательский опыт:
-
Специалисты по данным больше не «бронируют» время на GPU
-
Количество экспериментальных итераций выросло с 1–2 в день до 8–10 в день
-
Скорость итераций по моделям увеличилась в 3–4 раза
6 | Уроки и подводные камни
Что сработало хорошо: ✅ Постепенное внедрение: сначала dev-кластер, затем staging, и только потом prod ✅ Консервативная перенастройка: запускать только при приросте эффективности > 20% ✅ Правила pod affinity: держать многогпу-задачи на одном узле для производительности NCCL ✅ Мониторинг: вывод MIG-метрик в Prometheus для дашбордов Grafana
На что обратить внимание:
⚠️ MIG не бесплатен: накладные расходы ~2–3% по сравнению с полным GPU ⚠️ Не все GPU поддерживают MIG: только A100, A30, H100 (не V100, T4, A10) ⚠️ Задержка перенастройки: 30–90 секунд на GPU ⚠️ Многоузловое обучение: для NCCL-задач пришлось закреплять полные GPU (7g.80gb) ⚠️ Совместимость с драйверами: требуется NVIDIA driver 470+ и CUDA 11.4+
Что бы я изменил
Изначально я пытался перенастраивать MIG-профили при каждом ожидающем поде. Это привело к «дёрганью» — постоянным перенастройкам каждые несколько минут.
Решение — пакетная обработка и гистерезис:
-
Перенастраивать только если паттерн нагрузки сохраняется более 10 минут
-
Игнорировать временные всплески создания подов
-
Использовать экспоненциальную задержку (exponential backoff) между попытками перенастройки
Это сократило частоту перенастроек с ~15 в час до ~2 в час при сохранении отзывчивости системы.
7 | Мониторинг MIG-разделений в продакшене
Мы построили дашборды для отслеживания следующих показателей:
Метрики Prometheus
# Утилизация MIG-экземпляров по профилям
nvidia_mig_instance_utilization{profile="1g.10gb"}
# Частота перенастроек
rate(mig_reconfigurations_total[1h])
# Время ожидания подов по размеру GPU-запроса
histogram_quantile(0.95,
sum(rate(pod_gpu_pending_duration_bucket[5m]))
by (le, requested_profile)
)
Панели дашборда Grafana:
-
Распределение MIG-профилей по кластеру
-
Тепловая карта утилизации GPU (по экземплярам)
-
Временная шкала перенастроек
-
Задержка планирования подов по размеру GPU
-
Оценка экономии затрат (относительно покупки дополнительных GPU)
8 | Когда использовать этот подход (и когда не стоит)
✅ Идеально подходит, если:
-
Смешанные нагрузки: сочетание небольшого инференса и крупного обучения
-
Нестабильный спрос: непредсказуемые паттерны GPU-запросов
-
Бюджетные ограничения: нет возможности купить больше GPU
-
Современные GPU: A100, H100 или A30
-
Мультитенантные кластеры: ML-платформы, обслуживающие множество команд специалистов по данным
❌ Не подходит, если:
-
Устаревшие GPU: V100, P100, T4 не поддерживают MIG
-
Сверхвысокие требования к задержке: нельзя мириться с 30–90 с перенастройки
-
Однородные нагрузки: все используют одинаковый размер GPU
-
Небольшие кластеры: менее 4 GPU (накладные расходы себя не оправдывают)
-
Статичные паттерны нагрузки: предсказуемое использование (лучше просто предварительно разбить статически)
9 | Начало работы
Хотите попробовать в своём кластере? Вот пошаговый план:
Предварительные требования (предполагается, что всё это уже есть):
-
Kubernetes 1.24+
-
GPU NVIDIA A100/A30/H100
-
NVIDIA driver 470+
-
NVIDIA GPU Operator или Device Plugin
Шаг 1: Включите MIG на GPU
# SSH на GPU-узел
nvidia-smi -mig 1 # Включить режим MIG
nvidia-smi mig -cgi 1g.10gb -C # Создать экземпляры
Шаг 2: Установите NVIDIA GPU Feature Discovery
kubectl apply -f https://github.com/NVIDIA/gpu-feature-discovery/releases/download/v0.8.2/gpu-feature-discovery.yaml
Шаг 3: Разверните MIG-контроллер
Я опубликовал упрощённую версию этого контроллера в открытый доступ — напишите мне в LinkedIn для получения ссылки на репозиторий.
helm repo add mig-controller https://github.com/yourcompany/mig-controller
helm install mig-controller mig-controller/mig-controller \
--namespace gpu-system \
--create-namespace \
--set reconfigurationThreshold=20 \
--set checkInterval=5m
Шаг 4: Задайте политику разбиения
kubectl apply -f partition-policy.yaml
Шаг 5: Мониторинг и итерации
-
Следите за частотой перенастроек
-
Настраивайте пороговые значения под свои паттерны нагрузки
-
Добавляйте пользовательские правила для своих конкретных сценариев
10 | Заключение
GPU слишком дороги, чтобы расходовать их впустую. Но стандартная модель планирования GPU в Kubernetes трактует их как ресурс «всё или ничего», что неизбежно ведёт либо к голоданию, либо к расточительству.
Динамическое разбиение MIG дало нам лучшее из обоих миров:
-
Точное выделение ресурсов без ущерба для изоляции
-
Производительность на уровне железа с гарантиями
-
Нативная интеграция с k8s (без изменений в рабочих процессах специалистов по данным)
-
Существенный рост утилизации (+90%)
-
Значительная экономия затрат (более $120 тыс. за 6 месяцев)
Команда специалистов по данным перестала бороться за GPU и получила мгновенный доступ к ресурсам нужного размера. Итерации ускорились. Затраты упали. Все стали довольнее.
Если вы запускаете GPU-нагрузки в Kubernetes и наблюдаете низкую утилизацию или длинные очереди — возможно, именно это решение вам и нужно.
Начните с малого. Включите MIG на одном узле. Понаблюдайте за паттернами. Стройте автоматизацию постепенно. Вполне может оказаться, что ваш кластер начнёт делать больше с меньшими ресурсами.
Пусть GPU гибко подстраиваются под задачи. Пусть задачи получают ровно столько, сколько нужно. В этом и есть сила динамического разбиения MIG.