Aller au contenu
Développement medium

llama.cpp : serveur d'inférence LLM sur CPU et GPU

17 min de lecture

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.

  • 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.

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 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.

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.

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.

  1. 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-cpp
    curl -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.gguf

    Le dépôt bartowski est public : aucun compte Hugging Face n'est requis. Vérifiez que le fichier pèse bien autour de 940 Mo avec ls -lh models/.

  2. 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:server
    container_name: llama-server
    restart: unless-stopped
    ports:
    - "8080:8080"
    volumes:
    - ./models:/models:ro
    command:
    - -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.5b
    healthcheck:
    test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
    interval: 10s
    timeout: 5s
    retries: 6
    start_period: 30s

    Les options méritent un mot. --ctx-size 4096 fixe la fenêtre de contexte à 4096 tokens. --threads 8 aligne 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 0 force le CPU pur ; on le changera pour activer un GPU. --alias donne au modèle un nom court réutilisé dans les appels d'API.

  3. Démarrer le serveur et attendre l'état healthy.

    Fenêtre de terminal
    docker compose up -d
    docker compose ps

    Le healthcheck interroge l'endpoint /health toutes les 10 secondes. Le conteneur passe de health: starting à healthy une fois le modèle chargé en mémoire, ce qui prend quelques secondes pour un modèle de cette taille.

  4. Vérifier que le modèle est servi.

    Fenêtre de terminal
    curl -fsS http://localhost:8080/health

    La réponse attendue est exactement {"status":"ok"}. Si vous l'obtenez, le serveur d'inférence est opérationnel.

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.

Fenêtre de terminal
curl -fsS http://localhost:8080/v1/models

L'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.

Fenêtre de terminal
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.

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.

Fenêtre de terminal
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.

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.

Fenêtre de terminal
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 3

Les 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.

TestSignificationDébit mesuré
pp128Ingestion d'un prompt de 128 tokens160,97 ± 0,84 t/s
pp512Ingestion d'un prompt de 512 tokens164,32 ± 1,43 t/s
tg64Génération de 64 tokens28,57 ± 0,08 t/s
tg128Génération de 128 tokens28,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.

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.

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.

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ômeCause probableSolution
port is already allocated au démarrageLe port 8080 est pris par un autre serviceChanger le mapping hôte : "8888:8080" dans compose.yml
Conteneur en boucle RestartingChemin du modèle incorrect dans commandVérifier que le .gguf est bien dans models/ et le chemin -m exact
failed to load model dans les logsFichier GGUF tronqué ou corrompuRe-télécharger ; comparer la taille attendue avec ls -lh
Le conteneur est tué pendant le chargementRAM insuffisante (OOM)Choisir une quantification plus petite (Q4 au lieu de Q8) ou un modèle plus léger
Génération très lenteTrop ou trop peu de threadsAligner --threads sur le nombre de cœurs physiques
Réponse JSON tronquée (finish_reason: length)Modèle qui boucle, max_tokens atteintSimplifier 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.

  • 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) et tg (génération).
  • Sans clé d'API ni reverse proxy, llama-server ne doit jamais être exposé hors de la machine.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn