
Привет, Хабр! Меня зовут Макарий, и как Senior SRE в Yandex Cloud я не только участвовал в разработке Managed Service for Kubernetes, но и всегда любил в свободное время посмотреть, что интересного понавыпускали для «кубика». Kubernetes, как де‑факто стандарт оркестрации контейнеров, предлагает базовые механизмы для управления вычислительными ресурсами. Однако стандартный планировщик Kubernetes (kube‑scheduler) разрабатывался с учётом общих принципов балансировки нагрузки и не специализирован для уникальных особенностей рабочих GPU‑нагрузок.
Предлагаю рассмотреть весь спектр возможностей — от встроенных механизмов шедулинга K8s до специализированных планировщиков, таких как Volcano, Apache YuniKorn и KAI‑Scheduler. Проанализирую конкретные сценарии, в которых каждый из этих инструментов демонстрирует свои преимущества, и предложу рекомендации по выбору оптимального решения для ваших рабочих GPU‑нагрузок.
Что вы найдёте в статье:
Механизмы тонкой настройки размещения подов встроенными средствами:
— Node Selector
— Node Name
— Affinity/Anti‑Affinity
— Taint & Tolerations
— Pod Topology Spread Constraints
— Priority and Preemption
— Descheduler
— Комбинированный подход
— Кастомизация стандартного планировщикаПолнофункциональный кастомный шедулинг:
Что такое 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-оптимизации
RemoveDuplicates— удаляет дублирующиеся поды с одного узла.LowNodeUtilization— перемещает поды с недозагруженных узлов.HighNodeUtilization— освобождает перегруженные узлы.RemovePodsViolatingNodeAffinity— исправляет нарушения node affinity.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‑нагрузок без перехода на полностью кастомные решения. Ключевые преимущества этого подхода:
Минимальные изменения инфраструктуры. Не требуется устанавливать дополнительные компоненты или CRD.
Гибкость. Можно настроить различные аспекты планирования под конкретные GPU‑нагрузки.
Возможность нескольких профилей. Позволяет создать разные планировщики для разных типов GPU‑задач.
Совместимость. Стандартный компонент 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"
Граничные случаи и рекомендации
Оправдано использование стандартного шедулера
Небольшой кластер (менее 50 GPU) с предсказуемой нагрузкой.
Однородные GPU‑задачи без особых требований к межузловой коммуникации.
Чёткое разделение ресурсов между командами на основе выделенных пулов узлов.
Отсутствие сложных пайплайнов с множественными зависимостями.
Отсутствие строгих требований к одновременному запуску групп подов.
Пора переходить на кастомный шедулер
JobSet + Kueue:
— Требуется координированный запуск групп подов для распределённого обучения, но полноценный gang‑scheduler избыточен.
— Необходимо справедливое распределение GPU‑ресурсов между командами без сложной иерархии.
— Нужна простая в настройке система управления очередями с квотированием.
— Важна интеграция со стандартным планировщиком Kubernetes без замены базовой функциональности.Volcano:
— Часто блокируются распределённые тренировки из‑за нехватки ресурсов.
— Появляются жалобы на несправедливое распределение GPU между командами.
— Требуется запускать множество зависимых подов как единую задачу.YuniKorn:
— Инфраструктура масштабируется до сотен GPU с десятками команд.
— Необходимо динамическое перераспределение квот между подразделениями.
— Появляется потребность в сложной иерархии доступа к ресурсам.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% благодаря устранению узких мест в межузловой коммуникации.
Выводы и итоговые рекомендации
Начинайте с простого. Для большинства рабочих ML‑нагрузок может быть достаточен оптимизированный через
KubeSchedulerConfigurationстандартный планировщик с правильными настройками Node Affinity, Priority Class и Pod Topology Spread Constraints.Рассмотрите JobSet и Kueue как промежуточный шаг. Когда стандартного планировщика недостаточно, но полноценные кастомные решения избыточны, сочетание JobSet (для координации групп подов) и Kueue (для управления очередями) предоставляет эффективное решение с минимальными накладными расходами.
Мониторьте узкие места. Отслеживайте метрики утилизации GPU, время ожидания в очередях и проблемы с блокировкой ресурсов, чтобы определить необходимость перехода на кастомный шедулер.
Выбирайте шедулер по основной проблеме:
— Volcano, когда главная проблема — gang‑scheduling и базовое управление очередями.
— YuniKorn, когда требуется сложное иерархическое управление ресурсами на уровне предприятия.
— KAI‑Scheduler, когда критически важна оптимизация производительности на уровне аппаратной топологии GPU.
Следуйте эволюционному подходу:
— Начните с оптимизации стандартного планировщика через
KubeSchedulerConfiguration.— При необходимости добавьте JobSet для координации заданий и/или Kueue для управления очередями.
— Переходите к полноценным кастомным планировщикам только при обоснованной необходимости.
Начните с пилотного проекта. Внедряйте кастомный шедулер сначала для ограниченного набора рабочих нагрузок, постепенно расширяя его использование.
Комбинируйте при необходимости. В очень сложных средах можно использовать несколько планировщиков для разных типов рабочих нагрузок. Например, кастомизированный стандартный планировщик с Kueue для инференса и Volcano для распределённых тренировок.
Любые изменения в системе планирования должны быть обоснованы реальными потребностями и измеримыми улучшениями в эффективности использования ресурсов, производительности или управляемости инфраструктуры. Для большинства организаций оптимальным решением является постепенное наращивание сложности, начиная с настройки стандартных инструментов и переходя к специализированным решениям только при действительной необходимости.
Дополнительные материалы для изучения:
Доклад на Cloud Native Rejekts 2023: Descheduler: Your pods in the right place, all the time
Доклад на KubeCon + CloudNativeCon 2025: A Comparative Analysis of Kueue, Volcano, and YuniKorn
Статья о KAI Scheduler в блоге NVIDIA: NVIDIA Open Sources Run:ai Scheduler to Foster Community Collaboration
