Redémarrage de services IA et orchestration

LicenceInformatique et Intelligence ArtificielleRedemarrer un ordinateurRedémarrage de services IA et orchestration~12 min de lecture

Redémarrer un service IA sans coupure : checkpoints, GPU et déploiements Kubernetes

Un service d'IA perd tout ce qui vit en mémoire GPU quand son processus s'arrête ; pour redémarrer sans dommage, on sauvegarde l'état (checkpoints), on arrête proprement sur SIGTERM, on remplace les instances progressivement ou par bascule (rolling update, blue/green) et on vérifie la reprise par des tests.

Objectifs

  • Identifier ce qu'un redémarrage fait perdre à un entraînement et à un service d'inférence (poids chargés, état de l'optimiseur, sessions GPU, files de requêtes).
  • Concevoir une sauvegarde d'état (checkpoint) et choisir sa fréquence.
  • Configurer un arrêt propre et une mise à jour progressive dans Kubernetes : sondes, délai de grâce, maxSurge, maxUnavailable.
  • Comparer rolling update et blue/green, en tenant compte du nombre limité de GPU.
  • Écrire un plan de tests de reprise après redémarrage.

1. Ce qu'un redémarrage fait perdre à un système d'IA

La mémoire d'un GPU (VRAM) appartient au processus qui l'a allouée : quand le processus se termine, le pilote libère tout. Rien de ce qui s'y trouvait ne survit à un redémarrage, même d'un seul conteneur. On distingue deux cas.

1.1 Entraînement

L'état à sauvegarder pour reprendre exactement là où l'on s'était arrêté comprend :

  • les poids du modèle (en PyTorch, model.state_dict()) ;
  • l'état de l'optimiseur (optimizer.state_dict() : par exemple les moments d'Adam), sans lequel la reprise ne suit plus la même trajectoire ;
  • l'état du planificateur de taux d'apprentissage, le numéro d'époque et de pas, la position dans les données ;
  • les graines et états des générateurs aléatoires, si l'on veut une reprise reproductible.

On écrit tout dans un fichier de checkpoint, par exemple avec torch.save({...}, chemin), et on le recharge avec torch.load puis load_state_dict. Bonne pratique : écrire dans un fichier temporaire, puis le renommer (le renommage est atomique sur un même système de fichiers), pour ne jamais garder un checkpoint à moitié écrit.

1.2 Inférence (service en ligne)

  • Poids chargés en VRAM : il faut les recharger, ce qui peut prendre de quelques secondes à plusieurs minutes selon la taille du modèle et la vitesse du stockage.
  • Caches et sessions : contexte CUDA, noyaux compilés, cache de clés et valeurs d'un modèle de langage, conversations en cours.
  • Requêtes en vol : celles déjà acceptées mais pas encore répondues.

Conséquence : un service d'IA n'est pas « prêt » dès que son processus démarre. Il faut un échauffement (warm-up : chargement, première inférence) avant de lui envoyer du trafic.

Pour reprendre un entraînement avec Adam exactement comme avant la coupure, que faut-il sauvegarder au minimum ?

  1. Les poids du modèle seulement.
  2. Les poids, l'état de l'optimiseur et la position (époque, pas).
  3. Le contenu de la mémoire GPU, copié tel quel.
Adam garde des moyennes mobiles des gradients : sans elles, la reprise repart avec un optimiseur « neuf ». La VRAM, elle, ne se copie pas telle quelle : on sauvegarde des objets sérialisés.

2. Arrêt propre d'un conteneur dans Kubernetes

Quand Kubernetes supprime un pod, il le retire des points de terminaison du Service (il ne reçoit plus de nouveau trafic), exécute le crochet preStop s'il est défini, envoie SIGTERM au processus principal de chaque conteneur, attend au plus terminationGracePeriodSeconds (30 s par défaut), puis envoie SIGKILL. Le retrait des points de terminaison et l'envoi du signal se font en parallèle : un court preStop laisse le temps aux répartiteurs de charge de se mettre à jour.

Trois sondes décident de l'état du conteneur :

  • startupProbe : tant qu'elle n'a pas réussi, les autres sondes attendent ; utile pour un modèle long à charger ;
  • readinessProbe : en cas d'échec, le pod reste vivant mais ne reçoit pas de trafic ; elle doit répondre « prêt » seulement après le warm-up ;
  • livenessProbe : en cas d'échec répété, le kubelet redémarre le conteneur.

Extrait de Deployment (les valeurs sont un exemple à adapter) :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: traduction
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 0
      maxUnavailable: 1
  selector:
    matchLabels:
      app: traduction
  template:
    metadata:
      labels:
        app: traduction
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: modele
        image: registre.exemple/traduction:2.1
        resources:
          limits:
            nvidia.com/gpu: 1
        startupProbe:
          httpGet: {path: /pret, port: 8080}
          periodSeconds: 10
          failureThreshold: 30
        readinessProbe:
          httpGet: {path: /pret, port: 8080}
          periodSeconds: 5
        livenessProbe:
          httpGet: {path: /vivant, port: 8080}
          periodSeconds: 10
        lifecycle:
          preStop:
            exec:
              command: ["sleep", "5"]

Ici, la startupProbe laisse jusqu'à \(30 \times 10 = 300\) s pour charger le modèle. La ressource nvidia.com/gpu est fournie par le plugin de périphériques NVIDIA installé sur les nœuds.

Un pod d'inférence est démarré mais son modèle est encore en chargement. Quelle sonde doit l'empêcher de recevoir du trafic ?

  1. La readinessProbe.
  2. La livenessProbe.
  3. Aucune : Kubernetes attend automatiquement le modèle.
La readinessProbe retire le pod des points de terminaison du Service tant qu'il n'est pas prêt ; la livenessProbe, elle, redémarrerait le conteneur.

3. Stratégies de haute disponibilité

3.1 Rolling update (mise à jour progressive)

Le Deployment remplace les pods par vagues. Deux paramètres :

  • maxSurge : nombre (ou pourcentage, arrondi au-dessus) de pods en plus de replicas autorisés pendant la mise à jour ;
  • maxUnavailable : nombre (ou pourcentage, arrondi au-dessous) de pods qui peuvent manquer. Les deux ne peuvent pas valoir 0 en même temps. Valeur par défaut : 25 % chacun.

Commandes : kubectl rollout restart deployment/traduction (redémarre tous les pods selon la stratégie, sans changer l'image), kubectl rollout status deployment/traduction (suivi), kubectl rollout undo deployment/traduction (retour à la révision précédente), kubectl rollout history deployment/traduction.

3.2 Blue/green (bascule)

On déploie une seconde version complète (« green ») à côté de l'actuelle (« blue »), on la teste, puis on bascule tout le trafic en modifiant le sélecteur du Service :

kubectl patch service traduction -p '{"spec":{"selector":{"app":"traduction","version":"green"}}}'

Retour arrière immédiat : on remet version: blue. Contrepartie : pendant la bascule, il faut deux fois les ressources, donc deux fois les GPU.

3.3 Redémarrer un nœud (machine) sans couper le service

  1. kubectl cordon noeud-2 : plus aucun nouveau pod n'y est placé ;
  2. kubectl drain noeud-2 --ignore-daemonsets : expulse les pods (en respectant leur délai de grâce et les PodDisruptionBudget) ; ajouter --delete-emptydir-data si des pods utilisent un volume emptyDir, dont le contenu sera perdu ;
  3. redémarrer la machine (par exemple systemctl reboot), vérifier les pilotes avec nvidia-smi ;
  4. kubectl uncordon noeud-2 : le nœud accepte de nouveau des pods.

Un PodDisruptionBudget (par exemple minAvailable: 3 pour l'application traduction) interdit au drain d'expulser des pods si cela faisait passer le service sous ce seuil.

flowchart LR
  A[Nouvelle version] --> B{GPU libres pour une copie complete}
  B -- oui --> C[Blue green]
  B -- non --> D{Un GPU libre}
  D -- oui --> E[Rolling maxSurge 1]
  D -- non --> F[Rolling maxSurge 0 maxUnavailable 1]
  C --> G[Tests de reprise]
  E --> G
  F --> G

Pourquoi un blue/green peut-il échouer sur un cluster de 4 GPU qui fait déjà tourner 4 pods d'inférence à 1 GPU chacun ?

  1. Parce que Kubernetes interdit le blue/green pour les GPU.
  2. Parce que le sélecteur du Service ne peut pas changer.
  3. Parce que les 4 pods green ne trouvent aucun GPU libre et restent en attente (Pending).
Le blue/green exige de faire tourner les deux versions complètes en même temps, soit 8 GPU ici ; les nouveaux pods ne peuvent pas être placés.

4. Exemples résolus

Exemple 1 — Calculer les bornes d'un rolling update

Un Deployment a \(n = 10\) réplicas, maxSurge: 25% et maxUnavailable: 25%. Combien de pods au plus existent pendant la mise à jour, et combien au moins sont disponibles ?

Étape 1 — maxSurge s'arrondit au-dessus : \(\lceil 0{,}25 \times 10 \rceil = \lceil 2{,}5 \rceil = 3\).
Étape 2 — maxUnavailable s'arrondit au-dessous : \(\lfloor 0{,}25 \times 10 \rfloor = \lfloor 2{,}5 \rfloor = 2\).
Étape 3 — Bornes : au plus \(10 + 3 = 13\) pods, au moins \(10 - 2 = 8\) pods disponibles.
Étape 4 — Conséquence GPU : si chaque pod demande 1 GPU, il faut 13 GPU pendant la mise à jour. Avec seulement 10 GPU, les pods en surplus resteraient en attente : on choisirait maxSurge: 0.

Exemple 2 — Choisir la fréquence des checkpoints

À Lomé, un entraînement doit durer 40 h. Une sauvegarde complète prend 3 min. L'équipe compare une sauvegarde toutes les 30 min et toutes les 2 h. Calcule, pour chaque choix, le temps total passé à sauvegarder et la perte maximale en cas de panne. Cherche d'abord.

Voir la solution \[ \begin{align} \text{Toutes les 30 min : } & \frac{40 \times 60}{30} = 80 \text{ sauvegardes}, \quad 80 \times 3 = 240 \text{ min} = 4 \text{ h}, \quad \text{perte max} = 30 \text{ min} \\ \text{Toutes les 2 h : } & \frac{40}{2} = 20 \text{ sauvegardes}, \quad 20 \times 3 = 60 \text{ min} = 1 \text{ h}, \quad \text{perte max} = 2 \text{ h} \end{align} \]

Sauvegarder toutes les 30 min coûte 3 h de plus, mais limite la perte à 30 min. Si les coupures sont fréquentes (plusieurs par entraînement), la sauvegarde fréquente devient rentable ; si elles sont rares, toutes les 2 h suffit. Pour réduire le coût, on peut sauvegarder de façon asynchrone (copie en mémoire, écriture en tâche de fond) ou ne garder que les derniers checkpoints.

Exemple 3 — Plan de tests de reprise

Après le redémarrage du nœud qui héberge le service de traduction, que vérifier avant de déclarer la reprise réussie ?

Voir la solution
  1. Infrastructure : kubectl get nodes (nœud Ready), nvidia-smi (GPU visibles, pilote chargé), kubectl get pods -o wide (pods Running, compteur RESTARTS stable).
  2. Disponibilité : kubectl rollout status terminé ; tous les pods passent la readinessProbe.
  3. Exactitude : un jeu de référence de phrases (par exemple 50 phrases en français et leur traduction attendue) produit les mêmes sorties qu'avant le redémarrage, à tolérance près ; on vérifie aussi la version du modèle exposée par le service.
  4. Performance : la latence (par exemple le 95e centile) revient au niveau habituel après le warm-up.
  5. Entraînement : la reprise depuis le checkpoint repart au bon pas, et la perte (loss) continue la courbe au lieu de remonter brutalement.

Ces tests s'automatisent et se rejouent volontairement (redémarrage provoqué en préproduction) : on ne découvre pas en panne réelle qu'un test manquait.

5. Erreurs fréquentes

  • Ne sauvegarder que les poids : la reprise de l'entraînement n'est plus fidèle (optimiseur et position perdus).
  • Écrire le checkpoint dans le système de fichiers du conteneur ou un emptyDir : il disparaît avec le pod. Il faut un volume persistant ou un stockage objet.
  • Ignorer SIGTERM dans le serveur d'inférence : les requêtes en vol sont coupées au bout du délai de grâce.
  • Déclarer « prêt » avant le chargement du modèle : les premières requêtes échouent ou sont très lentes.
  • Prévoir un maxSurge sans GPU disponibles : la mise à jour se bloque avec des pods en attente.
  • Confondre kubectl rollout restart et kubectl delete pod en masse : le premier respecte la stratégie de mise à jour, le second peut supprimer tous les pods d'un coup.

6. Récapitulatif

  • La mémoire GPU ne survit pas à l'arrêt du processus : on sauvegarde poids, optimiseur, position et graines dans des checkpoints écrits de façon atomique sur un stockage persistant.
  • Arrêt propre dans Kubernetes : retrait du Service, preStop, SIGTERM, délai de grâce (30 s par défaut), SIGKILL.
  • startupProbe pour le chargement, readinessProbe pour le trafic, livenessProbe pour la relance.
  • Rolling update (maxSurge arrondi au-dessus, maxUnavailable au-dessous) ou blue/green (double capacité) ; cordon, drain, uncordon pour redémarrer un nœud.
  • Tests de reprise : infrastructure, disponibilité, exactitude, performance, continuité de l'entraînement.
Checkpoint
Fichier contenant poids, état de l'optimiseur et position, pour reprendre un entraînement après arrêt.
readinessProbe
Sonde qui décide si le pod reçoit du trafic ; prête seulement après chargement du modèle.
terminationGracePeriodSeconds
Délai entre SIGTERM et SIGKILL à la suppression d'un pod (30 s par défaut).
Rolling update
Remplacement des pods par vagues, réglé par maxSurge et maxUnavailable.
Blue/green
Deux versions complètes côte à côte, bascule du sélecteur du Service ; double capacité nécessaire.
cordon / drain
Bloquer un nœud puis en expulser les pods proprement avant de le redémarrer.

Teste tes connaissances sur ce cours

Crée ton compte gratuitement pour accéder aux quiz associés et suivre ta progression.