Déploiement d'un modèle IA sur K3s
Innovation & IA

Exécuter des modèles d'IA sur K3s : hôte neutre, pods et API compatible OpenAI

Aucun runtime IA sur l'hôte, les modèles sur disque montés en lecture seule, un service par fichier YAML, et un cycle réseau qui se referme : le cadre d'exploitation d'une plateforme d'inférence interne.

Le matériel est prêt, les partitions sont visibles depuis Kubernetes. Reste la question la plus concrète : comment faire tourner un modèle, proprement, de façon reproductible et sans transformer le serveur en accumulation de bricolages ? La réponse tient en un principe d'architecture et un manifeste.

Le principe : l'hôte reste neutre

Architecture d'exécution : hôte neutre, modèles sur disque, pods d'inférence

Aucun runtime IA sur le système hôte : les modèles vivent sur le disque, les moteurs dans des pods, et l'exposition passe par un service Kubernetes.

Aucun service d'IA n'est installé sur le système hôte. Ni moteur d'inférence, ni client, ni service exposé directement par l'OS. Toute exécution passe par des pods.

Cette règle, qui semble dogmatique, se justifie point par point :

  • isolation — deux moteurs, deux versions de bibliothèques CUDA, deux configurations Python peuvent cohabiter sans se marcher dessus ;
  • traçabilité — un service correspond à un fichier YAML versionné ; on sait ce qui tourne et pourquoi ;
  • réversibilité — supprimer un service ne laisse aucune trace sur l'hôte. Pas de paquets orphelins, pas d'environnement Python fossile ;
  • diagnostic — un hôte neutre reste compréhensible. C'est ce qui permet de distinguer un problème de plateforme d'un problème de modèle.

Une organisation de disque, deux règles

/opt
├── models/                 un dossier par modèle
│   ├── gemma-3-27b-it/
│   ├── Qwen2.5-7B-Instruct/
│   └── nomic-embed-text-v2/
└── k3s_services/           un dossier par espace de noms
    └── ia-prod/
        └── vllm-gemma-3-27b/
            └── vllm-gemma-3-27b.yaml

Deux règles gouvernent cette arborescence : un modèle = un dossier, un service = un fichier YAML. Et surtout : aucun modèle n'est téléchargé depuis un pod. Les poids sont récupérés une fois sur l'hôte, puis montés en lecture seule — sans quoi chaque redémarrage de pod retéléchargerait des dizaines de gigaoctets.

Le cycle réseau : ouvrir, télécharger, refermer

Sur une plateforme fermée, l'accès Internet est un événement, pas un état. Le cycle se déroule en trois temps :

  1. Ouverture temporaire — accès au dépôt de modèles et au registre d'images, via le proxy ;
  2. Déploiement — téléchargement des poids sur l'hôte, récupération de l'image du moteur ;
  3. Verrouillage — fermeture des flux, activation du mode hors ligne dans les pods, exploitation en circuit fermé.

La règle d'or tient en une phrase : tout ce qui a été ouvert pour déployer doit être refermé après. Côté pod, le mode hors ligne se déclare par variables d'environnement (HF_HUB_OFFLINE=1, TRANSFORMERS_OFFLINE=1) — le moteur cesse alors de tenter le moindre appel externe, ce qui évite aussi des démarrages qui traînent en attendant un délai d'expiration.

Le téléchargement lui-même mérite une astuce : cloner d'abord le dépôt sans les poids (les pointeurs seuls), puis récupérer les fichiers volumineux séparément. On voit ainsi ce qu'on s'apprête à télécharger, et un échec réseau n'oblige pas à tout reprendre.

Le manifeste d'un modèle, décortiqué

Un service d'inférence tient dans un fichier. Les points qui comptent :

Le moteur et le modèle

image: vllm/vllm-openai:latest
args:
  - "--model"
  - "/models/gemma-3-27b-it"      # chemin DANS le pod
  - "--served-model-name"
  - "gemma-3-27b-it"              # nom vu par les clients

Le moteur expose une API compatible OpenAI : les applications internes s'y connectent comme à un service externe, ce qui rend le remplacement possible dans les deux sens. Le nom de service annoncé est celui que les clients utiliseront — il vaut mieux le fixer explicitement.

Les deux réglages qui évitent les plantages

  - "--max-model-len"
  - "32768"
  - "--gpu-memory-utilization"
  - "0.92"

Le premier limite la longueur de contexte. Sans lui, le moteur tente d'allouer le contexte maximal annoncé par le modèle — parfois 128 000 jetons — et le cache d'attention explose la mémoire de la partition. C'est la première cause de pod qui ne démarre pas.

Le second fixe la part de mémoire que le moteur s'autorise : 92 %, soit une marge délibérée pour absorber les pics.

La partition GPU

resources:
  limits:
    cpu: "8"
    memory: "32Gi"
    nvidia.com/mig-3g.71gb: 1
  requests:
    cpu: "2"
    memory: "16Gi"
    nvidia.com/mig-3g.71gb: 1

Une partition, identique en demande et en limite. Le processeur et la mémoire vive comptent aussi : le chargement des poids et la préparation des requêtes ne sont pas gratuits.

Les modèles, en lecture seule

volumeMounts:
  - name: models
    mountPath: /models
    readOnly: true
volumes:
  - name: models
    hostPath:
      path: /opt/models
      type: Directory

Le readOnly n'est pas décoratif : il garantit qu'un moteur ne réécrira jamais dans le dossier des modèles — ni cache, ni fichier temporaire, ni téléchargement complémentaire.

L'exposition

Un Service rend l'API joignable. Le pod porte un label, le service le sélectionne : c'est le seul lien entre les deux, et c'est l'erreur la plus fréquente quand une API répond dans le pod mais reste inaccessible depuis l'extérieur.

Déployer, vérifier, tester

Déploiement d'un modèle et test de l'API

Appliquer le manifeste, suivre le démarrage, puis interroger l'API compatible OpenAI depuis le réseau interne.

kubectl -n ia-prod apply -f vllm-gemma-3-27b.yaml
kubectl -n ia-prod get pod vllm-gemma-3-27b -w
kubectl -n ia-prod logs -f vllm-gemma-3-27b

Le suivi des journaux pendant le premier démarrage n'est pas facultatif : c'est là qu'apparaissent le chargement des poids, l'allocation du cache, et les éventuelles erreurs de mémoire. Un modèle de 27 milliards de paramètres met une à deux minutes à être servi.

curl http://<serveur>:<port>/v1/models
curl http://<serveur>:<port>/v1/chat/completions -H "Content-Type: application/json" \
  -d '{"model":"gemma-3-27b-it","messages":[{"role":"user","content":"Bonjour"}],"max_tokens":512}'

Deux tests successifs : d'abord dans le pod pour valider le moteur, puis depuis le réseau pour valider l'exposition. Séparer les deux évite de chercher une panne réseau alors que c'est le modèle qui n'a pas démarré.

Le dépannage, en quatre lignes

  • API inaccessible → vérifier le service et ses points de terminaison : un label mal orthographié suffit ;
  • Pod en erreur → les journaux, en priorité la mémoire GPU et la longueur de contexte ;
  • GPU non visible → la ressource MIG demandée existe-t-elle et reste-t-il une partition libre ?
  • Modèle absent → le montage du dossier des modèles, et le chemin utilisé dans les arguments.

Ce que ce cadre apporte

Une plateforme d'inférence tenue par ces règles reste compréhensible six mois plus tard : chaque service est un fichier, chaque modèle un dossier, chaque partition une ressource. On peut ajouter un modèle sans rien casser, en retirer un sans laisser de traces, et expliquer à quelqu'un d'autre ce qui tourne et pourquoi.

C'est exactement ce qui distingue un serveur d'IA d'une machine sur laquelle on a fini par installer beaucoup de choses.

La série « IA en interne »

Cinq articles, du choix d'architecture à la mise en service :

  1. Pourquoi internaliser l'exécution des modèles
  2. La H200 : anatomie d'un accélérateur
  3. Partitionner une H200 avec MIG
  4. Présenter des GPU à Kubernetes
  5. Exécuter des modèles sur K3s avec vLLM — vous y êtes

Ces articles s'appuient sur une plateforme réellement exploitée. Les éléments d'infrastructure — noms de machines, adresses internes, espaces de noms — ont été neutralisés ; les principes, commandes et manifestes, eux, sont ceux qui tournent.

Retour au blog