Le réflexe par défaut de la plupart des équipes est d’appeler une API hébergée, et c’est justifié : c’est zéro opération, et les modèles frontière sont réellement meilleurs sur les tâches de raisonnement difficiles. L’auto-hébergement de modèles à poids ouverts comme Llama 3, Mistral ou Qwen cesse d’être un projet personnel et devient une décision d’entreprise sous trois conditions assez courantes : un volume d’inférence élevé et soutenu où le coût par token de l’API domine la facture, des exigences de résidence des données ou contractuelles interdisant d’envoyer les données clients à un tiers, et des applications sensibles à la latence où l’aller-retour vers une API externe ajoute une latence de queue inacceptable comparée à une inférence sur du matériel que vous contrôlez, sur le même réseau que votre application.
L’économie bascule à un point prévisible : un seul GPU exécutant un modèle de 7 à 14 milliards de paramètres à taux d’utilisation raisonnable peut servir un volume de requêtes soutenu qui coûterait plusieurs fois le coût horaire amorti du GPU en facturation au token sur une API hébergée, dès lors que vous traitez des dizaines de millions de tokens par jour. En dessous de ce volume, le coût fixe de la capacité GPU (payé qu’il soit occupé ou non) perd généralement face au modèle de paiement au token, donc l’auto-hébergement est un pari sur le volume et le taux d’utilisation, pas une mise à niveau universelle.
Le compromis honnête est la prise en charge opérationnelle : vous devenez responsable de la compatibilité des pilotes GPU, des mises à niveau de modèles, de la planification de capacité, et des modes de défaillance d’un service à état plutôt que d’un simple client HTTP avec des relances. Les équipes qui réussissent leur auto-hébergement le traitent comme une infrastructure avec la même rigueur qu’une base de données, avec supervision, marge de capacité et plan de retour arrière, et non comme un script ponctuel tournant sur une machine de secours.
2. Configuration d’Ollama en service de production systemd
Ollama est le chemin le plus rapide pour passer de zéro à un modèle local fonctionnel, en enveloppant llama.cpp avec de la gestion de modèles, une API HTTP et une détection GPU automatique, ce qui en fait le bon choix par défaut pour un auto-hébergement à débit modéré et pour les équipes qui veulent une configuration mono-nœud prête pour la production sans exploiter un cluster d’inférence complet. Le faire tourner comme un processus démarré manuellement convient pour du développement local mais pas pour la production : il doit survivre aux redémarrages, redémarrer en cas de crash, et journaliser vers un endroit que votre stack de supervision peut voir.
La configuration de production standard enveloppe le serveur Ollama dans une unité systemd avec Restart=always, un utilisateur système dédié non-root, et des variables d’environnement explicites contrôlant l’interface réseau d’écoute, l’emplacement du répertoire des modèles (qui doit se trouver sur un stockage NVMe local rapide, pas monté sur le réseau, car les fichiers de modèles pèsent des dizaines de gigaoctets et le temps de chargement affecte directement la latence de démarrage à froid), et des limites de concurrence.
Deux variables d’environnement comptent de façon disproportionnée en production : OLLAMA_NUM_PARALLEL contrôle combien de requêtes une même instance de modèle chargée peut traiter simultanément (la valeur par défaut est conservatrice ; l’augmenter échange de la mémoire contre du débit), et OLLAMA_MAX_LOADED_MODELS contrôle combien de modèles distincts peuvent résider en mémoire simultanément, ce qui compte si vous servez plusieurs modèles depuis le même nœud et devez éviter les allers-retours de mémoire GPU dus à des cycles répétés de chargement et déchargement.
3. Forte concurrence : vLLM et PagedAttention
Le modèle de concurrence d’Ollama convient pour des outils internes et un trafic faible à modéré, mais il n’a pas été conçu comme un serveur d’inférence multi-locataires à haut débit, et les équipes servant un trafic de production à concurrence significative (des dizaines à des centaines de requêtes simultanées par GPU) migrent en général leur couche de service vers vLLM tout en conservant les mêmes modèles à poids ouverts. L’innovation centrale de vLLM est PagedAttention, qui gère le cache KV (la mémoire par requête qui croît avec la longueur du contexte pendant la génération) par blocs de taille fixe et non contigus, à la manière dont un système d’exploitation gère les pages de mémoire virtuelle, plutôt que l’approche naïve consistant à préallouer un large tampon contigu par requête.
C’est important car l’approche naïve gaspille énormément de mémoire GPU en fragmentation interne et en surprovisionnement pour une longueur de séquence dans le pire cas que la plupart des requêtes n’atteignent jamais, ce qui plafonne le nombre de requêtes simultanées tenant en mémoire GPU. L’allocation par blocs de PagedAttention élimine l’essentiel de cette fragmentation, ce qui en pratique signifie que plusieurs fois plus de requêtes simultanées tiennent dans la même empreinte mémoire GPU comparé à une allocation naïve du cache KV, ce qui se traduit directement par un débit plusieurs fois supérieur par GPU.
vLLM implémente aussi le batching continu, où de nouvelles requêtes rejoignent un lot en cours d’exécution dès que de la capacité GPU se libère (plutôt que d’attendre que le lot entier se termine, comme avec le batching statique), ce qui maintient une utilisation GPU constamment élevée face à des arrivées de requêtes variables, au lieu d’osciller entre pleine utilisation et inactivité. Le coût opérationnel de ce saut est réel : vLLM a une empreinte de dépendances plus lourde, un déploiement plus impliqué (il expose un serveur API compatible OpenAI que vous exploitez vous-même, plutôt que la gestion de modèles plus simple pilotée par CLI d’Ollama), et des valeurs par défaut moins indulgentes ; la migration vaut donc le coup précisément quand la concurrence, et non la capacité brute du modèle, devient votre goulot d’étranglement.
4. Dimensionnement VRAM et quantification GGUF
La VRAM est la contrainte dure qui détermine quels modèles vous pouvez même faire tourner, et se tromper dans le calcul de dimensionnement est la cause la plus fréquente d’un crash mémoire sous charge qui n’était jamais apparu en test local avec une seule requête. La mémoire statique des poids d’un modèle en pleine précision (FP16/BF16) représente environ 2 octets par paramètre, donc un modèle de 7 milliards de paramètres a besoin d’environ 14 Go rien que pour contenir les poids, avant même de compter le cache KV, qui croît à la fois avec la longueur du contexte et le nombre de requêtes simultanées et qui est fréquemment la véritable cause des erreurs de mémoire insuffisante en production, pas les poids statiques.
La quantification réduit l’empreinte statique des poids en les stockant à précision réduite : la quantification 8 bits divise à peu près par deux la mémoire par rapport à FP16, et la quantification 4 bits (avec des formats comme GGUF Q4_K_M pour llama.cpp/Ollama, ou AWQ/GPTQ pour vLLM) la divise à peu près par quatre, ce qui rend un modèle de 70 milliards de paramètres exécutable sur un seul GPU grand public ou semi-professionnel de 24 ou 48 Go, au lieu de nécessiter du parallélisme tensoriel multi-GPU. Le compromis est une régression de qualité mesurable mais généralement modeste, typiquement quelques points de pourcentage sur les benchmarks standards en 4 bits par rapport à FP16, ce qui est acceptable pour la plupart des cas d’usage en production mais doit être validé sur votre propre jeu d’évaluation, pas simplement supposé.
Le cache KV lui-même peut aussi être quantifié (cache KV FP8 ou INT8 dans vLLM) indépendamment des poids du modèle, ce qui est souvent l’optimisation à plus fort effet de levier spécifiquement pour du service à forte concurrence, car la mémoire du cache KV croît linéairement avec le nombre de séquences simultanées et la longueur du contexte, tandis que la mémoire des poids reste fixe quelle que soit la charge. Une checklist de dimensionnement pratique : choisissez votre modèle cible et son niveau de quantification, calculez la mémoire statique des poids, réservez 15 à 20 % de la VRAM restante pour la surcharge CUDA et les activations, puis dimensionnez votre nombre maximal de séquences simultanées et votre longueur de contexte maximale en fonction de ce qui reste pour le cache KV, et testez en charge contre ce plafond plutôt que de faire confiance au chiffre théorique.
5. Observabilité : surveiller l’utilisation GPU et la profondeur de file
Un nœud d’inférence auto-hébergé échoue silencieusement bien plus souvent qu’il ne s’effondre bruyamment, et les deux signaux les plus importants à surveiller sont l’utilisation de la mémoire GPU approchant son plafond (un indicateur avancé d’un arrêt imminent pour manque de mémoire lors de pics de charge) et la profondeur de la file d’attente des requêtes, qui est le signal le plus clair que le volume de requêtes entrant a dépassé votre capacité de service et que les requêtes commencent à attendre plutôt qu’à être traitées immédiatement.
Pour vLLM, le serveur compatible OpenAI expose nativement des métriques Prometheus, incluant les compteurs de requêtes en cours et en attente, le pourcentage d’utilisation du cache KV GPU, et des histogrammes de temps jusqu’au premier token et de temps par token de sortie, qui devraient alimenter directement votre stack Prometheus et Grafana existante plutôt qu’un tableau de bord maison. Pour Ollama, la supervision est plus légère par défaut et se résume en général à interroger périodiquement la sortie de nvidia-smi ou à envelopper le processus avec un exportateur sidecar, car Ollama n’expose pas le même niveau de métriques natives.
Les seuils d’alerte à mettre en place dès le premier jour sont : mémoire GPU au-dessus de 90 % pendant plus de quelques minutes (risque d’épuisement mémoire imminent), temps d’attente en file dépassant le budget de latence acceptable de votre application (saturation de capacité), et temps jusqu’au premier token qui dérive vers le haut sur une fenêtre glissante même à charge constante (signe de fragmentation mémoire ou d’un modèle ayant besoin d’un redémarrage), chacun pointant vers une action différente : ajouter de la marge de concurrence, ajouter un nœud GPU, ou planifier un redémarrage, respectivement.
6. Routage de modèles et repli vers des API hébergées
Une configuration auto-hébergée mature fait rarement transiter 100 % du trafic par l’infrastructure locale ; le schéma le plus résilient et le plus rentable consiste à router selon les caractéristiques de la requête, en envoyant les requêtes courantes, à fort volume et sensibles à la latence vers le modèle auto-hébergé, et en réservant les API hébergées de pointe aux requêtes qui exigent réellement un raisonnement plus fort, un contexte plus long, ou des capacités que le modèle à poids ouverts n’a pas, comme un usage avancé d’outils ou la vision.
Cette décision de routage doit être explicite dans le code applicatif, fondée sur les métadonnées de la requête (type de tâche, longueur de contexte requise, une heuristique de complexité, ou simplement une configuration par point d’entrée) plutôt qu’implicite selon quel service se trouve être joignable, car une dégradation silencieuse de la qualité due à un repli non planifié est pire pour les utilisateurs qu’une politique de routage explicite et testée.
Le sens du repli compte aussi sur le plan opérationnel : un nœud auto-hébergé qui tombe (crash de pilote, manque de mémoire, panne matérielle) devrait déclencher un repli automatique vers une API hébergée pour assurer la continuité plutôt qu’une interruption de service, même si ce repli coûte plus cher par token, car la disponibilité pendant un incident vaut plus que la différence de coût marginal pour la durée de l’incident. Cela exige que votre couche applicative traite le point de service auto-hébergé et l’API hébergée comme interchangeables derrière une interface commune, ce qui vaut la peine d’être construit même si vous ne pensez jamais avoir besoin de ce chemin de repli, car vous en aurez besoin tôt ou tard.
7. Synthèse : un cadre de décision pour l’auto-hébergement
L’auto-hébergement de LLM à poids ouverts est un bon choix sous trois conditions : un volume soutenu suffisamment élevé pour que le coût amorti du GPU batte la tarification au token d’une API, des contraintes de résidence des données ou contractuelles excluant les API tierces, ou des exigences de latence qu’un aller-retour réseau ne peut satisfaire. En dehors de ces conditions, une API hébergée reste le choix par défaut le plus simple et souvent le moins cher, et auto-héberger uniquement pour éviter les coûts d’API à faible volume représente généralement une perte nette une fois le temps d’ingénierie et opérationnel intégré au calcul.
Le chemin de déploiement pratique est : commencer avec Ollama dans un service géré par systemd pour la justesse et la simplicité, valider le choix du modèle et le niveau de quantification sur votre propre jeu d’évaluation (pas sur des benchmarks publiés), et ne migrer vers vLLM que lorsque la concurrence, et non la capacité du modèle, devient votre goulot d’étranglement, car cette migration ajoute une réelle complexité opérationnelle qui ne vaut pas la peine d’être payée prématurément.
Traitez le dimensionnement VRAM, le niveau de quantification et les limites de concurrence comme des paramètres à ajuster face à une charge mesurée, pas des valeurs fixées une fois pour toutes depuis une fiche technique puis oubliées ; et construisez l’observabilité (mémoire GPU, profondeur de file, temps jusqu’au premier token) ainsi qu’un chemin de repli vers une API hébergée dès le premier jour, car un nœud d’inférence auto-hébergé devient une infrastructure de production dès l’instant où de vrais utilisateurs en dépendent, et il mérite la même rigueur opérationnelle que n’importe quel autre service à état de votre stack.
#!/usr/bin/env bashset -euo pipefailpython -m vllm.entrypoints.openai.api_server \ --model meta-llama/Meta-Llama-3-8B-Instruct \ --dtype auto \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --max-num-seqs 256 \ --tensor-parallel-size 1 \ --port 8000 \ --api-key "$VLLM_API_KEY"# --max-num-seqs borne le nombre de sequences simultanees en vol (batching continu)# --gpu-memory-utilization reserve une marge sous 1.0 pour le contexte CUDA + les activations
scripts/vram_estimate.shbash
#!/usr/bin/env bash# Estimation approximative de la VRAM pour un modele transformer dense# Usage: ./vram_estimate.sh <parametres_milliards> <octets_par_parametre> <gpu_vram_gb>PARAMS_B=$1 # ex: 8 pour un modele de 8BBYTES_PER_PARAM=$2 # 2 = FP16/BF16, 1 = INT8, 0.5 = INT4/GGUF Q4GPU_VRAM_GB=$3WEIGHT_GB=$(echo "$PARAMS_B * $BYTES_PER_PARAM" | bc)OVERHEAD_GB=$(echo "$GPU_VRAM_GB * 0.18" | bc)AVAILABLE_FOR_KV=$(echo "$GPU_VRAM_GB - $WEIGHT_GB - $OVERHEAD_GB" | bc)echo "Poids statiques: ${WEIGHT_GB} GB"echo "Surcharge reservee (18%): ${OVERHEAD_GB} GB"echo "Reste pour cache KV + activations: ${AVAILABLE_FOR_KV} GB"