Harmonogramowanie zadań w klastrach AI: SLURM, Kubernetes, Ray i moment, w którym żaden z nich nie jest potrzebny
Scheduler decyduje, które zadanie trafia na które GPU, kiedy i na czyj koszt. Bez niego wspólny klaster degeneruje się do pisania po Slacku z pytaniem, czy ktoś właśnie używa węzła 3. Ze złym schedulerem na YAML idzie więcej roboczogodzin inżynierów niż na pracę nad modelem.
Ten artykuł porównuje schedulery, po które naprawdę się sięga — SLURM, Kubernetes (z Kueue lub Volcano), Ray i KubeRay oraz komercyjne Run:ai i Determined — i mówi wprost o przypadku, w którym właściwą odpowiedzią jest „nic, wystarczy SSH”. Czytelnik zna już K01, K02 i K03 i zastanawia się teraz, co położyć na klastrze liczącym od 1 do 32 węzłów.
Co scheduler naprawdę robi
Trzy zadania, mniej więcej w kolejności trudności: rozmieścić pracę na wolnych zasobach, zakolejkować tę, która jeszcze się nie mieści, i skoordynować zadania wielowęzłowe — trening rozproszony wymaga, aby wszystkie N rang wystartowały jednocześnie (gang scheduling), ponieważ PyTorchowe dist.barrier() blokuje się w nieskończoność, jeśli któraś ranga nigdy się nie pojawi (patrz K06).
Wszystko powyżej klastra jednego zespołu wymaga dodatkowo wielodostępnych limitów z pożyczaniem, wygasania fair-share między zespołami, wywłaszczania i poprawnego rozliczania GPU (tak aby zadanie widziało CUDA_VISIBLE_DEVICES ustawione prawidłowo). Każdy z opisanych niżej schedulerów realizuje pierwsze trzy punkty. Różnią się dopiero drugą listą oraz tym, jaki rodzaj pracy zakładają.
Wielki podział: zadania wsadowe kontra usługi długodziałające
To najważniejsze rozróżnienie w całym temacie. Zadania wsadowe trenują przez 8 godzin i kończą pracę, przemiatają hiperparametry w 200 krótkich przebiegach albo przetwarzają zbiór danych przez noc — start, koniec, wynik, nic nie odpowiada po HTTP. Usługi długodziałające to endpoint vLLM obsługujący inferencję 24/7, baza pamięci scen, stos monitoringu — mają działać bez końca i wstawać po awarii.
SLURM powstał do pracy wsadowej. Kubernetes powstał do usług długodziałających. Cała reszta to naciąganie jednego z nich do roli drugiego.
| Obciążenie | SLURM | Kubernetes |
|---|---|---|
| 8-godzinny przebieg treningowy | Natywnie | Wymaga Kueue lub Volcano |
| Przemiatanie hiperparametrów w 200 zadaniach | Natywnie | Niewygodnie |
| Endpoint inferencyjny vLLM 24/7 | Niewygodnie | Natywnie |
| Mieszanka: trening + inferencja + narzędzia | Boleśnie | Natywnie (z Kueue) |
| Rozproszony trening wielowęzłowy | Natywnie (gang) | Wymaga wtyczki gang |
| Auto-restart usług po awarii | Nie | Natywnie |
Jeśli klaster robi tylko jedną z tych rzeczy, wybór jest prosty. Jeśli obie — a to coraz częstszy przypadek — pytanie brzmi, za który scheduler jest się gotowym zapłacić podatkiem operacyjnym.
SLURM — standard HPC, który wciąż wygrywa przy czystym treningu
SLURM obsługuje ponad 65% superkomputerów z listy TOP500 i większość opublikowanych przebiegów treningowych na granicy możliwości. Dla obciążeń typu „zgłoś zadanie treningowe, poczekaj na wyniki, odbierz checkpoint” od dwudziestu lat jest właściwą odpowiedzią.
Model myślowy jest prosty: partycja to nazwana pula węzłów (gpu-5090, cpu-only, bigmem); zadanie to skrypt powłoki z dyrektywami #SBATCH, zgłaszany poleceniem sbatch; GRES (Generic RESources) śledzi GPU — --gres=gpu:rtxpro6000:2 prosi o dwie konkretne karty; gang scheduling jest wbudowany.
Minimalny plik slurm.conf dla 4-węzłowego klastra z 8× RTX Pro 6000 Blackwell na węzeł:
ClusterName=kentino-lab
SlurmctldHost=head01
SchedulerType=sched/backfill
SelectType=select/cons_tres
SelectTypeParameters=CR_Core_Memory
GresTypes=gpu
# GPU accounting — without these two lines, fair-share runs on CPU-seconds and is meaningless
AccountingStorageType=accounting_storage/slurmdbd
AccountingStorageTRES=gres/gpu
PriorityType=priority/multifactor
PriorityDecayHalfLife=7-0
PriorityWeightFairshare=10000
PriorityWeightAge=1000
# cgroups isolate the GPUs a job is allowed to see
ProctrackType=proctrack/cgroup
NodeName=node[01-04] CPUs=128 RealMemory=1024000 Sockets=2 \
CoresPerSocket=64 ThreadsPerCore=1 Gres=gpu:rtxpro6000:8 State=UNKNOWN
PartitionName=train Nodes=node[01-03] Default=YES MaxTime=48:00:00 State=UP
PartitionName=interactive Nodes=node04 MaxTime=04:00:00 \
PriorityTier=10 State=UP
Odpowiadający mu plik gres.conf na każdym węźle:
AutoDetect=nvml
NodeName=node[01-04] Name=gpu Type=rtxpro6000 File=/dev/nvidia[0-7]
AutoDetect=nvml odpytuje bezpośrednio NVIDIA Management Library, automatycznie wykrywa plastry MIG i oszczędza ręcznego wpisywania ścieżek do plików urządzeń.
Użytkownik zgłasza zadanie:
#!/bin/bash
#SBATCH --job-name=qwen-finetune
#SBATCH --partition=train
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=8
#SBATCH --gres=gpu:rtxpro6000:8
#SBATCH --time=12:00:00
#SBATCH --output=logs/%j.out
srun python -m torch.distributed.run \
--nnodes=2 --nproc-per-node=8 \
--rdzv-backend=c10d \
--rdzv-endpoint=$(scontrol show hostnames | head -1):29500 \
train.py --config configs/qwen72b.yaml
PyTorch Elastic i SLURM składają się czysto: SLURM zarządza alokacją, srun uruchamia jeden proces na zadanie, a torch.distributed.run wraz z backendem rendezvous przydziela rangi. Przy utracie węzła --max-restarts w torchrun oraz --requeue w #SBATCH pozwalają wznowić pracę z ostatniego checkpointu. Wszyscy robią to właśnie tak.
Za darmo otrzymuje się: gang scheduling, wygasanie fair-share, backfill, rozliczanie (każda GPU-sekunda zapisana w sacct) oraz prostą semantykę — żadnych CRD, kontrolera ani grafu obiektów YAML. Czego nie ma: usług długodziałających z auto-restartem, ingressu HTTP, aktualizacji kroczących, kontenerów sidecar, stanu deklaratywnego. SLURM jest schedulerem wsadowym. Traktowanie go jak Kubernetesa zaboli.
SLURM jest właściwym wyborem, gdy: uruchamiane są treningi, inferencja wsadowa lub symulacje; użytkownicy piszą skrypty powłoki; do obsługi wystarczy jeden inżynier SRE na część etatu.
Gdzie SLURM trafia na sufit
W trzech miejscach. Usługi w stylu webowym — brak pojęcia „działaj bez końca, wstań po awarii, wystaw port 8000”. Da się wcisnąć vLLM w długodziałające zadanie SLURM, ale health-check, aktualizacje kroczące i równoważenie obciążenia stają się problemem zespołu. Obciążenia heterogeniczne — SLURM zakłada jedną pulę zasobów; klaster, na którym działa trening + inferencja + Jupyter + monitoring + przygotowanie danych, potrzebuje drobniejszej kontroli. Ścisła izolacja wielodostępna — cgroups izolują CPU i pamięć; izolacja GPU opiera się na CUDA_VISIBLE_DEVICES i na tym, że kod użytkownika tę zmienną respektuje. Brak przestrzeni nazw, brak polityk sieciowych na zadanie, brak izolacji klasy kontenerowej. W laboratorium wystarczy; w hostowanej usłudze dla wielu klientów już nie.
Po natrafieniu na którykolwiek z tych limitów właściwym ruchem jest zwykle dołożenie drugiej warstwy — Kubernetesa do usług — zamiast męczenia SLURM-a. SUNK od CoreWeave i zarządzane oferty z 2026 roku uruchamiają SLURM na Kubernetesie, dzięki czemu ten sam klaster robi jedno i drugie. W skali Kentino dwa osobne małe klastry są często prostsze.
Kubernetes dla AI: droga cloud-native
Kubernetes powstał po to, by utrzymywać bezstanowe usługi webowe. W klastrach AI „cała reszta” jest doklejana. Stos na rok 2026: NVIDIA GPU Operator (sterowniki, CUDA, NCCL, DCGM, wtyczka urządzeń, konfiguracja MIG), Kueue (kolejka, limity, dopuszczanie zadań), Volcano (scheduler świadomy pracy wsadowej), Kubeflow Trainer (CRD-y PyTorchJob, TFJob, MPIJob), KubeRay (Ray na K8s) oraz Karpenter lub Cluster Autoscaler do provisioningu węzłów.
Dlaczego naiwny Kubernetes nie nadaje się do AI: domyślny kube-scheduler działa w trybie FIFO i nie zna semantyki gang. Bez oporu uruchomi 7 z 8 podów rozproszonego zadania treningowego i zostawi ósmy w stanie Pending, podczas gdy działająca siódemka blokuje bezczynne GPU. To nie jest teoria — to najczęstszy błąd zespołów, które próbują uruchomić trening na klastrze zbudowanym pod inferencję. Lekarstwem są Kueue (dopuszczanie na poziomie zadania) i Volcano (gangowe rozmieszczanie podów). Obecna dobra praktyka to jedno i drugie: Kueue na górze, Volcano pod spodem.
Volcano
Volcano to wsadowy scheduler z CNCF, instalowany obok kube-schedulera albo zamiast niego. Dodaje prawdziwy gang scheduling z semantyką minAvailable (dopuść zero albo N, nigdy część), priorytety kolejek i wywłaszczanie, strategie fair-share / binpack / świadome topologii oraz pełnoprawne wsparcie dla PyTorchJob, TFJob, MPIJob, RayJob i SparkApplication.
Minimalna kolejka plus zadanie treningowe PyTorch z gang schedulingiem:
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: training
spec:
weight: 4
capability:
nvidia.com/gpu: 32
---
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: qwen-finetune
spec:
schedulerName: volcano
minAvailable: 16 # gang: all 16 ranks or zero
queue: training
policies:
- event: PodEvicted
action: RestartJob
tasks:
- replicas: 16
name: worker
template:
spec:
containers:
- name: pytorch
image: kentino/pytorch:2.5-cuda13
resources:
limits:
nvidia.com/gpu: 1
command: ["torchrun", "--nnodes=16", "--nproc-per-node=1", "train.py"]
Kiedy Volcano nie jest potrzebne: czysto inferencyjny klaster z niezależnymi Deploymentami vLLM. Każdy pod ma własne GPU, brak koordynacji między podami, domyślny kube-scheduler w zupełności wystarczy. Volcano zaczyna zarabiać na siebie w chwili dołożenia do klastra treningu rozproszonego.
Kueue i pożyczanie limitów
Kueue obsługuje warstwę, której Volcano nie dotyka: kto i ile klastra może zużyć oraz co się dzieje, gdy jeden zespół nic nie liczy.
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
name: rtxpro6000
spec:
nodeLabels:
nvidia.com/gpu.product: "RTX-PRO-6000-Blackwell"
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
name: team-research
spec:
cohort: "shared-gpu"
resourceGroups:
- coveredResources: ["nvidia.com/gpu"]
flavors:
- name: rtxpro6000
resources:
- name: "nvidia.com/gpu"
nominalQuota: 16
borrowingLimit: 8
Oba zespoły w kohorcie mają gwarantowane 16 GPU i mogą pożyczyć do 8 z bezczynnej puli drugiego. Praca o wysokim priorytecie wywłaszcza pożyczoną pojemność. Mapa drogowa Kueue na 2026 rok koncentruje się na MultiKueue (rozdzielaniu zadań między klastry) i na wywłaszczaniu kooperacyjnym — zadaniu zapisującemu checkpointy można powiedzieć „zapisz stan i ustąp w ciągu 60 sekund” zamiast ubijać je od razu.
MIG i współdzielenie GPU
Wtyczka urządzeń NVIDIA domyślnie przydziela całe GPU. Przy treningu i dużej inferencji to poprawne; przy małej inferencji, notatnikach czy pracy deweloperskiej — marnotrawne. Trzy tryby współdzielenia: MIG (sprzętowa izolacja pamięci i awarii, produkcyjna inferencja wielodostępna), MPS (kooperacyjny, bez izolacji), time-slicing (bez izolacji, tylko do prac deweloperskich).
MIG występuje w H100/H200, A100 i B200 — w żadnym GPU z oferty Kentino. RTX 5090, RTX 4090, RTX Pro 6000 Blackwell, L40 ani L4 nie obsługują MIG. Jeśli projekt wymaga sprzętowo izolowanych partycji GPU, zawęża to sprzęt do kart SXM/datacenter, których Kentino nie buduje. Time-slicing ogłasza N wirtualnych GPU na jedno fizyczne, a scheduler nie ma pojęcia, że są przesubskrybowane — stosować wyłącznie w pracach deweloperskich.
W czym Kubernetes jest dobry: długodziałające usługi inferencyjne (Deploymenty vLLM skalowane przez HPA według długości kolejki, aktualizacje kroczące, health-checki — SLURM nie ma nic porównywalnego), obciążenia heterogeniczne na jednym klastrze, ekosystem (Prometheus, Grafana, Argo). Koszt: działający stos K8s + GPU Operator + Kueue + Volcano + Kubeflow to minimum 20 komponentów. Realistyczna ocena: Kubernetes oznacza 5–10× większe obciążenie operacyjne niż SLURM przy czystym treningu wsadowym.
Kubernetes jest właściwym wyborem, gdy: klaster obsługuje usługi inferencyjne obok treningu, firma ma zespół platformowy, liczy się deklaratywność wszystkiego albo potrzebna jest przenośność między klastrami.
Ray i KubeRay — obliczenia rozproszone natywne dla Pythona
Ray to zupełnie inne zwierzę. Nie jest schedulerem klastra w rozumieniu SLURM-a czy K8s — to rozproszone środowisko uruchomieniowe Pythona, które przynosi ze sobą scheduler, magazyn obiektów i autoskaler. Kod w Pythonie pisze się z dekoratorami @ray.remote, a Ray sam ustala, gdzie uruchomić każde zadanie.
Gdzie Ray pasuje: strojenie hiperparametrów (Ray Tune — setki prób równolegle, wczesne odcinanie słabych; zastosowanie, do którego Ray powstał i w którym wciąż jest najlepszy), uczenie ze wzmocnieniem (RLlib — środowiska i uczące się agenty jako aktorzy, co czysto odwzorowuje się na Raya, a słabo na wsadowe zadania SLURM-a), trening rozproszony (Ray Train, opakowujący PyTorch DDP / FSDP / DeepSpeed), serwowanie modeli (Ray Serve, natywny dla Pythona, własne potoki wielomodelowe) oraz wstępne przetwarzanie danych (Ray Data). Czym Ray nie jest: wielodostępnym schedulerem wsadowym dla współdzielonego klastra. Ray zakłada, że klaster należy do jednej aplikacji.
KubeRay to operator uruchamiający Raya na Kubernetesie. Trzy CRD: RayCluster (długowieczny), RayJob (jednorazowy), RayService (Ray Serve, aktualizacje kroczące). Minimalny RayCluster:
apiVersion: ray.io/v1
kind: RayCluster
metadata:
name: rl-cluster
spec:
rayVersion: '2.55.0'
headGroupSpec:
rayStartParams: { dashboard-host: '0.0.0.0' }
template:
spec:
containers:
- name: ray-head
image: rayproject/ray:2.55.0-py311-cu125
resources:
limits: { cpu: 8, memory: 32Gi }
workerGroupSpecs:
- groupName: gpu-workers
replicas: 4
minReplicas: 0
maxReplicas: 8
rayStartParams: {}
template:
spec:
containers:
- name: ray-worker
image: rayproject/ray:2.55.0-py311-cu125
resources:
limits:
cpu: 16
memory: 128Gi
nvidia.com/gpu: 4
KubeRay skaluje grupę workerów między minReplicas a maxReplicas na podstawie tego, ile zadań czeka według Raya. W połączeniu z Kueue (gang scheduling RayJob + Kueue jest udokumentowany i działa) otrzymuje się wielodostępnego Raya na wielodostępnym klastrze K8s — produkcyjny układ, na którym faktycznie pracuje większość zespołów opartych na Rayu.
Kiedy sam Ray (bez K8s) ma sens: małe laboratorium badawcze, w którym klaster należy do jednej osoby, dynamika liczy się bardziej niż wielodostępność, a obciążenie opiera się głównie na RL lub przeszukiwaniu hiperparametrów. Hybryda, do której dochodzi większość zespołów: SLURM albo K8s jako warstwa bazowa, która zarządza węzłami i wielodostępnością; Ray uruchamiany wewnątrz zadania SLURM lub przestrzeni nazw K8s na czas trwania obciążenia jednego użytkownika. Raya nie należy instalować jako schedulera najwyższego poziomu.
Run:ai i Determined — półka komercyjna
Dwie płatne oferty pojawiają się na tyle często, że warto je omówić. Obie celują w ten sam ból: K8s + Volcano + Kueue + GPU Operator to mnóstwo YAML-a, a część organizacji woli kupić, niż budować.
NVIDIA Run:ai (przejęty w 2024, przemianowany w 2025) to scheduler GPU natywny dla K8s. Frakcjonowanie GPU dzieli pojedyncze GPU między obciążenia na poziomie pamięci — 0.5 GPU w żądaniach zasobów, dynamiczna zmiana rozmiaru, bin-packing. Run:ai zarabia na swoją cenę w środowiskach korporacyjnych, w których o wspólne GPU rywalizuje 10 i więcej zespołów ML. Poniżej tego progu Kueue + GPU Operator pokrywa większość tego samego obszaru za darmo.
Determined.AI (HPE, 2021) to zarządzana platforma treningowa — śledzenie eksperymentów, przeszukiwanie hiperparametrów, zarządzanie checkpointami i trening rozproszony w jednym produkcie. Najlepiej pasuje do zespołów badawczych, które chcą dopracowanego panelu bez samodzielnego integrowania pięciu narzędzi.
Problem interaktywnych notatników
Schemat, który dopada niemal każdy współdzielony klaster GPU: badacze chcą dostępu do GPU przez JupyterHub na potrzeby pracy deweloperskiej, a to musi współistnieć z długimi zadaniami treningowymi zajmującymi całe węzły. Naiwna odpowiedź — jedno dedykowane GPU na notatnik — marnuje 80% klastra. Odpowiedź rygorystyczna — zmusić badaczy, by wszystko zgłaszali przez sbatch — przepędza ich na laptopy z modelami-zabawkami.
Trzy działające wzorce: SLURM salloc + Jupyter na alokacji (salloc --gres=gpu:1 --time=4:00:00, uruchomienie serwera Jupyter na przydzielonym węźle i tunel; twardy limit czasu, GPU poprawnie rozliczone — najczystsza odpowiedź dla klastra SLURM), Kubernetes + JupyterHub + Kueue (pody notatników przez kolejkę o niskim priorytecie, zadania treningowe wywłaszczają bezczynne notatniki) albo Run:ai z frakcjonowaniem GPU (każdy notatnik dostaje 0.25 / 0.5 GPU). Niezależnie od wyboru sesjom interaktywnym trzeba nałożyć limit czasu — notatnik bez limitu to GPU trwale wycofane z klastra.
Szczera rzeczywistość małych zespołów
Większość materiałów o schedulerach zakłada klaster z 256 GPU i zespół platformowy. Realny klient Kentino to raczej 1–4 węzły, łącznie 4–32 GPU i 2–6 użytkowników, którzy się znają i dogadują na Slacku. Przy takim układzie scheduler nie jest potrzebny.
# user 1
ssh node01
tmux new -s my-training
CUDA_VISIBLE_DEVICES=0,1,2,3 python train.py
# Ctrl-B D to detach
# user 2 — coordinates on Slack first
ssh node01
nvidia-smi # which GPUs are free?
tmux new -s other-training
CUDA_VISIBLE_DEVICES=4,5,6,7 python train.py
To cały system planowania zadań. SLURM zaczyna się zwracać mniej więcej przy 16 GPU albo 8 użytkownikach, zależnie co nastąpi pierwsze. Poniżej tego progu narzut operacyjny przewyższa korzyść. Scheduler należy wdrożyć wtedy, gdy koordynacja między ludźmi przestaje działać — nie wcześniej.
Kiedy które narzędzie pasuje — podsumowanie
| Scenariusz | Rekomendacja |
|---|---|
| 1–2 węzły, 4–16 GPU, 2–6 użytkowników, którzy ze sobą rozmawiają | SSH + tmux + nvidia-smi. Bez schedulera. |
| Laboratorium badawcze, 4–32 węzły, wsadowe zadania treningowe | SLURM. Nudny, sprawdzony, pasuje. |
| Platforma inferencyjna obsługująca ruch klientów | Kubernetes + GPU Operator. Bez schedulera wsadowego. |
| Klaster mieszany: trening + inferencja + narzędzia | Kubernetes + Kueue + Volcano + Kubeflow. |
| Ciężki rozproszony Python: RL, przeszukiwanie hiperparametrów | Ray (lub KubeRay) na SLURM-ie albo K8s. |
| 10+ zespołów ML rywalizujących o GPU, budżet na narzędzia | Run:ai na Kubernetesie. |
| Zarządzane eksperymenty treningowe + śledzenie | Determined.AI. |
| 200+ GPU, wiele zespołów, wiele rodzajów obciążeń | Federacyjnie: SLURM do wsadu, K8s do usług. |
Co zrobić dalej — drzewo decyzyjne
Prosimy przejść je po kolei. Pierwsze „tak” kończy rozmowę.
- Mniej niż 16 GPU i mniej niż 8 użytkowników, którzy ze sobą rozmawiają? Tak → bez schedulera. Konwencje wystarczy opisać w pliku markdown. Do tematu można wrócić, gdy koordynacja zacznie zawodzić.
- Czy klaster obsługuje długodziałające usługi inferencyjne obok treningu? Tak → bazą jest Kubernetes. Do tego GPU Operator, potem Kueue, a następnie Volcano, jeśli w grę wchodzi trening rozproszony. Nie → bazą jest SLURM.
- Czy jest zespół platformowy albo budżet na niego (0.5–1.0 FTE przez sześć miesięcy, 0.25 FTE na stałe)? Nie → pozostać przy SLURM-ie niezależnie od miksu obciążeń. Podatek od K8s jest realny.
- >10 zespołów rywalizujących o czas GPU przy ścisłych wymaganiach co do limitów? Tak → warto ocenić Run:ai. Nie → Kueue plus kohorty załatwiają 80% sprawy.
-
Obciążenie oparte głównie na RL, przeszukiwaniu hiperparametrów lub rozproszonym Pythonie? Tak → Ray (KubeRay na K8s albo
salloc+ Ray na SLURM-ie). Nie → Raya zostawić na boku. - Czy sprzęt obsługuje MIG (karty datacenter)? W ofercie Kentino nie → planować przydział całych GPU albo time-slicing wyłącznie w pracach deweloperskich. Nie projektować wokół MIG.
Większość rozmów w Kentino kończy się na kroku 1, 2 lub 3. Reszta to dekoracja.
Artykuły towarzyszące: trening rozproszony w K02, klastry inferencyjne w K03, pamięć masowa klastra w K04, obsługa awarii w K06, sufit przepustowości PCIe w K07.
To część Kentino Wiki, serii referencyjnej o obliczeniach AI, robotyce i systemach, które je łączą. Komentarze i sprostowania prosimy kierować na info@kentino.com.