Vous utilisez peut-être déjà Ollama pour exécuter un LLM en local. Sous le capot, c'est llama.cpp qui charge le modèle et génère les tokens. Ce guide montre comment utiliser llama.cpp directement : pourquoi descendre à ce niveau, comment lancer llama-server (le serveur HTTP compatible OpenAI), comment forcer des sorties JSON et comment mesurer les performances avec llama-bench. Public visé : développeur ou administrateur à l'aise avec Docker, qui veut un contrôle fin sur l'inférence locale sans dépendre d'un service SaaS.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre ce que llama.cpp apporte par rapport à Ollama et quand le choisir.
- Lancer un serveur d'inférence compatible OpenAI avec Docker Compose.
- Interroger l'API REST et forcer une réponse JSON valide avec un schéma.
- Mesurer le débit CPU (tokens/seconde) avec l'outil officiel llama-bench.
- Savoir comment activer un GPU (CUDA, Vulkan, Metal) et durcir l'exposition réseau.
Prérequis
Section intitulée « Prérequis »Ce guide se reproduit entièrement sur un poste CPU, sans carte graphique. Vous avez besoin de :
- Docker et le plugin Docker Compose installés et fonctionnels.
- Environ 2 Go d'espace disque pour le modèle de test et 4 Go de RAM libres.
- Des notions de ligne de commande et de requêtes HTTP (
curl).
Si les notions d'inférence ou de quantification ne vous parlent pas encore, lisez d'abord les guides Comprendre l'inférence d'un LLM et Comprendre la quantification : ils posent le vocabulaire employé ici. Pour situer llama.cpp parmi les autres moteurs, voir le comparatif des backends d'inférence.
llama.cpp, le moteur derrière Ollama
Section intitulée « llama.cpp, le moteur derrière Ollama »llama.cpp est un projet open source (licence MIT) écrit en C/C++ qui exécute des modèles de langage avec un objectif précis : fonctionner partout, du Raspberry Pi au serveur multi-GPU. C'est la brique d'inférence que des outils grand public comme Ollama ou LM Studio embarquent sans le dire. Apprendre à l'utiliser directement, c'est comprendre ce qui se passe réellement quand votre modèle local répond.
Le projet livre plusieurs binaires complémentaires. llama-server expose un serveur HTTP avec une API compatible OpenAI : c'est lui que vous mettrez en production. llama-cli offre un mode interactif en terminal, pratique pour un test rapide. llama-bench mesure le débit d'inférence de façon reproductible. llama-quantize convertit un modèle vers un format plus léger — sujet d'un guide dédié.
llama.cpp lit exclusivement le format GGUF (GPT-Generated Unified Format), un conteneur unique qui embarque les poids du modèle, le vocabulaire et les métadonnées. Un modèle GGUF est déjà quantifié : ses poids sont compressés sur 4, 5 ou 8 bits au lieu des 16 bits d'origine, ce qui divise la taille par trois à quatre et rend l'exécution CPU réaliste.
Pourquoi descendre au niveau llama.cpp
Section intitulée « Pourquoi descendre au niveau llama.cpp »Ollama reste le choix le plus simple pour démarrer. Mais trois besoins justifient d'utiliser llama.cpp directement plutôt que la couche d'abstraction.
Le premier est le contrôle des paramètres. Ollama masque beaucoup de réglages derrière ses valeurs par défaut. Avec llama-server, vous pilotez explicitement la taille de contexte, le nombre de threads, le batch et les couches déchargées sur GPU. Le deuxième est la portabilité matérielle : llama.cpp se compile pour CUDA (NVIDIA), Vulkan (iGPU Intel/AMD et cartes grand public), Metal (Apple Silicon) ou ROCm (AMD), là où Ollama suit un sous-ensemble. Le troisième est la mesure : llama-bench donne des chiffres comparables que vous pouvez verser dans une décision d'architecture.
Lancer llama-server avec Docker
Section intitulée « Lancer llama-server avec Docker »L'équipe llama.cpp publie des images officielles sur le registre ghcr.io/ggml-org/llama.cpp. L'image :server contient le binaire llama-server prêt à l'emploi. Cette approche évite toute compilation et garantit un environnement reproductible.
-
Créer le dossier de travail et télécharger un modèle GGUF.
Le modèle de test est Qwen2.5-1.5B-Instruct, un modèle compact d'Alibaba qui répond correctement en français. Il est quantifié en Q4_K_M (poids sur 4 bits), soit environ 940 Mo — assez petit pour tourner confortablement sur CPU.
Fenêtre de terminal mkdir -p llama-cpp/models && cd llama-cppcurl -L --fail -o models/qwen2.5-1.5b-instruct-q4_k_m.gguf \https://huggingface.co/bartowski/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/Qwen2.5-1.5B-Instruct-Q4_K_M.ggufLe dépôt
bartowskiest public : aucun compte Hugging Face n'est requis. Vérifiez que le fichier pèse bien autour de 940 Mo avecls -lh models/. -
Décrire la stack dans un fichier
compose.yml.Ce fichier monte le dossier
models/en lecture seule dans le conteneur et passe les options de llama-server après le mot-clécommand.services:llama-server:image: ghcr.io/ggml-org/llama.cpp:servercontainer_name: llama-serverrestart: unless-stoppedports:- "8080:8080"volumes:- ./models:/models:rocommand:- -m- /models/qwen2.5-1.5b-instruct-q4_k_m.gguf- --host- "0.0.0.0"- --port- "8080"- --ctx-size- "4096"- --threads- "8"- --n-gpu-layers- "0"- --alias- qwen2.5-1.5bhealthcheck:test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]interval: 10stimeout: 5sretries: 6start_period: 30sLes options méritent un mot.
--ctx-size 4096fixe la fenêtre de contexte à 4096 tokens.--threads 8aligne le calcul sur le nombre de cœurs physiques de la machine — inutile de compter les threads logiques de l'Hyper-Threading.--n-gpu-layers 0force le CPU pur ; on le changera pour activer un GPU.--aliasdonne au modèle un nom court réutilisé dans les appels d'API. -
Démarrer le serveur et attendre l'état
healthy.Fenêtre de terminal docker compose up -ddocker compose psLe
healthcheckinterroge l'endpoint/healthtoutes les 10 secondes. Le conteneur passe dehealth: startingàhealthyune fois le modèle chargé en mémoire, ce qui prend quelques secondes pour un modèle de cette taille. -
Vérifier que le modèle est servi.
Fenêtre de terminal curl -fsS http://localhost:8080/healthLa réponse attendue est exactement
{"status":"ok"}. Si vous l'obtenez, le serveur d'inférence est opérationnel.
Interroger l'API OpenAI-compatible
Section intitulée « Interroger l'API OpenAI-compatible »llama-server expose une API REST alignée sur le format OpenAI. Concrètement, tout client ou bibliothèque qui sait parler à l'API OpenAI fonctionne sans modification : il suffit de pointer l'URL de base vers votre serveur. C'est ce qui rend llama.cpp interchangeable avec d'autres backends.
L'endpoint /v1/models liste les modèles chargés. C'est le test le plus rapide après le healthcheck.
curl -fsS http://localhost:8080/v1/modelsL'endpoint central est /v1/chat/completions. Il prend une liste de messages avec leurs rôles (system, user, assistant) et renvoie la réponse générée.
curl -fsS http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5-1.5b", "messages": [ {"role": "system", "content": "Tu es un assistant DevOps qui répond en français en une phrase."}, {"role": "user", "content": "Quelle commande systemd affiche les services actifs ?"} ], "temperature": 0.2, "max_tokens": 80 }'La réponse est un objet JSON dont le champ choices[0].message.content contient le texte généré — ici systemctl list-units --active --type=service. Le bloc usage détaille les tokens consommés et le bloc timings donne le débit réel de la requête (predicted_per_second), une information que l'API OpenAI publique ne fournit pas.
Le paramètre temperature contrôle l'aléa : une valeur basse (0.1 à 0.3) rend les réponses déterministes et factuelles, utile pour de l'extraction ou du code. max_tokens plafonne la longueur de la réponse et protège contre les générations qui s'emballent.
Forcer une réponse JSON structurée
Section intitulée « Forcer une réponse JSON structurée »Un LLM renvoie par défaut du texte libre, difficile à exploiter par un programme. llama-server sait contraindre la sortie à respecter un schéma JSON : c'est le structured output. Le serveur convertit le schéma en grammaire et empêche le modèle de produire le moindre token qui violerait la structure. La réponse est donc toujours du JSON valide.
On déclare le schéma via le champ response_format. L'exemple ci-dessous extrait des entités d'une phrase — un cas d'usage idéal pour un petit modèle.
curl -fsS http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5-1.5b", "messages": [ {"role": "user", "content": "Extrais la distribution Linux et son gestionnaire de paquets : Sur Debian 12, on installe les paquets avec apt."} ], "temperature": 0.1, "response_format": { "type": "json_schema", "json_schema": { "name": "extraction", "strict": true, "schema": { "type": "object", "properties": { "distribution": { "type": "string" }, "version": { "type": "string" }, "gestionnaire_paquets": { "type": "string" } }, "required": ["distribution", "version", "gestionnaire_paquets"] } } }, "max_tokens": 120 }'Le champ content de la réponse contient alors exactement {"distribution": "Debian", "version": "12", "gestionnaire_paquets": "apt"}. Vous pouvez le passer directement à json.loads() sans nettoyage. L'option strict: true interdit tout champ supplémentaire non déclaré dans le schéma.
Mesurer les performances avec llama-bench
Section intitulée « Mesurer les performances avec llama-bench »Affirmer « c'est rapide » ne suffit pas pour une décision d'architecture. llama-bench mesure le débit de façon reproductible, en répétant chaque test et en donnant un écart-type. Le binaire vit dans l'image :full, distincte de l'image :server.
L'outil distingue deux phases. Le prompt processing (noté pp) est la vitesse à laquelle le modèle ingère le contexte d'entrée. La text generation (notée tg) est la vitesse à laquelle il produit les tokens de réponse — c'est le chiffre que ressent l'utilisateur.
docker run --rm \ -v "$PWD/models:/models:ro" \ --entrypoint /app/llama-bench \ ghcr.io/ggml-org/llama.cpp:full \ -m /models/qwen2.5-1.5b-instruct-q4_k_m.gguf \ -p 128,512 -n 64,128 -t 8 -ngl 0 -r 3Les options -p et -n listent les tailles de prompt et de génération à tester, -t fixe les threads, -ngl les couches GPU (0 ici) et -r 3 répète chaque mesure trois fois. Sur un i7-12650H en CPU pur, 8 threads, le résultat obtenu est le suivant.
| Test | Signification | Débit mesuré |
|---|---|---|
| pp128 | Ingestion d'un prompt de 128 tokens | 160,97 ± 0,84 t/s |
| pp512 | Ingestion d'un prompt de 512 tokens | 164,32 ± 1,43 t/s |
| tg64 | Génération de 64 tokens | 28,57 ± 0,08 t/s |
| tg128 | Génération de 128 tokens | 28,42 ± 0,18 t/s |
La lecture est instructive. Le prompt processing est cinq à six fois plus rapide que la génération : ingérer un contexte se parallélise bien, produire les tokens un par un beaucoup moins. Une génération autour de 28 tokens/seconde reste confortable pour un assistant en lecture humaine, mais montre la limite du CPU dès qu'on vise plusieurs requêtes simultanées. C'est précisément le point de bascule vers un GPU ou vers un backend orienté débit comme vLLM.
Exploiter un GPU : CUDA, Vulkan, Metal
Section intitulée « Exploiter un GPU : CUDA, Vulkan, Metal »Le CPU suffit pour un petit modèle et un usage mono-utilisateur. Au-delà, un GPU change l'échelle. llama.cpp décharge tout ou partie des couches du modèle sur le GPU via l'option --n-gpu-layers : la valeur -1 décharge toutes les couches, un nombre intermédiaire répartit la charge entre GPU et CPU quand la VRAM est insuffisante.
Le choix de l'image dépend de votre matériel. Chaque cible a son build dédié car les bibliothèques de calcul diffèrent.
Sur un GPU NVIDIA, utilisez l'image :server-cuda et exposez la carte au conteneur. Le NVIDIA Container Toolkit doit être installé sur l'hôte.
image: ghcr.io/ggml-org/llama.cpp:server-cuda deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu]Passez ensuite --n-gpu-layers -1 dans la section command pour décharger l'intégralité du modèle sur le GPU.
Vulkan exploite les iGPU Intel et les cartes AMD grand public. L'image :server-vulkan cible ce cas. Le conteneur a besoin d'accéder au périphérique de rendu de l'hôte.
image: ghcr.io/ggml-org/llama.cpp:server-vulkan devices: - /dev/dri:/dev/driLe gain dépend fortement de la puissance de l'iGPU ; sur un GPU intégré modeste, l'écart avec le CPU peut rester limité.
Sur Mac Apple Silicon (M1 à M4), le backend Metal est actif par défaut. Docker ne donnant pas accès au GPU sur macOS, on installe llama.cpp nativement via Homebrew.
brew install llama.cppllama-server -m models/qwen2.5-1.5b-instruct-q4_k_m.gguf --port 8080Metal est utilisé automatiquement, sans option supplémentaire.
Sécurité
Section intitulée « Sécurité »llama-server n'embarque aucune authentification par défaut. L'option --host 0.0.0.0 utilisée plus haut fait écouter le serveur sur toutes les interfaces : pratique pour Docker, mais à ne jamais exposer tel quel sur Internet ou un réseau non maîtrisé.
Trois mesures suffisent pour un déploiement sain. D'abord, restreindre l'écoute : en exécution locale hors conteneur, préférez --host 127.0.0.1 pour n'accepter que les connexions de la machine. Ensuite, exiger une clé : l'option --api-key VOTRE_CLE impose un en-tête Authorization: Bearer VOTRE_CLE sur chaque requête. Cette clé est un secret : passez-la par une variable d'environnement, jamais en clair dans compose.yml versionné. Enfin, pour une exposition réelle, placez le serveur derrière un reverse proxy qui termine le TLS et applique un rate limiting.
Dépannage
Section intitulée « Dépannage »Les erreurs rencontrées en pratique tiennent à quelques causes récurrentes. Le tableau ci-dessous relie le symptôme observé à sa cause et à la correction.
| Symptôme | Cause probable | Solution |
|---|---|---|
port is already allocated au démarrage | Le port 8080 est pris par un autre service | Changer le mapping hôte : "8888:8080" dans compose.yml |
Conteneur en boucle Restarting | Chemin du modèle incorrect dans command | Vérifier que le .gguf est bien dans models/ et le chemin -m exact |
failed to load model dans les logs | Fichier GGUF tronqué ou corrompu | Re-télécharger ; comparer la taille attendue avec ls -lh |
| Le conteneur est tué pendant le chargement | RAM insuffisante (OOM) | Choisir une quantification plus petite (Q4 au lieu de Q8) ou un modèle plus léger |
| Génération très lente | Trop ou trop peu de threads | Aligner --threads sur le nombre de cœurs physiques |
Réponse JSON tronquée (finish_reason: length) | Modèle qui boucle, max_tokens atteint | Simplifier le schéma, baisser la température, ou prendre un modèle plus grand |
Pour inspecter ce que fait le serveur, docker compose logs -f llama-server affiche en continu le chargement du modèle, le template de chat détecté et chaque requête traitée.
À retenir
Section intitulée « À retenir »- llama.cpp est le moteur d'inférence que des outils comme Ollama embarquent ; l'utiliser directement donne un contrôle fin.
- llama-server expose une API compatible OpenAI : tout client OpenAI fonctionne en changeant l'URL de base.
- Le format GGUF embarque un modèle déjà quantifié, ce qui rend l'inférence CPU réaliste.
- Le structured output garantit un JSON valide ; un petit modèle peut toutefois bâcler le contenu.
- llama-bench mesure un débit reproductible : distinguer
pp(ingestion) ettg(génération). - Sans clé d'API ni reverse proxy, llama-server ne doit jamais être exposé hors de la machine.