Faire tourner un 35B sur une RX 9060 XT — ma ligne de commande llama.cpp

GPU - illustration du thème "Faire tourner une IA sur un carte graphique modeste"

On dit qu'il faut des clusters de GPU pour faire tourner des modèles sérieux. Ma configuration tient dans une tour classique, avec une AMD 9060 XT de 16 Go. Voici comment j'ai procédé, paramètre par paramètre.

Le défi

Un modèle de 35 milliards de paramètres, c'est imposant. Même quantifié en Q4 (soit environ 4 bits par paramètre), ça pèse 24 Go en VRAM. Et ma carte AMD RX 9060 XT n'a que 16 Go. On pourrait conclure que ce n'est pas possible, qu'une partie du calcul basculera sur le CPU. Avec ollama c'est sûr, c'est sa limite.

Pourtant, avec les bonnes techniques (et llama.cpp), ça tourne à 30 tokens par seconde en génération.

La solution, c'est une combinaison d'architecture MoE et de quantification agressive que seul llama.cpp sait gérer, de même qu'une gestion plus pointue de la mémoire. Voici ma commande llama.cpp.

La commande

llama-server -hf unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-IQ2_M \
  --cache-type-k q4_0 --cache-type-v q4_0 --no_mmap -c 262144 \
  --kv-unified -np 1 -fa on --spec-type draft-mtp \
  --spec-draft-n-max 2 -ngl 99

Le tout démarre en quelques secondes, via Vulkan. Pour quoi Vulkan ? ROCm ne s'installe pas encore proprement sur Debian, j'ai laissé tomber, mais si vous installez une machine dédiée, n'hésitez pas à installer une Ubuntu, cette distribution est supportée depuis longtemps par AMD.

Pourquoi ce modèle

Le Qwen3.6-35B-A3B est un MoE — Mixture of Experts. C'est-à-dire que sur les 35 milliards de paramètres du modèle, seuls 3 milliards sont actifs à chaque token généré (d'où le "A3B" dans le nom).

C'est utile quand on a un petit GPU. On a la connaissance d'un gros modèle (enfin presque), mais la rapidité d'un petit (enfin... presque, plus d'infos dans le comparatif en bas de l'article). Le modèle complet pèse 35B, mais le GPU n'en calcule que 3B à chaque passe.

En UD-IQ2_M (Ultra Discrete IQ2 à environ 2,7 bits par paramètre), le fichier fourni par Unsloth sur HuggingFace ne pèse que 11,9 Go. Ça tient dans les 16 Go de la 9060 XT, avec un peu de marge pour le reste.

La gestion de la VRAM

11,5 Go pour le modèle, ça laisse environ 4,5 Go pour le reste — le KV cache et les activations. Avec un contexte de 256 000 tokens, le cache explose vite si on ne fait rien.

J'ai choisi deux leviers.

Le KV Cache en q4_0. Les clés et valeurs du cache sont quantifiées en 4 bits, au lieu des 16 bits en pleine précision. Le KV cache passe de 20 Go à 5 Go, mais ça c'est quand le contexte est plein, Hermès fait en sorte de le garder en dessous de 128K tokens. Cette quantification génère quelques hallucinations, mais le compromis est acceptable pour mon usage. Si vous pouvez, utilisez une quantification q8 qui n'a pratiquement pas de perte par rapport au BF16 d'après les auteurs de llama.cpp.

Le cache unifié avec --kv-unified. Sans ce flag, chaque slot de conversation parallèle maintiendrait son propre cache, et donc diviserait le contexte disponible par le nombre de conversations. Par exemple, si j'ai 4 slots, mes 256K de contexte se transforment en 64K par conversation. Ce qui deviendrait peu utilisable avec Hermes.

Le nombre de slots est géré avec -np 1 (un seul thread d'inférence), je n'ai qu'une conversation à la fois parce que je ne veux pas que les conversations se parasitent entre elles. Les plus attentifs auront remarqué que ça ne sert à rien de faire un --kv-unified alors que je n'ai qu'une conversation. Disons que j'ai gardé ce paramètre comme garde-fou si un jour je veux tester l'augmentation des slots.

Au final, avec 256K de contexte, ça tient sans OOM (Out Of Memory). Le compresseur Hermes, se déclenche quand le contexte dépasse 128K (50% de 256K de contexte) mais il ne se déclenche pas trop souvent, et mon KV-cache reste sous les 2.5 Go dans la VRAM.

Le speculative decoding par MTP

D'ordinaire, le speculative decoding demande deux modèles : un petit modèle draft qui génère des tokens candidats très vite, et le grand modèle qui les vérifie. Ça demande de charger deux fichiers en VRAM mais là, il n'y a pas la place.

Pour pallier ça, le Qwen3.6-35B-A3B-A3B intègre une tête MTP -- un petit réseau de prédiction de tokens supplémentaire, entraîné à deviner les prochains tokens. Comme c'est une tête légère et pas un modèle complet, elle tient en VRAM et ça fait le travail.

Avec --spec-type draft-mtp --spec-draft-n-max 2, le modèle génère deux tokens candidats à chaque itération, puis les vérifie en une seule passe parallèle. Le gain en débit dépend du modèle — un MoE comme celui-ci est moins efficace en speculative decoding qu'un modèle dense, parce que l'activation n'est pas aussi prévisible. Le benchmark UnSloth sur RTX 6000 donne environ 15% de gain sur ce modèle en particulier.

C'est modeste, mais ça ne coûte rien en mémoire supplémentaire.

Les petits plus

Llama.cpp peut utiliser mmap pour utiliser un modèle sans le charger en RAM (en VRAM), apparemment c'est plus lent pour le chargement donc j'ai rajouté --no-mmap.

On peut aussi utiliser de la flash attention --fa-on 1 pour accélérer certains calculs. C'est surtout vrai pour Nvidia mais il existe aussi une optimisation pour Vulkan même si elle n'est pas aussi efficace.

Ensuite sous ollama, comme sous llama.cpp, le programme peut choisir de déléguer quelques couches au CPU, ce qui ralente énormément la génération. En utilisant -ngl 99 on charge au moins 99 couches dans le GPU (Qwen3.6 en a 40). Et si ça ne marche pas, le logiciel plantera, mais comme ça je le verrai plus vite.

Le résultat

Le modèle tourne à 30 tokens par seconde en génération sur mon GPU AMD 9060 XT 16 Go via Vulkan. Le prompt processing démarre à 600 tokens par seconde et descend à 300 une fois passés les 120 000 tokens — c'est là que le MoE 35B-A3B souffre le plus sur cette carte.

C'est stable et plutôt réactif.

Le 35B-A3B face au 27B

Le 27B de Qwen est souvent cité comme le petit frère le plus intéressant du moment — meilleur en qualité absolue sur les tâches complexes, d'après les benchmarks. Il est dense : les 27 milliards de paramètres sont actifs à chaque token.

Donc j’ai utilisé llama-bench pour les comparer (attention, ce n’est pas un bench, je n’obtiens pas ses performances dans la vie en utilisant Hermes).

$ build/bin/llama-bench -m /home/seba/.cache/huggingface/hub/models--unsloth--Qwen3.6-35B-A3B-MTP-GGUF/snapshots/5bc3e238d916f48a861bac2f8a1990a0e9b7e98d/Qwen3.6-35B-A3B-UD-IQ2_M.gguf -ub 64,256,512,1024 -ngl 99 --cache-type-k q4_0 --cache-type-v q4_0 -fa on
WARNING: radv is not a conformant Vulkan implementation, testing use only.
ggml_vulkan: Found 1 Vulkan devices:
ggml_vulkan: 0 = AMD Radeon Graphics (RADV GFX1200) (radv) | uma: 0 | fp16: 1 | bf16: 0 | warp size: 64 | shared memory: 65536 | int dot: 1 | matrix cores: none
| model | size | params | backend | ngl | n_ubatch | type_k | type_v | fa | test | t/s |
| ------------------------------ | ---------: | ---------: | ---------- | --: | -------: | -----: | -----: | --: | --------------: | -------------------: |
| qwen35moe 35B.A3B IQ2_M - 2.7 bpw | 11.06 GiB | 35.51 B | Vulkan | 99 | 64 | q4_0 | q4_0 | 1 | pp512 | 225.70 ± 5.32 |
| qwen35moe 35B.A3B IQ2_M - 2.7 bpw | 11.06 GiB | 35.51 B | Vulkan | 99 | 64 | q4_0 | q4_0 | 1 | tg128 | 40.41 ± 0.06 |
| qwen35moe 35B.A3B IQ2_M - 2.7 bpw | 11.06 GiB | 35.51 B | Vulkan | 99 | 256 | q4_0 | q4_0 | 1 | pp512 | 364.75 ± 2.18 |
| qwen35moe 35B.A3B IQ2_M - 2.7 bpw | 11.06 GiB | 35.51 B | Vulkan | 99 | 256 | q4_0 | q4_0 | 1 | tg128 | 40.31 ± 0.31 |
| qwen35moe 35B.A3B IQ2_M - 2.7 bpw | 11.06 GiB | 35.51 B | Vulkan | 99 | 512 | q4_0 | q4_0 | 1 | pp512 | 902.04 ± 5.84 |
| qwen35moe 35B.A3B IQ2_M - 2.7 bpw | 11.06 GiB | 35.51 B | Vulkan | 99 | 512 | q4_0 | q4_0 | 1 | tg128 | 40.44 ± 0.09 |
| qwen35moe 35B.A3B IQ2_M - 2.7 bpw | 11.06 GiB | 35.51 B | Vulkan | 99 | 1024 | q4_0 | q4_0 | 1 | pp512 | 904.84 ± 8.19 |
| qwen35moe 35B.A3B IQ2_M - 2.7 bpw | 11.06 GiB | 35.51 B | Vulkan | 99 | 1024 | q4_0 | q4_0 | 1 | tg128 | 40.43 ± 0.09 |

build: 0d2d9ccbf (9727)
$ build/bin/llama-bench -m /home/seba/.cache/huggingface/hub/models--unsloth--Qwen3.6-27B-MTP-GGUF/snapshots/5cb35eb3dcbf52dbce5f87dbc64df6aaffadcace/Qwen3.6-27B-UD-IQ2_M.gguf -ub 64,256,512,1024 -ngl 99 --cache-type-k q4_0 --cache-type-v q4_0 -fa on
WARNING: radv is not a conformant Vulkan implementation, testing use only.
ggml_vulkan: Found 1 Vulkan devices:
ggml_vulkan: 0 = AMD Radeon Graphics (RADV GFX1200) (radv) | uma: 0 | fp16: 1 | bf16: 0 | warp size: 64 | shared memory: 65536 | int dot: 1 | matrix cores: none
| model | size | params | backend | ngl | n_ubatch | type_k | type_v | fa | test | t/s |
| ------------------------------ | ---------: | ---------: | ---------- | --: | -------: | -----: | -----: | --: | --------------: | -------------------: |
| qwen35 27B IQ2_M - 2.7 bpw | 10.26 GiB | 27.32 B | Vulkan | 99 | 64 | q4_0 | q4_0 | 1 | pp512 | 113.03 ± 0.17 |
| qwen35 27B IQ2_M - 2.7 bpw | 10.26 GiB | 27.32 B | Vulkan | 99 | 64 | q4_0 | q4_0 | 1 | tg128 | 13.70 ± 0.02 |
| qwen35 27B IQ2_M - 2.7 bpw | 10.26 GiB | 27.32 B | Vulkan | 99 | 256 | q4_0 | q4_0 | 1 | pp512 | 77.82 ± 0.22 |
| qwen35 27B IQ2_M - 2.7 bpw | 10.26 GiB | 27.32 B | Vulkan | 99 | 256 | q4_0 | q4_0 | 1 | tg128 | 13.70 ± 0.01 |
| qwen35 27B IQ2_M - 2.7 bpw | 10.26 GiB | 27.32 B | Vulkan | 99 | 512 | q4_0 | q4_0 | 1 | pp512 | 195.58 ± 0.21 |
| qwen35 27B IQ2_M - 2.7 bpw | 10.26 GiB | 27.32 B | Vulkan | 99 | 512 | q4_0 | q4_0 | 1 | tg128 | 13.72 ± 0.01 |
| qwen35 27B IQ2_M - 2.7 bpw | 10.26 GiB | 27.32 B | Vulkan | 99 | 1024 | q4_0 | q4_0 | 1 | pp512 | 195.67 ± 0.15 |
| qwen35 27B IQ2_M - 2.7 bpw | 10.26 GiB | 27.32 B | Vulkan | 99 | 1024 | q4_0 | q4_0 | 1 | tg128 | 13.72 ± 0.01 |

build: 0d2d9ccbf (9727)

Le tg128 (token generation), c'est la vitesse de génération d'une réponse avec un contexte de 128 tokens. C'est ce que vous voyez s'écrire petit à petit.

Le pp512, lui, c’est le test prompt processing. Le modèle prend les fameux tokens et les convertit en tenseurs sémantiques (une grosse suite de nombres à virgule flottante qui représente la signification du token). “Et 100-200 token/s, ça va, c’est rapide par rapport à la génération.” Eh bien non! C’est horriblement lent. Quand j’ai un contexte de 128K token, ça met 1000 secondes à calculer, soit 15 minutes.

Heureusement il y a un cache, mais dès qu'on change de conversation ou qu'Hermes compacte la conversation, il faut retraiter le prompt... et là, je vais prendre un café, enfin avec cette chaleur, je vais plutôt prendre un thé... un thé glacé.

J’ai eu l’occasion de faire tourner les mêmes modèles sur une carte Nvidia RTX 5070 Ti avec 16 RAM, les constats sont un peu les mêmes.

Conclusion

Je suis assez content de mon setup, mais gardez bien en tête que les quantifications appliquées ici sont très agressives et font perdre de la pertinence à ce super modèle.

J'ai quand même deux griefs qui rendent cette solution peu agréable au quotidien : * Le prompt processing entre 600 et 300 token/s (il décroît pendant qu’il traite le prompt), ça pénalise beaucoup lors de la compression du prompt. Une carte avec plus de bande passante serait plus efficace dans ce cas. * L’impossibilité de faire plusieurs conversations en même temps : pour moi ça veut dire pas deux conversations ou vibe coding avec mon agent en même temps, mais pour une entreprise ça veut dire pas d’utilisateurs en parallèle.

Et tout ça c’est à cause du manque de RAM donc si vous pouvez, privilégiez une installation avec au minimum 32 Go de VRAM, même sur des APU AMD. Ou alors il faut tester des modèles plus petits. Il parait que Gemma 4 14B n'est pas mal.

Quelques sources

Pour ceux qui veulent creuser, voici les sources qui ont nourri le post :

  1. llama.cpp — README serveur officiel — ggerganov / ggml-org, 2026, https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md
  2. Unsloth Qwen3.6-35B-A3B-GGUF — Unsloth, avril 2026, https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF — Le GGUF UD-IQ2_M utilisé dans la commande (11,5 Go, contexte 262144 tokens par défaut).
  3. MTP (Multi-Token Prediction) speculative decoding — llama.cpp PR #22673, mergée le 16 mai 2026, https://github.com/ggml-org/llama.cpp/pull/22673
  4. KV cache quantization (--cache-type-k q4_0) — llama.cpp discussion TurboQuant https://github.com/ggml-org/llama.cpp/discussions/20969

Commande testée sur AMD RX 9060 XT 16 Go, Debian, llama-server.compiled (build/bin/llama-server), modèle Qwen3.6-35B-A3B-MTP par Unsloth (HuggingFace).