
Привет, Хабр! Меня зовут Макарий, и как 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/hostnameTaint & 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