Un projet open-source qu'on utilise sérieusement finit toujours par nous faire buter contre une de ses limites. Sur notre banc Loom, cette limite était un plancher de latence qu'on ne comprenait pas. Trois semaines de sondes plus tard, on a trouvé la cause racine, écrit un fix de six lignes, et ouvert notre première PR sur ggml-org/llama.cpp. Voici comment.
La lenteur qu'on ne comprenait pas
Sur notre configuration de banc (RTX 2060 en principal, iGPU intégré côté laptop pour du calibration croisée), la création d'un checkpoint de contexte prenait ~3,3 secondes. Multiplié par deux dans chaque tour (avant / après), ça faisait un plancher de latence d'environ 8 secondes qu'aucun réglage de llama.cpp ne semblait pouvoir descendre. Sur nos MoE 35B avec offload d'experts en RAM, le décodage progressait à ~8 tokens/seconde : 30 % en dessous de ce que la même machine faisait sur du dense en full-VRAM.
La faute au réglage ? Non : mêmes flags, même modèle, même OS. On a passé une bonne semaine à supposer que c'était une histoire de flash-attention, de KV cache, de layouts. Toutes les hypothèses se sont écroulées face au log verbeux horodaté du run.
La sonde
La méthode qui a fini par marcher : instrumenter llama.cpp pour horodater chaque étape du chemin critique du prefill, puis lire les logs sans a priori. Le coupable est apparu net : ggml_vk_buffer_read_2d, l'appel Vulkan qui lit un buffer côté hôte pour construire le checkpoint. Sur les architectures à mémoire unifiée (iGPU Intel, APU AMD, Strix Halo, et donc aussi notre laptop de test), cet appel memcpy lit directement depuis la mémoire host-visible.
Le pattern surprise
Cette mémoire host-visible côté Vulkan est en WRITE-COMBINED non-cachée. Les écritures sont rapides (11 ms pour le blob de 62 Mio testé). Les lectures plafonnent à ~19 Mo/s.Chaque création de checkpoint relit 62,8 Mio de données à travers ce chemin non-caché : 3,3 secondes. Deux fois par prompt : le plancher ~8 s. Ce n'était ni un bug applicatif, ni un choix de llama.cpp ; c'était un piège des APIs Vulkan sur UMA que le code appliquait sans le savoir.
Le fix : six lignes
La correction est déconcertante par sa taille. Au lieu de faire un memcpy direct depuis la mémoire host-visible WRITE-COMBINED, on route la lecture par un chemin device + staging buffer : la GPU copie vers un buffer cachable, puis on lit depuis là. Le code applicatif ne change pas ; c'est un remplacement local dans le back-end Vulkan de llama.cpp.
Six lignes dans le fichier ggml/src/ggml-vulkan/ggml-vulkan.cpp. Résultat sur notre banc, avec les mêmes prompts et les mêmes arguments avant/après :
- Checkpoint iGPU/UMA : 3,3 s → 25 ms (×130)
- Append d'un token (modèle ornith) : 7,4 s → 0,67 s (×11)
- Décode MoE offloadé : 8,3 → 9,2 t/s (+11 %)
Le +130 sur le checkpoint est le chiffre qui rend le fix intéressant au-delà de notre cas : n'importe quel utilisateur de llama.cpp sur iGPU (Intel), APU (AMD Ryzen mobile), ou architecture unifiée type Strix Halo bénéficie potentiellement du même ordre de grandeur : sur les checkpoints, les slot saves, et par ricochet sur les sessions persistantes.
PR n°26004 : la règle du repo
Notre première PR sur ggml-org/llama.cpp est ouverte : n°26004. Elle porte un correctif complémentaire découvert avant celui-ci : le hot resume côté serveur pour les slot KV. Elle est en revue.
Le fix Vulkan cached readback est en queue : le repo ggml-org applique la règle une seule PR ouverte à la fois pour un nouveau contributeur. C'est sain, et c'est un choix de mainteneurs qui protège leur capacité à réviser. On patiente. En attendant, notre laptop tourne sur une build maison patchée avec les deux fixes ; les gains sont ceux mesurés ci-dessus.
Ce que ça dit d'une pratique open-weight
On aurait pu écrire un article « sur » l'open-source, comme tant d'autres. On préfère montrer ce que ça implique concrètement : quand tu utilises sérieusement un projet open-source, tu finis par y contribuer. Pas par militantisme, par nécessité. Une lenteur inexpliquée devient une sonde, une sonde devient une cause racine, une cause racine devient un patch, et un patch bien étayé devient une PR.
Il y a une symétrie qui compte ici avec notre note précédente : dans un écosystème sain, ceux qui bénéficient participent à sa maintenance. Sinon on redevient dépendant, juste d'un maintainer bénévole au lieu d'un cloud provider. C'est le même problème structurel.
Loom est publié sous MIT sur GitHub Tough-Respawn. La PR 26004 est visible sur le repo llama.cpp. Nous rendrons compte de son merge, et de la PR n°2 quand elle sera ouverte : dans une prochaine note.