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ę.

  1. 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ć.
  2. 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.
  3. 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.
  4. >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.
  5. 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.
  6. 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.