Ordonnancement des jobs pour clusters IA : SLURM, Kubernetes, Ray — et savoir quand vous n'avez besoin d'aucun d'eux
Un ordonnanceur décide quel travail s'exécute sur quels GPU, à quel moment et aux frais de qui. Sans lui, un cluster partagé dégénère en messages Slack pour demander si quelqu'un utilise le nœud 3. Avec le mauvais ordonnanceur, vous passez plus d'heures d'ingénierie sur du YAML que sur vos modèles.
Cet article compare les ordonnanceurs réellement retenus — SLURM, Kubernetes (avec Kueue ou Volcano), Ray et KubeRay, ainsi que les offres commerciales Run:ai et Determined — et assume le cas où la bonne réponse est « rien, juste SSH ». Le lecteur a déjà lu K01, K02 et K03 et cherche maintenant quoi poser au-dessus d'un cluster de 1 à 32 nœuds.
Ce que fait réellement un ordonnanceur
Trois tâches, à peu près par ordre de difficulté : placer le travail sur les ressources libres, mettre en file d'attente celui qui ne tient pas encore, et coordonner les travaux multi-nœuds — l'entraînement distribué exige que les N rangs démarrent tous en même temps (gang scheduling), car un dist.barrier() PyTorch bloque indéfiniment si un rang ne se présente jamais (voir K06).
Au-delà d'un cluster mono-équipe, il faut aussi des quotas multi-locataires avec emprunt, une décroissance de l'équité entre équipes, la préemption et une comptabilité GPU correcte (pour que le travail voie CUDA_VISIBLE_DEVICES correctement défini). Tous les ordonnanceurs ci-dessous assurent les trois premiers points. Ils divergent sur la seconde liste et sur le type de travail qu'ils supposent.
La grande séparation : travaux par lots ou services permanents
C'est le cadrage le plus important. Les travaux par lots entraînent pendant 8 heures puis se terminent, exécutent un balayage de 200 courts travaux d'hyperparamètres ou traitent un jeu de données pendant la nuit — début, fin, résultat, rien ne répond en HTTP. Les services permanents sont un point de terminaison vLLM servant de l'inférence 24h/24 et 7j/7, une base de mémoire de scène, une pile de supervision — censés rester en ligne indéfiniment et redémarrer après un plantage.
SLURM a été conçu pour le traitement par lots. Kubernetes a été conçu pour les services permanents. Tout le reste n'est que l'un des deux étiré pour faire la moitié de l'autre.
| Charge de travail | SLURM | Kubernetes |
|---|---|---|
| Entraînement de 8 heures | Natif | Nécessite Kueue ou Volcano |
| Balayage de 200 jeux d'hyperparamètres | Natif | Peu commode |
| Point de terminaison d'inférence vLLM 24h/24 et 7j/7 | Peu commode | Natif |
| Mixte : entraînement + inférence + outillage | Pénible | Natif (avec Kueue) |
| Entraînement distribué multi-nœuds | Natif (gang) | Nécessite un greffon gang |
| Redémarrage automatique des services plantés | Non | Natif |
Si votre cluster ne fait que l'une de ces choses, le choix est simple. S'il fait les deux — ce qui devient la norme — la question est de savoir sur quel ordonnanceur vous acceptez de payer la taxe opérationnelle.
SLURM — la référence HPC qui reste gagnante pour l'entraînement pur
SLURM fait tourner plus de 65 % des supercalculateurs du TOP500 et la plupart des entraînements de pointe publiés. Pour les charges de travail du type « je soumets un entraînement, j'attends les résultats, je récupère le point de contrôle », c'est la bonne réponse depuis vingt ans.
Le modèle mental est simple : une partition est un pool de nœuds nommé (gpu-5090, cpu-only, bigmem) ; un travail est un script shell avec des directives #SBATCH, soumis via sbatch ; les GRES (Generic RESources) comptabilisent les GPU — --gres=gpu:rtxpro6000:2 demande deux cartes précises ; le gang scheduling est intégré.
Un fichier slurm.conf minimal pour un cluster de 4 nœuds à 8 × RTX Pro 6000 Blackwell par nœud :
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
Le fichier gres.conf correspondant sur chaque nœud :
AutoDetect=nvml
NodeName=node[01-04] Name=gpu Type=rtxpro6000 File=/dev/nvidia[0-7]
AutoDetect=nvml interroge directement la NVIDIA Management Library, détecte automatiquement les tranches MIG et vous évite d'écrire à la main les chemins des fichiers de périphérique.
Un utilisateur soumet :
#!/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 et SLURM se combinent proprement : SLURM détient l'allocation, srun lance un processus par tâche, torch.distributed.run et le backend de rendez-vous gèrent l'attribution des rangs. En cas de perte d'un nœud, --max-restarts sur torchrun combiné à --requeue dans le #SBATCH permet de repartir du dernier point de contrôle. Tout le monde procède ainsi.
Ce que vous obtenez sans effort : gang scheduling, décroissance de l'équité, backfill, comptabilité (chaque GPU-seconde consignée dans sacct), et une sémantique simple — pas de CRD, pas de contrôleur, pas de graphe d'objets YAML. Ce que vous n'obtenez pas : des services permanents avec redémarrage automatique, l'ingress HTTP, les mises à jour progressives, les conteneurs sidecar, l'état déclaratif. SLURM est un ordonnanceur par lots. Le traiter comme Kubernetes fera mal.
SLURM convient quand : vous faites de l'entraînement, de l'inférence par lots ou de la simulation ; vos utilisateurs écrivent des scripts shell ; un SRE à temps partiel suffit à l'exploiter.
Où SLURM atteint ses limites
Trois endroits. Les services de type web — aucune notion de « tourner indéfiniment, redémarrer en cas d'arrêt, exposer le port 8000 ». Vous pouvez faire entrer vLLM de force dans un travail SLURM permanent, mais les contrôles de santé, les mises à jour progressives et la répartition de charge deviennent votre problème. Les charges de travail hétérogènes — SLURM suppose un seul pool de ressources ; un cluster qui mêle entraînement, inférence, Jupyter, supervision et préparation de données réclame des contrôles plus fins. L'isolation multi-locataire stricte — les cgroups isolent le CPU et la mémoire ; l'isolation GPU repose sur CUDA_VISIBLE_DEVICES et sur le fait que le code de l'utilisateur le respecte. Pas d'espaces de noms, pas de politique réseau par travail, pas de cloisonnement de niveau conteneur. Acceptable pour un laboratoire, pas pour un service hébergé multi-clients.
Dès que l'un de ces points se présente, la bonne démarche est généralement d'ajouter une seconde couche — Kubernetes pour les services — plutôt que de torturer SLURM. SUNK de CoreWeave et les offres managées de 2026 font tourner SLURM sur Kubernetes pour que le même cluster fasse les deux. À l'échelle de Kentino, deux petits clusters séparés sont souvent plus simples.
Kubernetes pour l'IA : la voie cloud-native
Kubernetes a été conçu pour maintenir en vie des services web sans état. Pour les clusters IA, « tout le reste » se greffe par-dessus. La pile de 2026 : NVIDIA GPU Operator (pilotes, CUDA, NCCL, DCGM, greffon de périphérique, configuration MIG), Kueue (file d'attente, quota, admission), Volcano (ordonnanceur orienté lots), Kubeflow Trainer (CRD PyTorchJob, TFJob, MPIJob), KubeRay (Ray sur K8s), et Karpenter ou Cluster Autoscaler pour le provisionnement des nœuds.
Pourquoi un Kubernetes naïf ne convient pas à l'IA : le kube-scheduler par défaut est FIFO, sans sémantique de gang. Il démarre volontiers 7 des 8 pods d'un entraînement distribué et laisse le huitième en attente pendant que les 7 autres immobilisent des GPU inactifs. Ce n'est pas théorique — c'est l'erreur la plus fréquente des équipes qui tentent d'entraîner sur un cluster bâti pour l'inférence. Les remèdes sont Kueue (admission au niveau du travail) et Volcano (placement gang au niveau du pod). La bonne pratique actuelle est d'utiliser les deux : Kueue au-dessus, Volcano en dessous.
Volcano
Volcano est un ordonnanceur par lots de la CNCF qui s'installe à côté de kube-scheduler ou à sa place. Il apporte un véritable gang scheduling avec la sémantique minAvailable (admettre zéro ou N, jamais partiellement), des priorités de file et de la préemption, des stratégies d'équité, de binpack et de prise en compte de la topologie, ainsi qu'une prise en charge de premier ordre de PyTorchJob, TFJob, MPIJob, RayJob et SparkApplication.
Une file minimale accompagnée d'un entraînement PyTorch ordonnancé en gang :
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"]
Quand vous n'avez pas besoin de Volcano : un cluster d'inférence pur exécutant des déploiements vLLM indépendants. Chaque pod possède ses GPU, aucune coordination entre pods, le kube-scheduler par défaut suffit. Volcano devient rentable dès que vous mélangez de l'entraînement distribué dans le cluster.
Kueue et l'emprunt de quota
Kueue gère une couche que Volcano ignore : qui a le droit de consommer quelle part du cluster, et ce qui se passe lorsqu'une équipe est inactive.
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
Les deux équipes de la cohorte disposent de 16 GPU garantis et peuvent en emprunter jusqu'à 8 au pool inactif de l'autre. Les travaux prioritaires préemptent la capacité empruntée. La feuille de route 2026 de Kueue se concentre sur MultiKueue (répartition multi-cluster) et la préemption coopérative — on peut dire à un travail qui fait des points de contrôle « sauvegarde l'état et libère la ressource dans 60 secondes » au lieu de le tuer sèchement.
MIG et partage de GPU
Le greffon de périphérique NVIDIA alloue par défaut des GPU entiers. Pour l'entraînement et la grande inférence, c'est correct ; pour la petite inférence, les notebooks ou le développement, c'est du gaspillage. Trois modes de partage : MIG (isolation matérielle de la mémoire et des pannes, inférence multi-locataire en production), MPS (coopératif, sans isolation), découpage temporel (sans isolation, développement uniquement).
MIG existe sur les H100/H200, A100 et B200 — sur aucun GPU de la gamme Kentino. Les RTX 5090, RTX 4090, RTX Pro 6000 Blackwell, L40 et L4 ne prennent pas MIG en charge. Si votre conception exige des partitions GPU isolées matériellement, cela restreint votre matériel aux pièces SXM/centre de données que Kentino ne fabrique pas. Le découpage temporel annonce N GPU virtuels par GPU physique et l'ordonnanceur ignore totalement qu'ils sont sursouscrits — réservez-le au développement.
Ce que Kubernetes fait bien : les services d'inférence permanents (déploiements vLLM mis à l'échelle par HPA selon la profondeur de file, mises à jour progressives, contrôles de santé — SLURM n'a rien de comparable), les charges de travail hétérogènes sur un même cluster, l'écosystème (Prometheus, Grafana, Argo). Le prix à payer : une pile K8s + GPU Operator + Kueue + Volcano + Kubeflow fonctionnelle compte au minimum 20 composants. Estimation réaliste : Kubernetes représente 5 à 10 × la charge d'exploitation de SLURM pour de l'entraînement par lots pur.
Kubernetes est le bon choix quand : le cluster exécute des services d'inférence en parallèle de l'entraînement, vous disposez d'une équipe plateforme, le tout-déclaratif compte, ou il vous faut une portabilité multi-cluster.
Ray et KubeRay — le calcul distribué natif Python
Ray est d'une autre nature. Ce n'est pas vraiment un ordonnanceur de cluster au sens de SLURM ou K8s — c'est un runtime Python distribué livré avec un ordonnanceur, un magasin d'objets et un autoscaler. Vous écrivez du Python avec des décorateurs @ray.remote et Ray détermine où exécuter chaque tâche.
Là où Ray s'impose : le réglage d'hyperparamètres (Ray Tune — des centaines d'essais en parallèle, avec élagage précoce des mauvais ; le cas d'usage pour lequel Ray a été conçu et où il reste le meilleur), l'apprentissage par renforcement (RLlib — environnements et apprenants sous forme d'acteurs, ce qui se projette bien sur Ray et mal sur des travaux SLURM par lots), l'entraînement distribué (Ray Train, qui enveloppe PyTorch DDP / FSDP / DeepSpeed), le service de modèles (Ray Serve, natif Python, pipelines multi-modèles sur mesure) et le prétraitement des données (Ray Data). Ce que Ray n'est pas : un ordonnanceur par lots multi-locataire pour cluster partagé. Ray part du principe qu'une seule application possède le cluster.
KubeRay est l'opérateur qui fait tourner Ray sur Kubernetes. Trois CRD : RayCluster (longue durée), RayJob (coup unique), RayService (Ray Serve, mises à jour progressives). Un RayCluster minimal :
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 met à l'échelle le groupe de workers entre minReplicas et maxReplicas selon la vue qu'a Ray des tâches en attente. Combiné à Kueue (l'association RayJob + gang scheduling Kueue est documentée et fonctionne), vous obtenez du Ray multi-locataire sur un cluster K8s multi-locataire — l'installation de production que font réellement tourner la plupart des « maisons Ray ».
Quand Ray seul (sans K8s) convient : un petit laboratoire de recherche où une seule personne possède le cluster, où le dynamisme prime sur le multi-locataire, avec une charge de travail dominée par l'apprentissage par renforcement ou la recherche d'hyperparamètres. L'hybride vers lequel convergent la plupart des équipes : SLURM ou K8s comme couche de base qui possède les nœuds et le multi-locataire ; Ray lancé à l'intérieur d'un travail SLURM ou d'un espace de noms K8s pour la durée de la charge de travail d'un utilisateur. N'installez pas Ray comme ordonnanceur de premier niveau.
Run:ai et Determined — l'offre commerciale
Deux offres payantes reviennent assez souvent pour mériter d'être traitées. Toutes deux visent le même point de douleur : K8s + Volcano + Kueue + GPU Operator, cela fait beaucoup de YAML, et certaines organisations préfèrent acheter que construire.
NVIDIA Run:ai (racheté en 2024, renommé en 2025) est un ordonnanceur GPU natif K8s. Le fractionnement de GPU partage un seul GPU entre plusieurs charges de travail au niveau de la mémoire — demandes de 0.5 GPU, redimensionnement dynamique, bin-packing. Run:ai justifie son prix dans les environnements d'entreprise où plus de 10 équipes ML se disputent des GPU partagés. En deçà, Kueue + GPU Operator couvrent gratuitement l'essentiel du même terrain.
Determined.AI (HPE, 2021) est une plateforme d'entraînement managée — suivi d'expériences, recherche d'hyperparamètres, gestion des points de contrôle et entraînement distribué dans un seul produit. Convient surtout aux équipes de recherche qui veulent un tableau de bord soigné sans intégrer elles-mêmes cinq outils.
Le problème des notebooks interactifs
Un schéma qui piège presque tous les clusters GPU partagés : les chercheurs veulent accéder aux GPU via JupyterHub pour développer, et cela doit coexister avec des entraînements permanents qui occupent des nœuds entiers. La réponse naïve — un GPU dédié par notebook — gaspille 80 % du cluster. La réponse rigide — obliger les chercheurs à tout passer par sbatch — les renvoie sur leur portable avec des modèles jouets.
Trois schémas praticables : SLURM salloc + Jupyter sur l'allocation (salloc --gres=gpu:1 --time=4:00:00, démarrez un serveur Jupyter sur le nœud alloué et créez un tunnel ; limite de temps stricte, GPU correctement comptabilisé — la réponse la plus propre pour un cluster SLURM), Kubernetes + JupyterHub + Kueue (pods de notebook dans une file à faible priorité, les entraînements préemptent les notebooks inactifs), ou Run:ai avec fractionnement de GPU (chaque notebook reçoit 0.25 ou 0.5 GPU). Quel que soit votre choix, fixez une limite de temps aux sessions interactives — un notebook sans délai d'expiration est un GPU définitivement retiré du cluster.
La réalité honnête des petites équipes
La plupart des contenus sur les ordonnanceurs supposent un cluster de 256 GPU et une équipe plateforme. Le client Kentino réaliste, c'est plutôt 1 à 4 nœuds, 4 à 32 GPU au total, 2 à 6 utilisateurs qui se connaissent tous et se coordonnent sur Slack. Dans cette configuration, vous n'avez besoin d'aucun ordonnanceur.
# 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
Voilà tout le système d'ordonnancement. SLURM commence à être rentable vers 16 GPU ou 8 utilisateurs, selon ce qui arrive en premier. En dessous, la charge d'exploitation dépasse la valeur apportée. Installez un ordonnanceur quand la coordination humaine échoue, pas avant.
Quand chaque outil convient — la synthèse
| Scénario | Recommandation |
|---|---|
| 1 à 2 nœuds, 4 à 16 GPU, 2 à 6 utilisateurs qui se parlent | SSH + tmux + nvidia-smi. Pas d'ordonnanceur. |
| Laboratoire de recherche, 4 à 32 nœuds, entraînements par lots | SLURM. Ennuyeux, éprouvé, adapté. |
| Plateforme d'inférence servant du trafic client | Kubernetes + GPU Operator. Pas d'ordonnanceur par lots. |
| Cluster mixte : entraînement + inférence + outillage | Kubernetes + Kueue + Volcano + Kubeflow. |
| Python distribué intensif : RL, recherche d'hyperparamètres | Ray (ou KubeRay) au-dessus de SLURM ou K8s. |
| Plus de 10 équipes ML se disputant les GPU, budget outillage | Run:ai sur Kubernetes. |
| Expériences d'entraînement managées + suivi | Determined.AI. |
| Plus de 200 GPU, multi-équipes, multi-charges | Fédéré : SLURM pour les lots, K8s pour les services. |
La suite — l'arbre de décision
Parcourez-le dans l'ordre. Le premier « oui » met fin à la discussion.
- Moins de 16 GPU et moins de 8 utilisateurs qui se parlent ? Oui → aucun ordonnanceur. Documentez les conventions dans un fichier markdown. À revoir quand la coordination casse.
- Le cluster exécute-t-il des services d'inférence permanents en plus de l'entraînement ? Oui → Kubernetes comme socle. Ajoutez GPU Operator, puis Kueue, puis Volcano si l'entraînement distribué est au programme. Non → SLURM comme socle.
- Disposez-vous d'une équipe plateforme ou du budget pour en avoir une (0.5 à 1.0 ETP pendant six mois, 0.25 ETP en régime de croisière) ? Non → restez sur SLURM quel que soit le mélange de charges. La taxe K8s est bien réelle.
- >10 équipes se disputant du temps GPU avec des besoins de quota stricts ? Oui → évaluez Run:ai. Non → Kueue + cohortes vous emmènent à 80 % du chemin.
-
Charge de travail dominée par le RL, la recherche d'hyperparamètres ou le Python distribué ? Oui → Ray (KubeRay sur K8s, ou
salloc+ Ray sur SLURM). Non → laissez Ray de côté. - Matériel compatible MIG (cartes de centre de données) ? Non sur la gamme Kentino → prévoyez une allocation par GPU entier, ou du découpage temporel en développement uniquement. Ne concevez pas autour de MIG.
La plupart des échanges chez Kentino s'arrêtent à l'étape 1, 2 ou 3. Le reste n'est que décoration.
Articles complémentaires : l'entraînement distribué dans K02, les clusters d'inférence dans K03, le stockage de cluster dans K04, la gestion des pannes dans K06, le plafond de bande passante PCIe dans K07.
Cet article fait partie du Kentino Wiki, une série de référence sur le calcul IA, la robotique et les systèmes qui les relient. Commentaires et corrections bienvenus à info@kentino.com.