Привет, Хабр! Меня зовут Макарий, и как Senior SRE в Yandex Cloud я не только участвовал в разработке Managed Service for Kubernetes, но и всегда любил в свободное время посмотреть, что интересного понавыпускали для «кубика». Kubernetes, как де‑факто стандарт оркестрации контейнеров, предлагает базовые механизмы для управления вычислительными ресурсами. Однако стандартный планировщик Kubernetes (kube‑scheduler) разрабатывался с учётом общих принципов балансировки нагрузки и не специализирован для уникальных особенностей рабочих GPU‑нагрузок.

Предлагаю рассмотреть весь спектр возможностей — от встроенных механизмов шедулинга K8s до специализированных планировщиков, таких как Volcano, Apache YuniKorn и KAI‑Scheduler. Проанализирую конкретные сценарии, в которых каждый из этих инструментов демонстрирует свои преимущества, и предложу рекомендации по выбору оптимального решения для ваших рабочих GPU‑нагрузок.

Что вы найдёте в статье:

  1. Механизмы тонкой настройки размещения подов встроенными средствами: 

    Node Selector
    Node Name
    Affinity/Anti‑Affinity
    Taint & Tolerations
    Pod Topology Spread Constraints
    Priority and Preemption
    Descheduler
    Комбинированный подход
    Кастомизация стандартного планировщика

  2. Использование кастомных планировщиков JobSet и Kueue

  3. Полнофункциональный кастомный шедулинг:

    Volcano Scheduler
    Apache YuniKorn
    KAI‑Scheduler

  4. Граничные случаи

  5. Итоговые выводы и рекомендации

Что такое Scheduling в K8S?

Под шедулингом мы понимаем процесс распределения подов по узлам (нодам, или Nodes) кластера K8S. Этим процессом управляет компонент под названием kube‑scheduler. На основе различных критериев он отвечает за выбор подходящих нод для каждого пода, например, таких как ресурсы, политики, метки и так далее. Особенно эти механизмы полезны при работе с GPU‑нагрузкой, так как такой вид мощностей сейчас всегда ограничен.

Механизмы тонкой настройки размещения подов в Kubernetes

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

Default Scheduling

Kube‑scheduler автоматически распределяет поды по узлам на основе доступных ресурсов (CPU, RAM, GPU) и других факторов.

Юзкейсы: когда лень что‑то придумывать и у нас нет требований к ноде, на которой будет поднят под.

Node Selector

nodeSelector — простейший способ ограничить размещение подов на определенных узлах.Позволяет указать метку ноды (label), на которой должен быть запущен под.

Юзкейс: компания имеет кластер с разными типами GPU (V100, A100, T4). Задача инференса требует конкретно V100 для оптимальной производительности.

apiVersion: v1
kind: Pod
metadata:
  name: gpu-inference
spec:
  containers:
  - name: inference-container
    image: ai-model:latest
    resources:
      limits:
        nvidia.com/gpu: 1
  nodeSelector:
    gpu-type: "tesla-v100"

❗️ Не забудьте убедиться, что на ноде есть нужная метка — установить её можно как вручную, так и автоматически.

Node Name

Это поле в спецификации пода, при указании значения которого игнорируются все другие механизмы планировки. Если указанный узел недоступен или не может запустить под (например, из‑за нехватки ресурсов), под останется в состоянии Pending.

Юзкейс: отладка производительности на конкретном аппаратном обеспечении или гарантированное выполнение критичной задачи на узле с известными характеристиками.

apiVersion: v1
kind: Pod
metadata:
  name: direct-placement-gpu
spec:
  nodeName: gpu-node-0012
  containers:
  - name: gpu-workload
    image: nvidia/cuda:11.0-base
    resources:
      limits:
        nvidia.com/gpu: 1

Эта опция подходит для тестирования или ручного управления распределением нагрузки.

?Но из‑за отсутствия гибкости можно попрощаться с доступностью вашего приложения и его масштабированием.

Affinity/Anti-Affinity

Механизмы, которые позволяют управлять тем, как поды распределяются по нодам (Node Affinity/Anti‑Affinity) или как они взаимодействуют с другими подами (Pod Affinity/Anti‑Affinity) на основе меток.

Node Affinity/Anti-Affinity

nodeAffinity предлагает более гибкий подход, чем nodeSelector, с поддержкой сложной логики выбора узлов.
Юзкейсы:

  • Команда ML требует строго V100 GPU, но предпочитает специфические инстансы Yandex Cloud (меньшая стоимость).

    Пример Node Affinity
    apiVersion: v1
    kind: Pod
    metadata:
      name: gpu-training
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: gpu-type
                operator: In
                values:
                - v100
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            preference:
              matchExpressions:
              - key: node.kubernetes.io/instance-type
                operator: In
                values:
                - gpu-standard-v2
      containers:
      - name: training-container
        image: deep-learning:latest
        resources:
          limits:
            nvidia.com/gpu: 4
  • Критичная инференс-нагрузка производственного сервиса, которая должна избегать узлов, где выполняются экспериментальные или разделяемые GPU-задачи.

    Пример Node Affinity с исключением определённого типа нод для изоляции критичных задач
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        preference:
          matchExpressions:
          - key: workload-type
            operator: NotIn
            values:
            - gpu-shared

Pod Affinity / Anti-Affinity

Pod Affinity управляет размещением подов относительно других подов, что важно для сложных ML‑пайплайнов.

Юзкейс
: сервер инференса должен быть размещен на том же узле, что и под кеширования моделей, для минимизации задержки загрузки моделей.

Пример Pod Affinity
apiVersion: v1
kind: Pod
metadata:
  name: model-server
  labels:
    app: inference
    model: gpt-j
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - model-cache
        topologyKey: kubernetes.io/hostname
  containers:
  - name: inference-server
    image: model-server:latest
    resources:
      limits:
        nvidia.com/gpu: 1

Юзкейс: распределение нескольких экземпляров сервиса инференса по разным узлам для повышения доступности и отказоустойчивости.

Пример Pod Anti-Affinity
podAntiAffinity:
  preferredDuringSchedulingIgnoredDuringExecution:
  - weight: 100
    podAffinityTerm:
      labelSelector:
        matchExpressions:
        - key: app
          operator: In
          values:
          - inference
      topologyKey: kubernetes.io/hostname

Taint & Tolerations

Позволяет управлять тем, какие поды могут быть запущены на каких узлах. Узлы могут быть «загрязнены» (tainted), чтобы отклонять запуск подов, которые не имеют подходящей «толерантности» (tolerations). 

Поможет для резервирования нод под специфические задачи или в предотвращении запуска определённых подов на определённых нодах. Также полезно использовать для обслуживания/обновления нод.

Выделение GPU-узлов для критичных нагрузок

Юзкейс: изоляция дорогостоящих GPU‑узлов для производственных рабочих нагрузок ML, предотвращение размещения на них некритичных подов.

# Пометка узла
kubectl taint nodes gpu-node-1 dedicated=ml-prod:NoSchedule


# Под с toleration
apiVersion: v1
kind: Pod
metadata:
  name: critical-model-training
spec:
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "ml-prod"
    effect: "NoSchedule"
  containers:
  - name: training-job
    image: ml-framework:latest
    resources:
      limits:
        nvidia.com/gpu: 8
Выделение специфических GPU для конкретных команд

Юзкейс: разделение GPU‑ресурсов между командами в крупной организации, где команда компьютерного зрения получает выделенные GPU для своих специфических задач.

# Узел для команды компьютерного зрения
kubectl taint nodes gpu-node-2 team=computer-vision:NoSchedule


# Только поды команды CV могут использовать эти узлы
apiVersion: v1
kind: Pod
metadata:
  name: object-detection-training
spec:
  tolerations:
  - key: "team"
    operator: "Equal"
    value: "computer-vision"
    effect: "NoSchedule"
  containers:
  - name: cv-container
    # ...

❗️ Под может иметь несколько толерантностей, чтобы соответствовать нескольким taints на узле.

DaemonSets автоматически добавляют толерантности к taints с эффектом NoSchedule, чтобы их поды могли запускаться на всех узлах.

Pod Topology Spread Constraints

Pod Topology Spread Constraints обеспечивает распределение подов по топологическим доменам.

Юзкейс: распределённое обучение модели, где поды должны быть распределены по зонам доступности для устойчивости к сбоям, но при этом некоторая локальность на уровне узлов допускается для оптимизации производительности.

apiVersion: v1
kind: Pod
metadata:
  name: distributed-training
  labels:
    app: ml-training
spec:
  topologySpreadConstraints:
  - maxSkew: 1 # Максимальная разница подов между доменами
    topologyKey: topology.kubernetes.io/zone # Топологический домен
    whenUnsatisfiable: DoNotSchedule # Если правило не будет выполнено
    labelSelector: # Применяется только к подам с меткой
      matchLabels:
        app: ml-training
  - maxSkew: 2
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app: ml-training
  containers:
  - name: pytorch-training
    image: pytorch:latest
    resources:
      limits:
        nvidia.com/gpu: 2

Для поля whenUnsatisfiable есть два варианта значения, определяющих поведение, когда правило не выполняется:

  • DoNotSchedule: под не будет запланирован. Повиснет в статусе Pending.

  • ScheduleAnyway: под будет запланирован на любой узел.

Priority and Preemption

Механизмы, позволяющие управлять важностью подов и определять, какие поды будут запущены в первую очередь. Особенно полезно при нехватке ресурсов.

Priority:

  • Каждому поду назначается приоритет, который указывает на его важность относительно других подов.

  • Этот приоритет определяется с помощью PriorityСlass.

Preemption:

  • При нехватке в кластере ресурсов для запуска подов с высоким приоритетом, k8s вытеснит (удалит) поды с более низким приоритетом, чтобы освободить ресурсы.

PriorityClass:

  • Объект K8S, определяющий уровень приоритета. У каждого класса есть поля value (числовое значение приоритета), description и globalDefault (должен ли этот PriorityClass использоваться по умолчанию для всех подов).

Примеры использования

Создание PriorityClass
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: production-critical
value: 1000000
globalDefault: false
description: "Critical production ML inference"

❗️ Обратите внимание: чем выше значение value, тем выше приоритет.  

Использование PriorityClass
apiVersion: v1
kind: Pod
metadata:
  name: customer-facing-inference
spec:
  priorityClassName: production-critical
  containers:
  - name: inference-server
    image: inference:latest
    resources:
      limits:
        nvidia.com/gpu: 1

Юзкейс: ML‑сервис, обслуживающий клиентов в реальном времени, имеет высший приоритет и может вытеснять поды обучения/тестирования при нехватке GPU‑ресурсов. После вытеснения (Preemption) менее приоритетные поды перейдут в состояние Pending, а более приоритетные будут запущены.

❗️ Не все поды могут быть вытеснены, например системные поды (kube-system) обычно защищены от вытеснения.

Kube-scheduler учитывает приоритеты при выборе узлов для подов. Поды с более высоким приоритетом имеют преимущество при распределении ресурсов.
Quality of Service (Guaranteed, Burstable, BestEffort) влияет на процесс вытеснения.

Вытесненные поды переходят в статус Pending на короткий период времени, если ресурсов в кластере достаточно, или продолжительный, при их нехватке. В связи с этим их запрошенные ресурсы (CPU, memory и другие) учитываются при оценке ноды планировщиком. 

Для оптимизации использования ресурсов надо как‑то их перемещать, например, с помощью Descheduler.

Descheduler

Descheduler — это инструмент, который периодически анализирует размещение подов в кластере и может перемещать их для оптимизации использования ресурсов. В контексте GPU‑нагрузок Descheduler особенно полезен для борьбы с фрагментацией ресурсов и обеспечения эффективного использования дорогостоящих GPU.

Основные стратегии Descheduler для GPU-оптимизации

  1. RemoveDuplicates — удаляет дублирующиеся поды с одного узла.

  2. LowNodeUtilization — перемещает поды с недозагруженных узлов.

  3. HighNodeUtilization — освобождает перегруженные узлы.

  4. RemovePodsViolatingNodeAffinity — исправляет нарушения node affinity.

  5. RemovePodsViolatingTopologySpreadConstraint — балансирует распределение подов.

Пример 1: Консолидация GPU-ресурсов для освобождения узлов

Юзкейс: в кластере с 10 GPU‑узлами поды распределены неравномерно — на каждом узле используется только 1–2 GPU из 8 доступных. Descheduler консолидирует нагрузку, освобождая целые узлы для размещения задач, требующих много GPU (например, обучение больших моделей).

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemoveDuplicates":
    enabled: true
  "LowNodeUtilization":
    enabled: true
    params:
      nodeResourceUtilizationThresholds:
        thresholds:
          "nvidia.com/gpu": 25  # Узлы с утилизацией GPU менее 25%
          "cpu": 20
          "memory": 20
        targetThresholds:
          "nvidia.com/gpu": 80  # Целевая утилизация GPU 80%
          "cpu": 70
          "memory": 70
      numberOfNodes: 2  # Максимум 2 узла могут быть недозагружены
  "RemovePodsViolatingNodeAffinity":
    enabled: true
    params:
      nodeAffinityType:
      - "requiredDuringSchedulingIgnoredDuringExecution"
Пример 2: Балансировка GPU-нагрузки между зонами доступности

Юзкейс: критичные ML‑сервисы должны быть равномерно распределены по зонам доступности, но со временем распределение нарушается. Descheduler восстанавливает баланс.

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemovePodsViolatingTopologySpreadConstraint":
    enabled: true
    params:
      includeSoftConstraints: true
  "LowNodeUtilization":
    enabled: true
    params:
      nodeResourceUtilizationThresholds:
        thresholds:
          "nvidia.com/gpu": 30
        targetThresholds:
          "nvidia.com/gpu": 75
      # Обрабатываем только ML namespace'ы
      evictableNamespaces:
        include:
        - "ml-inference"
        - "ml-training"
Пример 3: Исправление нарушений размещения по типам GPU

Юзкейс: в кластере есть узлы с T4 GPU (для инференса) и A100 GPU (для обучения). Со временем поды могут оказаться на неоптимальных узлах из‑за изменений в кластере. Descheduler исправляет такие нарушения, перемещая поды на узлы с подходящими типами GPU.

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemovePodsViolatingNodeAffinity":
    enabled: true
    params:
      nodeAffinityType:
      - "requiredDuringSchedulingIgnoredDuringExecution"
      - "preferredDuringSchedulingIgnoredDuringExecution"
  "LowNodeUtilization":
    enabled: true
    params:
      nodeResourceUtilizationThresholds:
        thresholds:
          "nvidia.com/gpu": 20
        targetThresholds:
          "nvidia.com/gpu": 70
      # Обрабатываем только ML-нагрузки
      evictableNamespaces:
        include:
        - "ml-inference"
        - "ml-training"
        - "ml-experiments"
      # Учитываем селекторы подов для правильного размещения
      labelSelector:
        matchExpressions:
        - key: gpu-workload-type
          operator: In
          values:
          - "inference"
          - "training"
Пример 4: Продвинутая конфигурация с метриками и фильтрами

Юзкейс: крупная ML‑платформа с различными типами нагрузок требует тонкой настройки Descheduler с учётом приоритетов, возраста подов и специфических меток.

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "LowNodeUtilization":
    enabled: true
    params:
      nodeResourceUtilizationThresholds:
        thresholds:
          "nvidia.com/gpu": 25
          "cpu": 20
          "memory": 20
        targetThresholds:
          "nvidia.com/gpu": 75
          "cpu": 70
          "memory": 70
      evictableNamespaces:
        include:
        - "ml-training"
        - "ml-experiments"
      nodeFit: true  # Проверяем, что под может быть перемещен
      # Фильтры для исключения критичных подов
      thresholdPriority: 10000  # Не трогаем поды с приоритетом выше 10000
      thresholdPriorityClassName: "high-priority-inference"
  "RemovePodsHavingTooManyRestarts":
    enabled: true
    params:
      podRestartThreshold: 5  # Удаляем поды с более чем 5 перезапусками
      includingInitContainers: true
  "PodLifeTime":
    enabled: true
    params:
      maxPodLifeTimeSeconds: 86400  # 24 часа для экспериментальных подов
      podStatusPhases:
      - "Pending"
      - "PodInitializing"
      labelSelector:
        matchLabels:
          workload-type: "experiment"

Мониторинг эффективности Descheduler

Для оценки эффективности работы Descheduler важно отслеживать метрики его работы и влияние на утилизацию GPU‑ресурсов. Descheduler предоставляет встроенные метрики для Prometheus, включая количество вытесненных подов и информацию о выполненных операциях.

Подробную информацию о доступных метриках и примеры их использования можно найти в официальной документации Descheduler.

Важные замечания

  • Descheduler не создаёт новые поды, а только удаляет существующие для их перепланирования.

  • Поды с PodDisruptionBudget условно защищены в рамках бюджета недоступности.

  • Критичные системные поды (kube‑system) по умолчанию исключены из обработки.

  • Рекомендуется начинать с --dry-run=true для тестирования конфигурации.

  • Частота запуска должна балансировать между оптимизацией и стабильностью системы.

Парочка примеров комбинированного подхода

Сценарий 1: Производственная ML-платформа с разделением ресурсов

Юзкейс: критичный производственный сервис инференса, который должен иметь:

  • Высокий приоритет для вытеснения других подов.

  • Размещение на специальных узлах с T4 GPU (оптимальных для инференса).

  • Изоляцию от подов обучения для стабильной задержки.

  • Распределение по зонам доступности для обеспечения отказоустойчивости.

apiVersion: v1
kind: Pod
metadata:
  name: production-inference
  labels:
    app: inference
    environment: production
spec:
  priorityClassName: high-priority-inference
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: nvidia.com/gpu.product
            operator: In
            values:
            - NVIDIA-T4  # Оптимально для инференса
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - training  # Избегать узлов с подами обучения
        topologyKey: kubernetes.io/hostname
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "inference"
    effect: "NoSchedule"
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: inference
  containers:
  - name: inference-container
    image: inference-server:latest
    resources:
      limits:
        nvidia.com/gpu: 1
Сценарий 2: Распределённое обучение большой модели

Юзкейс: распределённое обучение крупной языковой модели, которое требует:

  • Мощные A100 GPU с большим объемом памяти.

  • Размещение подов одного и того же задания на одних и тех же узлах (когда возможно) для оптимизации межузловой коммуникации.

  • Толерантность к taint «training», чтобы использовать узлы, выделенные для обучения.

  • Запрос большого количества ресурсов (8 GPU, hugepages, память).

apiVersion: v1
kind: Pod
metadata:
  name: distributed-llm-training-worker-1
  labels:
    app: distributed-training
    role: worker
    job-id: llm-training-job-123
spec:
  priorityClassName: medium-priority-training
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: nvidia.com/gpu.product
            operator: In
            values:
            - NVIDIA-A100
          - key: nvidia.com/gpu.memory
            operator: Gt
            values:
            - "40000"  # Нужно минимум 40GB памяти
    podAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: job-id
              operator: In
              values:
              - llm-training-job-123
          topologyKey: kubernetes.io/hostname
  tolerations:
  - key: "workload"
    operator: "Equal"
    value: "training"
    effect: "NoSchedule"
  containers:
  - name: training-container
    image: pytorch-distributed:latest
    resources:
      limits:
        nvidia.com/gpu: 8
        hugepages-2Mi: 5Gi
        memory: 128Gi

Кастомизируем стандартный планировщик

Стандартный планировщик kube‑scheduler можно существенно настроить через KubeSchedulerConfiguration для оптимизации размещения GPU‑нагрузок без перехода на полностью кастомные решения. Ключевые преимущества этого подхода:

  1. Минимальные изменения инфраструктуры. Не требуется устанавливать дополнительные компоненты или CRD.

  2. Гибкость. Можно настроить различные аспекты планирования под конкретные GPU‑нагрузки.

  3. Возможность нескольких профилей. Позволяет создать разные планировщики для разных типов GPU‑задач.

  4. Совместимость. Стандартный компонент Kubernetes гарантирует совместимость и поддержку.

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

❗️ Для изменения настроек стандартного планировщика в кластере нужен доступ к мастерам. А значит это не применимо для managed‑решений. 

Но в кластере возможно запустить ещё один стандартный планировщик с другим именем и настроить его под свои нужды. Поды могут выбирать, каким шедулером они будут назначаться, через поле spec.schedulerName

Пример 1: Оптимизация для предотвращения фрагментации GPU

Юзкейс: ограниченное количество GPU, требуется максимальная утилизация ресурсов. Конфигурация направлена на «плотную упаковку» подов, чтобы минимизировать фрагментацию GPU‑ресурсов. Поды будут размещаться на узлах, где уже используются GPU, до полного исчерпания ресурсов узла.

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: gpu-optimized-scheduler
  plugins:
    score:
      enabled:
      - name: NodeResourcesBalancedAllocation
        weight: 2  # Повышенный вес для сбалансированного размещения
      - name: NodeResourcesFit
        weight: 4  # Высокий вес для плотной упаковки
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: MostAllocated  # Стратегия плотной упаковки
        resources:
        - name: nvidia.com/gpu
          weight: 10  # Высокий вес для GPU
        - name: cpu
          weight: 3
        - name: memory
          weight: 1
Пример 2: Оптимизация для равномерного распределения GPU-нагрузки

Юзкейс: сервис инференса, требующий стабильной задержки и предсказуемой производительности. Конфигурация равномерно распределяет нагрузку по всем доступным GPU, предотвращая перегрузку отдельных узлов. Это минимизирует соревнование за ресурсы и обеспечивает стабильное время отклика.

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: gpu-balanced-scheduler
  plugins:
    score:
      enabled:
      - name: NodeResourcesBalancedAllocation
        weight: 5  # Сильный акцент на сбалансированное распределение
      - name: NodeResourcesFit
        weight: 2
      - name: TaintToleration
        weight: 1
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: LeastAllocated  # Стратегия равномерного распределения
        resources:
        - name: nvidia.com/gpu
          weight: 8
        - name: cpu
          weight: 2
        - name: memory
          weight: 1
Пример 3: Особая обработка тяжелых GPU-заданий

Юзкейс: обучение очень больших моделей (например, LLM), требующих значительных объемов GPU‑памяти. Конфигурация отдает приоритет узлам с наибольшим объёмом доступных ресурсов GPU и памяти, чтобы предотвратить аварийное завершение подов из‑за нехватки памяти.

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: large-model-scheduler
  plugins:
    preFilter:
      enabled:
      - name: NodeResourcesFit
      - name: NodePorts
    filter:
      enabled:
      - name: NodeResourcesFit
      - name: NodeName
      - name: NodeUnschedulable
    preScore:
      enabled:
      - name: InterPodAffinity
      - name: NodeAffinity
      - name: NodeResourcesFit
    score:
      enabled:
      - name: NodeResourcesBalancedAllocation
        weight: 1
      - name: NodeResourcesFit
        weight: 10
  pluginConfig:
  - name: NodeResourcesFit
    args:
      # Увеличенный буфер для избежания OOM-ситуаций
      scoringStrategy:
        type: MostAllocated
        resources:
        - name: nvidia.com/gpu
          weight: 10
        - name: memory
          weight: 5
        - name: cpu
          weight: 3
Пример 4: Планировщик с учетом топологии GPU и NVLink

Юзкейс: высокопроизводительные вычисления, где критически важна скорость межпроцессорного обмена данными. Планировщик отдаёт предпочтение узлам с GPU, которые соединены через NVLink, и размещает поды с учетом NUMA‑топологии.

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: nvlink-aware-scheduler
  plugins:
    filter:
      enabled:
      - name: NodeResourcesFit
      - name: NodeAffinity
      - name: PodTopologySpread
    score:
      enabled:
      - name: NodeResourcesFit
        weight: 8
      - name: NodeAffinity
        weight: 5
      - name: PodTopologySpread
        weight: 3
      - name: InterPodAffinity
        weight: 2
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: MostAllocated
        resources:
        - name: nvidia.com/gpu
          weight: 15
        - name: hugepages-2Mi
          weight: 8  # Важно для высокопроизводительных вычислений
        - name: memory
          weight: 5
        - name: cpu
          weight: 3
  - name: NodeAffinity
    args:
      addedAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
          - matchExpressions:
            - key: nvidia.com/gpu.nvlink
              operator: Exists
            - key: node.kubernetes.io/instance-type
              operator: In
              values:
              - gpu-h100-8x  # Узлы с 8 H100 GPU и NVLink
Пример 5: Многопрофильный планировщик для разных типов нагрузок

Юзкейс: универсальная конфигурация для кластера с различными типами GPU‑нагрузок. Разные профили оптимизированы под конкретные сценарии использования.

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
# Профиль для инференса - быстрое размещение, равномерное распределение
- schedulerName: inference-scheduler
  plugins:
    score:
      enabled:
      - name: NodeResourcesFit
        weight: 3
      - name: NodeResourcesBalancedAllocation
        weight: 5
      - name: ImageLocality
        weight: 2  # Учитываем локальность образов
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: LeastAllocated
        resources:
        - name: nvidia.com/gpu
          weight: 8
        - name: cpu
          weight: 3
        - name: memory
          weight: 2


# Профиль для обучения - плотная упаковка, максимальная утилизация
- schedulerName: training-scheduler
  plugins:
    score:
      enabled:
      - name: NodeResourcesFit
        weight: 8
      - name: NodeResourcesBalancedAllocation
        weight: 2
      - name: PodTopologySpread
        weight: 3
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: MostAllocated
        resources:
        - name: nvidia.com/gpu
          weight: 12
        - name: memory
          weight: 6
        - name: hugepages-2Mi
          weight: 4
        - name: cpu
          weight: 2


# Профиль для экспериментов - гибкое размещение с низким приоритетом
- schedulerName: experiment-scheduler
  plugins:
    score:
      enabled:
      - name: NodeResourcesFit
        weight: 4
      - name: NodeResourcesBalancedAllocation
        weight: 3
      - name: TaintToleration
        weight: 2
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: LeastAllocated
        resources:
        - name: nvidia.com/gpu
          weight: 6
        - name: cpu
          weight: 3
        - name: memory
          weight: 2
Пример 6: Планировщик с поддержкой GPU-шаринга и MIG

Юзкейс: современные GPU A100/H100 поддерживают Multi‑Instance GPU (MIG), позволяя разделять один физический GPU на несколько виртуальных. Планировщик оптимизирован для эффективного использования MIG‑инстансов.

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: mig-aware-scheduler
  plugins:
    filter:
      enabled:
      - name: NodeResourcesFit
      - name: NodeAffinity
    score:
      enabled:
      - name: NodeResourcesFit
        weight: 10
      - name: NodeResourcesBalancedAllocation
        weight: 3
      - name: NodeAffinity
        weight: 2
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: MostAllocated  # Максимизируем использование MIG-слайсов
        resources:
        - name: nvidia.com/mig-1g.5gb    # MIG 1/7 A100
          weight: 8
        - name: nvidia.com/mig-2g.10gb   # MIG 2/7 A100
          weight: 10
        - name: nvidia.com/mig-3g.20gb   # MIG 3/7 A100
          weight: 12
        - name: nvidia.com/mig-7g.40gb   # MIG 7/7 A100 (полный GPU)
          weight: 15
        - name: memory
          weight: 4
        - name: cpu
          weight: 2

Практические рекомендации:

  • Начинайте с простых конфигураций и постепенно добавляйте сложность.

  • Тестируйте новые конфигурации на dev‑окружении перед продакшн‑средой.

  • Мониторьте метрики планировщика для оценки эффективности.

  • Используйте разные планировщики для разных типов нагрузок.

  • Регулярно пересматривайте конфигурации при изменении паттернов использования GPU.

Планирование нагрузки с использованием JobSet и Kueue

JobSet и Kueue — относительно новые инструменты в экосистеме Kubernetes, которые предлагают элегантные решения для оркестрации сложных рабочих GPU‑нагрузок. В отличие от полномасштабных кастомных планировщиков, эти инструменты фокусируются на конкретных аспектах управления пакетными заданиями, сохраняя при этом интеграцию со стандартным планировщиком Kubernetes.

JobSet: координация взаимосвязанных заданий

JobSet — это Kubernetes‑расширение для управления группами взаимосвязанных заданий, которые должны выполняться вместе.

Ключевые возможности:

  • Координированный запуск и завершение группы подов.

  • Управление жизненным циклом всего набора заданий.

  • Обнаружение сервисов между подами в наборе.

Юзкейс: распределённое обучение модели, где один главный под и четыре рабочих пода должны запускаться и завершаться вместе. JobSet обеспечивает координацию всех компонентов, что исключает простои дорогостоящих GPU.

apiVersion: jobset.x-k8s.io/v1alpha2
kind: JobSet
metadata:
  name: distributed-training
spec:
  replicatedJobs:
    - name: master
      replicas: 1
      template:
        spec:
          template:
            spec:
              containers:
              - name: training
                image: ml-training:latest
                resources:
                  limits:
                    nvidia.com/gpu: 1
    - name: worker
      replicas: 4
      template:
        spec:
          template:
            spec:
              containers:
              - name: training
                image: ml-training:latest
                resources:
                  limits:
                    nvidia.com/gpu: 2

Kueue: управление очередями для ресурсов

Kueue — это система управления очередями в Kubernetes, оптимизирующая использование дорогостоящих ресурсов через квотирование и приоритизацию.

Ключевые возможности

  • Управление квотами для различных типов GPU.

  • Разделение ресурсов между командами и проектами.

  • Приоритизация критических рабочих нагрузок.

  • Учёт различных типов оборудования (resource flavors).

Юзкейс: организация с несколькими командами ML, использующими общий пул GPU A100. Kueue гарантирует, что ни одна команда не монополизирует дорогостоящие ресурсы.

# Определение типов GPU
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: nvidia-a100
spec:
  nodeLabels:
    gpu-type: a100
---
# Создание кластерной очереди с квотами
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: ml-queue
spec:
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu"]
    flavors:
    - name: nvidia-a100
      resources:
      - name: nvidia.com/gpu
        nominalQuota: 16
---
# Очередь для команды ML
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  namespace: ml-team
  name: training-jobs
spec:
  clusterQueue: ml-queue

Совместное использование JobSet и Kueue

Юзкейс: распределённая тренировка модели, требующая 8 GPU. Kueue ставит эту задачу в очередь и выделяет ресурсы в соответствии с квотой команды, а JobSet обеспечивает, что все поды запускаются как единый блок.

apiVersion: jobset.x-k8s.io/v1alpha2
kind: JobSet
metadata:
  name: distributed-training
  namespace: ml-team
  annotations:
    kueue.x-k8s.io/queue-name: training-jobs  # Интеграция с Kueue
spec:
  replicatedJobs:
    - name: training-job
      replicas: 4
      template:
        spec:
          template:
            spec:
              containers:
              - name: training
                image: ml-model:latest
                resources:
                  limits:
                    nvidia.com/gpu: 2

Когда использовать

  • JobSet. Когда нужен координированный запуск нескольких подов для распределённого обучения, но не требуется полноценный gang scheduler типа Volcano, с возможностью запускать все поды в задаче одновременно.

  • Kueue. Когда необходимо управлять доступом разных команд к ограниченным GPU‑ресурсам, но не требуется сложная иерархия (как в YuniKorn).

  • JobSet + Kueue. Оптимальное решение для большинства организаций, которым требуется как координация заданий, так и справедливое распределение GPU‑ресурсов. Эта комбинация проще в настройке и использовании, чем полнофункциональные кастомные планировщики.

Custom Scheduling

Стандартные инструменты шедулинга K8S достаточны, когда есть:

  • Простые GPU‑задачи с минимальными требованиями.

  • Ограниченное количество типов GPU‑рабочих нагрузок.

  • Среда с доминированием одиночных подов.

  • Предсказуемые паттерны использования GPU.

Посмотрим, когда пригодятся различные полнофункциональные кастомные планировщики.

Volcano Scheduler

Volcano предоставляет фреймворк для высокопроизводительных вычислений (HPC) и задач машинного обучения.

Ключевые возможности Volcano Scheduler

  • Gang Scheduling. Обеспечивает одновременный запуск всех подов в задаче.

  • Очереди с приоритетами. Позволяет определять приоритеты для разных типов рабочих нагрузок.

  • Справедливое разделение ресурсов. Гарантирует доступ к ресурсам для разных команд.

Пример использования для тренировки модели на нескольких GPU

В этом примере Volcano гарантирует, что все четыре пода (1 parameter server и 3 workers) будут запущены одновременно, что критично для распределённого обучения.

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: distributed-training
spec:
  minAvailable: 4
  schedulerName: volcano
  plugins:
    env: []
    svc: []
  policies:
    - event: PodEvicted
      action: RestartJob
  tasks:
    - replicas: 1
      name: ps
      template:
        spec:
          containers:
            - image: tensorflow/tensorflow:gpu
              name: tensorflow
              resources:
                limits:
                  nvidia.com/gpu: 1
    - replicas: 3
      name: worker
      template:
        spec:
          containers:
            - image: tensorflow/tensorflow:gpu
              name: tensorflow
              resources:
                limits:
                  nvidia.com/gpu: 2

Apache YuniKorn

YuniKorn — это облачный планировщик для контейнерных рабочих нагрузок, ориентированный на оркестрацию ресурсов в больших кластерах.

Ключевые возможности Apache YuniKorn

  • Иерархические очереди. Сложная структура очередей с наследованием политик.

  • Резервация ресурсов. Способность резервировать ресурсы для будущих задач.

  • Управление квотами. Точный контроль использования ресурсов командами.

Пример конфигурации для обучения моделей

В этом сценарии обучения YuniKorn гарантирует справедливое распределение GPU‑ресурсов между разными задачами обучения с соблюдением квот для различных команд. Современный YuniKorn использует стандартные Kubernetes‑ресурсы с аннотациями.

# Job для обучения модели с YuniKorn аннотациями
apiVersion: batch/v1
kind: Job
metadata:
  name: inference-job
  namespace: ml-inference
  annotations:
    # Указываем очередь YuniKorn
    yunikorn.apache.org/queue: root.engineering.ml-team
    # Включаем gang scheduling для координированного запуска подов
    yunikorn.apache.org/task-group-name: inference-workers
    yunikorn.apache.org/task-groups: |-
      [{
        "name": "inference",
        "minMember": 4,
        "minResource": {
          "nvidia.com/gpu": 4,
          "memory": "32Gi",
          "cpu": "16"
        },
        "nodeSelector": {
          "gpu-type": "tesla-v100"
        },
        "tolerations": [{
          "key": "dedicated",
          "operator": "Equal",
          "value": "ml-training",
          "effect": "NoSchedule"
        }]
      }]
spec:
  parallelism: 4
  completions: 4
  template:
    metadata:
      labels:
        app: inference
        yunikorn.apache.org/task-group-name: inference-workers
    spec:
      schedulerName: yunikorn
      restartPolicy: Never
      containers:
      - name: inference
        image: inference-server:latest
        resources:
          requests:
            nvidia.com/gpu: 1
            memory: "8Gi"
            cpu: "4"
          limits:
            nvidia.com/gpu: 1
            memory: "8Gi"
            cpu: "4"

KAI-Scheduler

KAI‑Scheduler специализируется на рабочих нагрузках AI/ML с глубоким пониманием топологии GPU и требований пайплайнов обучения.

Ключевые возможности KAI-Scheduler

  • Топологическая оптимизация. Размещает поды с учетом NVLink и других соединений между GPU.

  • Прогнозирование использования GPU. Анализирует шаблоны использования для оптимального планирования.

  • Приоритизация на основе SLA. Учитывает SLA для критичных задач ML.

Пример для обучения большой модели

В этом примере KAI‑Scheduler размещает задачу обучения крупной языковой модели на узел с 8 GPU, которые соединены через NVLink, обеспечивая максимальную производительность межпроцессорного обмена данными.

apiVersion: scheduling.kai.io/v1
kind: GPUTask
metadata:
  name: llm-training
spec:
  schedulerName: kai-scheduler
  priority: high
  topologyAware: true
  gpuCount: 8
  nvlinkRequired: true
  template:
    spec:
      containers:
      - name: training
        image: llm-training:latest
        resources:
          limits:
            nvidia.com/gpu: 8
            memory: "128Gi"

Граничные случаи и рекомендации

Оправдано использование стандартного шедулера

  1. Небольшой кластер (менее 50 GPU) с предсказуемой нагрузкой.

  2. Однородные GPU‑задачи без особых требований к межузловой коммуникации.

  3. Чёткое разделение ресурсов между командами на основе выделенных пулов узлов.

  4. Отсутствие сложных пайплайнов с множественными зависимостями.

  5. Отсутствие строгих требований к одновременному запуску групп подов.

Пора переходить на кастомный шедулер

  1. JobSet + Kueue:
    — Требуется координированный запуск групп подов для распределённого обучения, но полноценный gang‑scheduler избыточен.
    — Необходимо справедливое распределение GPU‑ресурсов между командами без сложной иерархии.
    — Нужна простая в настройке система управления очередями с квотированием.
    — Важна интеграция со стандартным планировщиком Kubernetes без замены базовой функциональности.

  2. Volcano:
    — Часто блокируются распределённые тренировки из‑за нехватки ресурсов.
    — Появляются жалобы на несправедливое распределение GPU между командами.
    — Требуется запускать множество зависимых подов как единую задачу.

  3. YuniKorn:
    — Инфраструктура масштабируется до сотен GPU с десятками команд.
    — Необходимо динамическое перераспределение квот между подразделениями.
    — Появляется потребность в сложной иерархии доступа к ресурсам.

  4. KAI‑Scheduler:
    — Производительность критически зависит от конкретных GPU‑характеристик.
    — Значительные инвестиции в дорогие GPU (A100/H100) требуют максимальной эффективности.
    — Имеются сложные гетерогенные рабочие нагрузки с разными оптимальными конфигурациями.

Практические примеры перехода на кастомные шедулеры

Пример 1: От стандартного к Volcano

Исходная ситуация. Команда ML‑исследователей используют стандартный kube‑scheduler:

apiVersion: batch/v1
kind: Job
metadata:
  name: distributed-training
spec:
  parallelism: 4
  template:
    spec:
      containers:
      - name: training
        image: training:latest
        resources:
          limits:
            nvidia.com/gpu: 1

Проблема. Из‑за нехватки GPU только часть подов запускается, остальные остаются в состоянии Pending, блокируя уже запущенные поды, которые ожидают остальные.

Решение с Volcano:

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: distributed-training
spec:
  minAvailable: 4
  schedulerName: volcano
  tasks:
    - replicas: 4
      name: worker
      template:
        spec:
          containers:
          - name: training
            image: training:latest
            resources:
              limits:
                nvidia.com/gpu: 1

Результат. Поды либо запускаются все одновременно, либо ждут вместе, что устраняет проблему с блокировкой ресурсов и повышает общую утилизацию GPU в кластере.

Пример 2: От стандартного к YuniKorn

Исходная ситуация. Компания использует ручное разделение ресурсов через отдельные namespaces с ResourceQuotas:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-a-quota
  namespace: team-a
spec:
  hard:
    nvidia.com/gpu: 8
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-b-quota
  namespace: team-b
spec:
  hard:
    nvidia.com/gpu: 8

Проблема. Команда A часто не использует все выделенные GPU, в то время как команда B страдает от нехватки ресурсов и длительных очередей подов.

Решение с YuniKorn:

# Конфигурация YuniKorn
partitions:
  - name: default
    queues:
      - name: root
        queues:
          - name: team-a
            resources:
              guaranteed:
                nvidia.com/gpu: 4
              max:
                nvidia.com/gpu: 12
          - name: team-b
            resources:
              guaranteed:
                nvidia.com/gpu: 4
              max:
                nvidia.com/gpu: 12

Результат. Каждая команда получает гарантированный минимум GPU, но может использовать больше ресурсов, если они доступны. YuniKorn динамически выделяет неиспользуемые GPU команды A для команды B, повышая общую утилизацию ресурсов на 40%.

Пример 3: От Volcano к KAI-Scheduler

Исходная ситуация. Исследовательская лаборатория использует Volcano для обучения больших языковых моделей:

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: llm-training
spec:
  minAvailable: 8
  schedulerName: volcano
  tasks:
    - replicas: 8
      name: training
      template:
        spec:
          containers:
          - name: llm-container
            image: llm-training:latest
            resources:
              limits:
                nvidia.com/gpu: 1

Проблема. Тренировка выполняется неоптимально из‑за случайного размещения подов, которые могут оказаться на разных узлах с ограниченной межузловой пропускной способностью сети.

Решение с KAI‑Scheduler:

apiVersion: scheduling.kai.io/v1
kind: GPUWorkload
metadata:
  name: llm-training
spec:
  schedulerName: kai-scheduler
  topologyAware: true
  preferSameNode: true
  communicationPattern: allToAll
  template:
    spec:
      containers:
      - name: llm-container
        image: llm-training:latest
        resources:
          limits:
            nvidia.com/gpu: 8

Результат. KAI‑Scheduler размещает все 8 GPU для задачи на одном узле с NVLink‑соединениями, сокращая время обучения на 35% благодаря устранению узких мест в межузловой коммуникации.

Выводы и итоговые рекомендации

  1. Начинайте с простого. Для большинства рабочих ML‑нагрузок может быть достаточен оптимизированный через KubeSchedulerConfiguration стандартный планировщик с правильными настройками Node Affinity, Priority Class и Pod Topology Spread Constraints.

  2. Рассмотрите JobSet и Kueue как промежуточный шаг. Когда стандартного планировщика недостаточно, но полноценные кастомные решения избыточны, сочетание JobSet (для координации групп подов) и Kueue (для управления очередями) предоставляет эффективное решение с минимальными накладными расходами.

  3. Мониторьте узкие места. Отслеживайте метрики утилизации GPU, время ожидания в очередях и проблемы с блокировкой ресурсов, чтобы определить необходимость перехода на кастомный шедулер.

  4. Выбирайте шедулер по основной проблеме:

    — Volcano, когда главная проблема — gang‑scheduling и базовое управление очередями.

    — YuniKorn, когда требуется сложное иерархическое управление ресурсами на уровне предприятия.

    — KAI‑Scheduler, когда критически важна оптимизация производительности на уровне аппаратной топологии GPU.

  5. Следуйте эволюционному подходу:

    — Начните с оптимизации стандартного планировщика через KubeSchedulerConfiguration.

    — При необходимости добавьте JobSet для координации заданий и/или Kueue для управления очередями.

    — Переходите к полноценным кастомным планировщикам только при обоснованной необходимости.

  6. Начните с пилотного проекта. Внедряйте кастомный шедулер сначала для ограниченного набора рабочих нагрузок, постепенно расширяя его использование.

  7. Комбинируйте при необходимости. В очень сложных средах можно использовать несколько планировщиков для разных типов рабочих нагрузок. Например, кастомизированный стандартный планировщик с Kueue для инференса и Volcano для распределённых тренировок.
    Любые изменения в системе планирования должны быть обоснованы реальными потребностями и измеримыми улучшениями в эффективности использования ресурсов, производительности или управляемости инфраструктуры. Для большинства организаций оптимальным решением является постепенное наращивание сложности, начиная с настройки стандартных инструментов и переходя к специализированным решениям только при действительной необходимости.

Дополнительные материалы для изучения:

Комментарии (0)