Aller au contenu

Comment fonctionne l'inférence d'un modèle de langage

Comment fonctionne l’inférence d’un modèle de langage

Section intitulée « Comment fonctionne l’inférence d’un modèle de langage »

L’inférence est la phase où un modèle déjà entraîné produit une réponse, par opposition à l’entraînement qui le fabrique. C’est elle qui consomme le matériel au quotidien, qui expose un port réseau et qui fixe le coût par requête.

→ Lire l’explication complète de la marmotte

Préremplissage et décodage : les deux phases d’une requête

Section intitulée « Préremplissage et décodage : les deux phases d’une requête »

Une requête d’inférence se coupe en deux phases sans rapport. Le préremplissage traite tout le prompt d’un bloc : les jetons d’entrée traversent le modèle en parallèle, la charge est dominée par le calcul, et cette phase fixe le délai avant le premier jeton (TTFT, Time To First Token). Le décodage produit ensuite un jeton à la fois, chacun dépendant du précédent : impossible à paralléliser sur une même séquence, et borné par la bande passante mémoire, car chaque jeton relit la totalité des poids depuis la mémoire du GPU (Graphics Processing Unit).

Le plafond théorique du décodage vaut donc à peu près la bande passante divisée par la taille des poids : 16 Go de poids et 1 To/s donnent environ 60 jetons par seconde pour une séquence, quelle que soit la puissance de calcul affichée sur la plaquette. Elle fait vendre des cartes, la bande passante fait tourner les réponses. Quantifier les poids (8 ou 4 bits) relève ce plafond au prix d’une perte de qualité qui se mesure (voir le comparatif des formats de quantification).

Cache de clés et valeurs, batching et compromis entre latence et débit

Section intitulée « Cache de clés et valeurs, batching et compromis entre latence et débit »

Pour ne pas recalculer le passé à chaque jeton, le serveur garde les clés et valeurs d’attention de chaque jeton déjà vu : le cache KV (Key-Value), proportionnel au contexte et au nombre de séquences actives. Pour un modèle de 8 milliards de paramètres (32 couches, 8 têtes de clés et valeurs de dimension 128, cache en 16 bits), le coût est de 2 × 32 × 8 × 128 × 2 octets, soit 128 Kio par jeton et 1 Gio pour 8 192 jetons. Vingt séquences simultanées réclament 20 Gio, plus que les poids du même modèle en 4 bits : le dimensionnement se joue sur le cache autant que sur le modèle, comme le détaillent dimensionner la mémoire d’un serveur d’inférence local et fenêtre de contexte et mémoire d’un modèle local.

Le batching continu fait avancer plusieurs séquences d’un même pas de décodage : les poids sont lus une fois pour tout le lot, et le débit total grimpe presque linéairement tant que la bande passante reste le goulot. Le prix : chaque séquence attend dans un lot plus gros, la latence entre jetons s’allonge, le cache s’épuise. C’est le compromis central, on achète du débit (un coût par jeton plus bas) avec de la latence.

La position à tenir : un service interactif se dimensionne sur la latence, jamais sur le débit maximal. Fixer un objectif de TTFT et de latence entre jetons au 95e centile, plafonner la concurrence là où les deux tiennent, puis seulement acheter du matériel. Un traitement par lots inverse la règle, et les deux sur les mêmes cartes imposent deux files. Un tableau de bord qui annonce 3 000 jetons par seconde sans le nombre d’utilisateurs simultanés raconte une histoire exacte et inexploitable.

Exemple : mesurer préremplissage et décodage avec Ollama

Section intitulée « Exemple : mesurer préremplissage et décodage avec Ollama »

Ollama, via son API (Application Programming Interface), renvoie pour chaque réponse non streamée la durée de chaque phase. Prérequis : le service en écoute locale (voir déployer Ollama sur un poste de travail) et jq installé.

Fenêtre de terminal
# Modèle de 8 milliards de paramètres, téléchargé une fois
ollama pull llama3.1:8b
# Requête non streamée ; les durées reviennent en nanosecondes
curl -s http://127.0.0.1:11434/api/generate -d '{
"model": "llama3.1:8b",
"prompt": "Explique le chiffrement symétrique en trois phrases.",
"stream": false,
"options": { "num_predict": 128, "temperature": 0 }
}' | jq '{
chargement_s: (.load_duration / 1e9),
prefill_jetons: .prompt_eval_count,
prefill_s: (.prompt_eval_duration / 1e9),
decode_jetons: .eval_count,
decode_jetons_par_s: (.eval_count / (.eval_duration / 1e9))
}'

Les champs prompt_eval_* mesurent le préremplissage, eval_* le décodage, load_duration le chargement des poids. Le second appel affiche un chargement quasi nul, et prompt_eval_count peut disparaître sur un prompt déjà vu (cache de préfixe, qui réutilise le cache KV d’un début de prompt identique). Avec plusieurs requêtes simultanées, le débit par requête baisse et le total monte : le compromis du batching, sur sa propre machine.

Authentification d’abord : Ollama n’authentifie pas ses clients, d’autres moteurs proposent une clé statique (llama.cpp via --api-key), soit un secret partagé et non une identité. Un port 11434 ouvert sur Internet « pour tester depuis la maison » offre du calcul gratuit à tout le monde, avec en prime la suppression à distance des modèles. Contre-mesure : écoute locale, passerelle avec TLS (Transport Layer Security) et authentification individuelle, comme dans contrôler l’accès à un assistant interne.

Artefacts de modèle : les formats fondés sur pickle exécutent du code à la désérialisation ; safetensors et GGUF (GPT-Generated Unified Format) non, par conception, mais leurs analyseurs ont eu leurs failles. Empreinte vérifiée, miroir interne, pas de téléchargement au démarrage.

Épuisement de ressources : prompt géant, sortie non bornée ou concurrence libre saturent le GPU et le cache pour tous. Limites d’entrée et de sortie, quotas par client, file bornée, et le versant financier dans denial of wallet sur une application adossée à un fournisseur de modèles.

Cache de préfixe partagé : un TTFT anormalement court révèle qu’un autre client a déjà envoyé ce préfixe ; cache cloisonné par locataire. Journaux : prompts et réponses consignés en texte brut accumulent, avec la rétention, ce qu’on voulait protéger.

Pas de déterminisme : à température zéro, la sortie varie encore, car l’ordre des opérations flottantes dépend de la taille du lot et du matériel. « Température zéro » promet un modèle déterministe comme « un clic » promet une migration sans incident. Un test par égalité exacte est fragile, et un audit rejouable exige de consigner modèle, quantification, moteur et matériel.

Pas de véracité : le moteur échantillonne des probabilités, la justesse vient de l’évaluation. Pas d’équivalence entre versions : une mise à jour du moteur change des sorties sans l’annoncer. Pas d’isolation ni de quotas natifs : c’est le travail de la passerelle.

Prérequis : pilote GPU, runtime et moteur à versions épinglées (un pilote qui change modifie les performances, parfois les sorties), mémoire pour poids, cache et marge, stockage rapide (charger des dizaines de Go prend des dizaines de secondes), passerelle d’authentification devant le moteur.

Secrets : un jeton par client, rotation planifiée, pour couper un client sans couper les autres. Supervision : TTFT, latence entre jetons, profondeur de file, occupation du cache, mémoire du GPU, avec les métriques de observer un modèle de langage en production. La sonde de santé teste une génération courte, pas le port, car un moteur peut accepter des connexions sans plus produire un jeton.

Sauvegarde et reprise : miroir interne des poids avec empreintes, configuration versionnée en dépôt, réplique chaude contre le rechargement à froid, jeu d’évaluation rejoué avant toute bascule de modèle.

Sans ce cadrage, le serveur d’inférence devient le maillon que personne ne surveille : un port ouvert livre le GPU et la gestion des modèles à n’importe quel client du réseau, un prompt géant ou une sortie non bornée suffit à saturer la mémoire et à faire tomber le service pour tous, et les journaux accumulent en texte brut prompts et réponses. Faute de mesure du TTFT et de la file d’attente, la dégradation ne se voit qu’au premier utilisateur mécontent, et un moteur mis à jour sans épinglage change les sorties sans qu’aucune trace n’explique pourquoi une réponse contestée a changé.

Un modèle de langage ne stocke pas des réponses : il calcule, mot après mot, le morceau de texte le plus probable à la suite de ce qu’on lui a donné. Quand une question arrive, le serveur la découpe en petits morceaux appelés jetons, puis la fait lire au modèle en une seule fois : c’est la phase de lecture, qui détermine l’attente avant le premier mot affiché. Ensuite le modèle écrit la réponse un jeton à la fois, et chaque jeton demande de relire tout le modèle en mémoire, ce qui rend la vitesse d’écriture dépendante de la vitesse de la mémoire plus que de celle du calcul. Pour ne pas tout recalculer, le serveur garde en mémoire une trace de ce qui a déjà été lu ; cette trace grossit avec la longueur de la conversation et avec le nombre de personnes servies en même temps. Servir plusieurs personnes à la fois fait gagner du temps au total, mais chacune attend un peu plus. Si la mémoire est pleine, le serveur refuse des demandes ou ralentit fortement. Enfin, le serveur ne vérifie pas qui pose la question : sans contrôle d’accès devant lui, n’importe quel appareil du réseau peut l’utiliser. Le même modèle peut aussi répondre un peu différemment d’un jour à l’autre, selon le matériel et la charge.

À lire aussi

Pages voisines