Pourquoi llama.cpp a gagné le local

Faire tourner un LLM sur sa propre machine était, il y a peu, réservé aux possesseurs de gros GPU. llama.cpp a renversé la table avec trois partis pris assumés : pas de dépendances lourdes (un binaire C++ qui se compile en cinq minutes), un format de modèle autoportant qui ne suppose ni Python ni framework ML, et une quantification fine qui fait tenir un 13 B dans la RAM d’un ordinateur portable. Comme on le voyait dans le comparatif des runtimes, c’est devenu le standard de fait du déploiement local. La suite explique pourquoi, et le mécanisme exact qui rend ce gain possible.

GGUF : un format pensé pour le mmap

GGUF GPT-Generated Unified Format. Format de fichier de llama.cpp qui stocke dans un seul fichier les tenseurs, leurs types de quantification, le vocabulaire et les métadonnées du modèle. Sa structure alignée permet de le lire par mmap : le modèle « démarre » en une fraction de seconde. Approfondir dans le glossaire (GPT-Generated Unified Format) regroupe dans un seul fichier les tenseurs du modèle, leurs types de quantification, le vocabulaire du tokenizer et les métadonnées d’architecture. C’est délibéré : un fichier unique se déplace, se synchronise, se vérifie par hash sans coordination entre plusieurs blobs. Mais l’apport décisif n’est pas le rangement, c’est que le fichier se mappe d’un bloc. Chaque tenseur est aligné (par défaut sur 32 octets, general.alignment, pour les accès SIMD, et non sur la taille de page de l’OS) et le fichier entier est projeté en mémoire d’un seul mmap Appel système qui « mappe » un fichier dans l'espace d'adressage d'un processus. Le fichier n'est pas chargé en RAM d'un coup : l'OS charge les pages à la demande, au premier accès. C'est ce qui permet à llama.cpp de démarrer un modèle de 40 Go en une fraction de seconde. , l’appel système qui mappe un fichier dans l’espace d’adressage virtuel d’un processus sans le copier ; le kernel charge ensuite les pages à la demande, au niveau du syscall.

Et c’est là que se joue la victoire silencieuse du format. Charger un modèle de 20 Go avec une bibliothèque ML classique signifie ouvrir le fichier, allouer 20 Go de RAM, recopier le contenu, fermer le fichier : quelques secondes au mieux sur SSD NVMe, plusieurs dizaines de secondes sur disque ordinaire, et toujours 20 Go résidents même si le modèle ne sert qu’à une inférence ponctuelle. Avec mmap, le kernel se contente d’inscrire le fichier dans la table de pages du processus : aucune copie, aucune allocation. La RAM ne contient qu’une promesse : « ces 20 Go d’adresses virtuelles pointent quelque part dans ce fichier ». Quand llama.cpp accède à un tenseur, le CPU lève un page fault Interruption déclenchée par le CPU quand un processus accède à une page mémoire absente de la RAM physique. Un page fault mineur est résolu rapidement par l'OS ; un page fault majeur exige un accès disque et gèle le calcul pendant des millisecondes. , le kernel charge la page correspondante depuis le SSD, l’inscrit en RAM, et l’instruction reprend. C’est le même mécanisme que la mémoire virtuelle d’un OS, appliqué aux poids d’un LLM.

Trois conséquences pratiques en découlent. D’abord, le démarrage est instantané, utile pour un outil en ligne de commande qu’on lance plusieurs fois par session, là où un runtime à chargement explicite paie 30 secondes à chaque appel. Ensuite, plusieurs processus peuvent partager les mêmes pages physiques sans les dupliquer : deux instances de llama.cpp qui chargent le même GGUF se partagent la VRAM/RAM, parce que l’OS reconnaît qu’elles mappent le même inode. Enfin, si le système subit une pression mémoire (un compilateur lancé en parallèle, par exemple), l’OS évince des pages du modèle sans les écrire nulle part (elles sont clean, déjà sur disque), et les recharge au besoin. Le swap classique est évité.

Les schémas de quantification

Le second pilier de llama.cpp, c’est sa famille de schémas de quantification. Le principe est simple : plutôt que de stocker chaque poids sur 16 bits, on regroupe les poids par blocs (typiquement 32 valeurs) et on encode chaque bloc par une échelle commune (un float 16 bits) et des indices de quelques bits qui désignent la valeur quantifiée à l’intérieur de l’intervalle. Les variantes « K » affinent ce schéma avec des sous-blocs et des échelles à deux niveaux, ce qui réduit l’erreur de quantification sur les distributions hétérogènes de poids qu’on rencontre dans les LLM modernes.

SchémaBits / poidsTaille modèle 7BQualité
F1616≈ 14,5 GoRéférence
Q8_08≈ 7,7 GoQuasi identique
Q5_K_M≈ 5,5≈ 5,1 GoTrès bonne
Q4_K_M≈ 4,5≈ 4,4 GoBon compromis
Q2_K≈ 2,6≈ 3,0 GoDégradée
Tableau 1 : Schémas de quantification courants en GGUF (modèle 7 B, ordres de grandeur).

Le sweet spot pratique est Q4_K_M dans la grande majorité des cas : la perte de qualité reste faible et mesurable (de l’ordre du point de perplexité sur les benchmarks standards), pour une empreinte mémoire divisée par trois. En dessous de 3 bits par poids, la dégradation devient nette et dépend beaucoup du modèle : un Llama 70B en Q2_K reste utilisable, un 7B au même format perd visiblement en cohérence.

Le chemin d’un token

Le mieux pour comprendre llama.cpp est de suivre ce qu’il fait, étape par étape, entre le texte d’entrée et le token de sortie.

Tokenizer texte → ids entrée
Graphe GGML couches ordonnancement
Kernels dequant + matmul calcul
Sampler logits → token sortie
Figure 1 : Étapes principales d'un pas de génération dans llama.cpp.

Le point qui mérite l’attention est l’étape « kernels ». Les poids restent quantifiés en mémoire jusqu’au dernier moment ; ils ne sont déquantifiés que dans le kernel, par blocs de 32, juste avant la matmul Multiplication de matrices, l'opération dominante des couches d'un réseau de neurones. Quand on dit qu'un GPU « calcule », il fait à 90 % des matmul. . On lit donc peu d’octets, ce qui est bon pour la bande passante mémoire, qui est presque toujours le facteur limitant en inférence, pour un surcoût de calcul modéré, parce que la déquantification est essentiellement une multiplication par une échelle et un décalage.

Un kernel de déquantification

Voici la forme générale d’une déquantification de bloc Q4 : une échelle par bloc, des poids sur 4 bits, reconstruits à la volée.

ggml dequant_q4.c C
// Déquantifie un bloc Q4 : 32 poids 4 bits + une échelle f16
void dequantize_q4_block(const block_q4 * restrict b, float * restrict out) {
    const float scale = f16_to_f32(b->scale);
    for (int i = 0; i < 16; i++) {
        const uint8_t packed = b->qs[i];      // deux poids par octet
        const int lo = (packed & 0x0F) - 8;   // centré sur zéro
        const int hi = (packed >>   4) - 8;
        out[i]      = scale * (float) lo;
        out[i + 16] = scale * (float) hi;
    }
}

Tout est là : deux poids par octet (c’est ainsi qu’on atteint 4 bits par valeur), un décalage de −8 pour recentrer la valeur sur zéro, une échelle unique par bloc. Multiplié sur les millions de blocs d’un modèle 7B, c’est ce qui transforme un fichier de 14,5 Go en un fichier de 4,4 Go sans toucher au reste du moteur. La même couche d’attention, le même graphe de calcul : seul le format de stockage des poids change.

Quand llama.cpp n’est pas le bon choix

llama.cpp brille sur une à quelques requêtes : son ordonnanceur n’est pas conçu pour entrelacer des dizaines de générations concurrentes à la manière de vLLM. Sans batching continu Ordonnancement à l'itération : ajouter et retirer des requêtes du batch à chaque pas de génération, au lieu d'attendre qu'un batch entier se termine. Formalisé par Orca (OSDI 2022), popularisé par vLLM. Multiplie par 2 à 4 le débit d'un serveur d'inférence sous forte concurrence. Approfondir dans le glossaire , chaque requête supplémentaire allonge la latence de toutes les autres au lieu de partager le coût de la lecture des poids. Et sur GPU datacenter récent, des runtimes spécialisés exploitent mieux les formats FP8 Format à virgule flottante 8 bits. Format de travail polyvalent pour l'inférence (et l'entraînement) sur GPU récents. Divise par 2 l'empreinte mémoire et le débit nécessaires par rapport au FP16, pour une perte de précision marginale sur la plupart des modèles. Approfondir dans le glossaire / FP4 Format à virgule flottante 4 bits, frontière 2026 de l'inférence à haut débit. Quatre fois moins de mémoire que le FP16, mais une portée dynamique très étroite : ne tient qu'avec un scaling fin via formats à blocs (MXFP4, NVFP4). Approfondir dans le glossaire câblés dans le silicium, là où llama.cpp paie une déquantification logicielle. llama.cpp est l’outil du local et du portable, pas du service à grande échelle, qui appartient à d’autres runtimes.

Conclusion

llama.cpp n’a pas gagné par magie ni par marketing : il a gagné en empruntant à l’ingénierie système (mmap, alignement, pagination) ce que l’écosystème ML refusait de faire faute de bibliothèques le permettant. La question intéressante n’est donc pas « pourquoi llama.cpp », c’est : quelles autres optimisations système restent à transposer vers les runtimes d’inférence (gestion mémoire NUMA, pinning, prefetch) avant que la limite redevienne purement matérielle ? Pour le prolongement vers l’embarqué, voir NPU vs GPU à l’edge.

Sources et méthode

Format GGUF. Spécification officielle dans le dépôt GGML (objectifs « single-file » et compatibilité mmap). La spécification officielle ne développe pas l’acronyme. L’expansion la plus citée dans la documentation tierce (Hugging Face, Qwen, communauté) est GPT-Generated Unified Format, reprise ici.

Schémas de quantification. Code source ggml-common.h de llama.cpp, qui définit chaque bloc et son coût effectif en bits par poids (block_q4_K « 4.5 bits », block_q5_K « 5.5 bits », block_q2_K « 2.625 bits »). Compromis qualité : outil quantize.

Déquantification. Fonction de référence dequantize_row_q4_0 dans ggml-quants.c. Le code de l’article est une version simplifiée et fidèle de ce bloc Q4_0 ; il ne reproduit pas une variante « K » (super-blocs et échelles à deux niveaux).

Tailles de fichiers (modèle 7 B). Ordres de grandeur mesurés sur des dépôts GGUF publics, par exemple TheBloke/Mistral-7B-Instruct-v0.2-GGUF. Ils varient de quelques pour cent selon le modèle et le tokenizer.