Динамическое MIG-разбиение GPU в Kubernetes

GPU в Kubernetes стоят дорого. Очень дорого. И когда команда специалистов по данным грызётся за GPU-ресурсы, как покупатели на распродаже, — очевидно, что-то сломано.

Именно с такой проблемой я столкнулся несколько лет назад. Решение было реализовано тогда же, и хотя с тех пор прошло немало времени, я решил, что сейчас самое время поделиться этим опытом.

В нашем кластере k8s было 8 GPU NVIDIA A100, и наши специалисты по данным были… недовольны. Небольшие задачи обучения моделей часами ждали доступа к GPU, пока крупные задачи распределённого обучения монополизировали целые GPU при 30%-й утилизации.

Виновник — гранулярность выделения GPU (GPU allocation granularity). Kubernetes воспринимает GPU как атомарные ресурсы: либо ты получаешь целый GPU, либо не получаешь ничего. Промежуточного варианта нет.

Нужно было это исправить. И решение оказалось совсем не тем, чего я ожидал.

Иллюстрация проблемы GPU-голодания в Kubernetes

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 нативно без пользовательских планировщиков

Нам было нужно решение, которое даст:

  1. Точное выделение GPU

  2. Изоляцию производительности

  3. Нативную интеграцию с Kubernetes

  4. Нулевые изменения в рабочих процессах специалистов по данным

Архитектурная схема распределения GPU-ресурсов

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 приходится разбивать заранее, а изменение разделения требует:

  1. Остановки всех рабочих нагрузок

  2. Перенастройки GPU

  3. Перезапуска всего

Для рабочего кластера это совершенно непрактично.

Схема статического разбиения MIG на A100

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
График улучшения утилизации GPU после внедрения решения

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:

  1. Распределение MIG-профилей по кластеру

  2. Тепловая карта утилизации GPU (по экземплярам)

  3. Временная шкала перенастроек

  4. Задержка планирования подов по размеру GPU

  5. Оценка экономии затрат (относительно покупки дополнительных 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.

© 2026 meganuke