Sur un Snapdragon X Elite, llama.cpp en OpenCL sur le GPU Adreno génère quatre fois moins vite que sur le CPU : 4-5 jetons/s contre 21 avec gemma-4-E4B. Le GPU ne gagne que 2 s sur la lecture à froid du prompt (20,1 s contre 22,1 s). Le mode hybride (lecture sur le GPU, génération sur le CPU) ne fait pas mieux que le CPU seul. J’ai donc gardé le CPU pour l’assistant du Journal des Nuisances, et cherché le gain du côté d’un modèle plus petit.
Le contexte : chercher un modèle plus rapide pour l’assistant
En mars, j’avais constaté qu’Ollama et LM Studio tournent en CPU seul sur Snapdragon, et que llama.cpp avait intégré un backend Adreno GPU via OpenCL. Voici la suite concrète : ce backend, mesuré sur une vraie charge.
La machine est pc-llm, un PC Snapdragon X Elite 64 Go (GPU Adreno X1-85). Elle sert l’assistant du Journal des Nuisances, une application de suivi de nuisances que j’ai co-fondée, avec gemma4:26b dérivé en assistant-gemma4 sur Ollama, CPU seul. La référence à battre, mesurée le 10 octobre 2026 :
| Scénario | Lecture du prompt | Total |
|---|---|---|
| Première question | 34,8 s (27 jetons/s) | 35,8 s |
| Question suivante | 3,2 s | 7,3 s |
| Froid (prompt hors cache) | ~85 s | — |
Critère retenu : une lecture au moins trois fois plus rapide (≥ ~85 jetons/s), avec réutilisation du cache de préfixe du prompt sur l’accélérateur.
Pourquoi llama-server à côté d’Ollama
Ollama garde au plus deux modèles chargés (OLLAMA_MAX_LOADED_MODELS=2). Charger un candidat déchargerait l’un des deux modèles en service ; pour l’assistant, c’est 6 min 38 s avant de revenir. Et Ollama n’utilise pas l’Adreno. Les candidats tournent donc sur llama-server, le serveur HTTP de llama.cpp (build b11541), un port par backend : 8080 pour le CPU, 8081 pour OpenCL, 8082 pour Vulkan, ouverts au seul VPN.
llama.cpp publie trois archives Windows ARM64 : llama-b11541-bin-win-cpu-arm64.zip, llama-b11541-bin-win-opencl-adreno-arm64.zip et llama-b11541-bin-win-vulkan-arm64.zip. Pas de version précompilée du backend Hexagon pour le NPU (45 TOPS qui restent inutilisés) : il faudra le compiler, pour des modèles de moins de 4 milliards de paramètres.
⚠️ Au premier essai, le GPU était bloqué par Smart App Control, le filtrage d’applications de Windows 11. Je l’ai désactivé sur cette machine, isolée sur le VPN ; llama.cpp OpenCL a alors vu l’Adreno X1-85.
# tout sur le GPU ; -ngl 0 pour le mode hybride, binaire CPU pour le processeur
C:\llama-assistant\bin\opencl\llama-server.exe `
-m C:\llama-assistant\modeles\gemma-4-E4B_q4_0-it.gguf `
--host 10.66.0.20 --port 8081 -c 8192 -np 1 --jinja -ngl 99 --swa-full
--swa-full est indispensable avec Gemma 4 : ses couches à fenêtre glissante empêchent sinon llama.cpp de réutiliser le préfixe. Chaque requête porte aussi "cache_prompt": true.
Le GPU Adreno est-il plus rapide que le CPU avec llama.cpp ?
Mesures sur gemma-4-E4B (Q4_0), vrai prompt de l’assistant avec le contexte d’un journal de démonstration de 45 incidents, deux passages par mode :
| Mode | Lecture à froid (3 499 jetons) | Génération | 1ʳᵉ question | Question suivante |
|---|---|---|---|---|
| Processeur (binaire CPU) | 22,1 s | 21 jetons/s | 10,7 s | 6,5 s |
Tout sur le GPU (OpenCL, -ngl 99) | 20,1 s | 4-5 jetons/s | 15,9-24,4 s | 18,3 s |
Hybride (OpenCL, -ngl 0) | 19,7 s | 21 jetons/s | 9,0-9,2 s | 4,0-4,8 s |
Le mode hybride lance le binaire OpenCL sans couche sur le GPU : les poids et la génération restent sur le CPU, et llama.cpp confie au GPU les gros calculs de lecture du prompt (op offload). Il gagne 2,4 s sur la lecture à froid (19,7 s contre 22,1 s), une fois par journal. La lecture d’une question et la génération sont identiques au CPU ; les écarts de total viennent de la longueur des réponses.
Sur Snapdragon X Elite, l’Adreno en OpenCL accélère à peine la lecture du prompt et divise la génération par quatre : le CPU reste le bon choix.
Six petits modèles mesurés sur le CPU
Le gain est donc venu d’un modèle plus petit. Six bases denses sous licence Apache 2.0, mesurées le 10 octobre sur le CPU (réflexion désactivée, même prompt) :
| Candidat | Froid | 1ʳᵉ question | Suivante | Génération | Verdict |
|---|---|---|---|---|---|
| gemma-4-E2B | 13,0 s | 5,0 s | 1,5 s | 40 jetons/s | brève, à côté, n’invente rien |
| gemma-4-E4B | 24,3 s | 10,7 s | 6,5 s | 21 jetons/s | la plus pertinente |
| qwen3-4B-2507 | 42,9 s | 23,1 s | 28,9 s | 9-13 jetons/s | trop lent à générer |
| smollm3-3B | 15,9 s | 8,7 s | 14,2 s | 14-23 jetons/s | invente des faits |
| luciole-1B | 11,4 s | 10,2 s | 16,7 s | 22-34 jetons/s | invente des faits |
| ministral-3-3B | 30,1 s | 22,0 s | 33,2 s | 7-13 jetons/s | invente des faits |
Restent les deux Gemma 4 : E2B, 6 à 7 fois plus rapide que la référence, et E4B, 3 fois plus rapide.
Deux pièges de requête en chemin. Sans "chat_template_kwargs": {"enable_thinking": false}, Gemma 4 passait ses 400 jetons à réfléchir et la réponse arrivait vide. Et le gabarit de conversation de Ministral 3 refuse un second message system : il faut fusionner le socle et le contexte en un seul, dans le même ordre pour garder le préfixe.
Lessons learned
- Le GPU Adreno en OpenCL génère quatre fois moins vite que le CPU. 4-5 jetons/s contre 21 sur gemma-4-E4B, pour 2 s gagnées sur la lecture à froid.
- Le mode hybride de llama.cpp ne vaut pas plus que le CPU seul sur Snapdragon. Seule la lecture à froid gagne 2 s.
- Smart App Control peut bloquer le GPU sous Windows. L’Adreno n’est apparu dans llama.cpp qu’après sa désactivation, sur une machine isolée.
- Un petit modèle bat un accélérateur. gemma-4-E2B sur le CPU répond 6 à 7 fois plus vite que gemma4:26b.
- llama-server à côté d’Ollama évite de décharger le modèle de production. Avec
OLLAMA_MAX_LOADED_MODELS=2, un candidat de plus aurait coûté 6 min 38 s de rechargement.