Pourquoi deux cartes RTX 3090 atteignent-elles 55 tok/s au lieu de 220 avec SGLang et DFlash2 ?
Une présentation pratique de l’exécution de Qwen3.8-27B sur deux cartes 3090 à l’aide de SGLang, ainsi que de la manière dont les liaisons PCIe x4, l’absence de fonction P2P, un cache KV trop restreint et des pics d’énergie ont limité la vitesse annoncée.
Une courte vidéo a circulé parmi les amateurs d’hébergement autonome. Elle montre Qwen3.8-27B écrire du code à l’aide de deux cartes RTX 3090, avec une vitesse de génération atteignant 220 tokens par seconde. La configuration utilise SGLang associé à DFlash2, un outil de draftage spéculatif. Ce petit modèle propose simultanément sept tokens potentiels, que le modèle plus grand accepte ou rejette en une seule passe, de sorte que les prédictions correctes ne coûtent rien.
L’intérêt dépasse celui d’une simple démonstration. À cette vitesse, un agent pourrait faire appel à un modèle hébergé localement, permettant d’exécuter plusieurs agents de codage en parallèle sur une seule machine, sans clés API, sans facturation basée sur la consommation et sans transfert de données hors du bâtiment. Un serveur domestique équipé de deux GPU de type 3090 semble idéal : il offre deux fois plus de bande passante mémoire, ce qui devrait assurer bien 220 tokens par seconde.
Cela ne fonctionne pas de cette manière. Les raisons sont plus éducatives que la vidéo elle-même.
Installation de SGLang
Tout ceux qui sont habitués à llama.cpp, où un seul binaire et un fichier de modèle permettent d’avoir une conversation en dix minutes, trouveront SGLang plutôt déroutant. Voici la séquence à suivre sur un serveur Ubuntu.
Étape 1 : l’environnement Python. SGLang possède un arbre de dépendances très complexe, il faut donc créer son propre environnement virtuel basé sur Python 3.12 après avoir cloné le répertoire.
git clone https://github.com/sgl-project/sglang.git
cd sglang
python3.12 -m venv .venv
source .venv/bin/activate
Étape 2 : une installation difficile. La première tentative d’installation véritable a duré environ 39 minutes, remplissant le cache des paquets avec la majeure partie du contenu de PyPI, avant d’être interrompue en raison d’une erreur.
RuntimeError: cargo is required to discover the Rust extension modules
SGLANG_BUILD_RUST_EXTS=none pip install -e "./python[all]"
Ce deuxième essai a nécessité encore environ 30 minutes avant de se terminer sans problème. Le résultat obtenu était sglang 0.5.20, torch compilé pour CUDA 13 et triton automatiquement fixé à 3.7.1, ce qui correspond justement aux besoins des GPU Ampere pour éviter un bug connu du noyau.
Étape 3 : les modèles. Deux téléchargements suivent la méthode recommandée par la communauté :
cyankiwi/Qwen3.8-27B-AWQ-INT4est le modèle principal. La quantification INT4 réduit sa taille à environ 20 GB en 5 fragments, suffisamment petit pour être stocké sur deux GPU de 24 GB.z-lab/Qwen3.8-27B-DFlash2est une version préliminaire, d’environ 3,9 GB.
Étape 4 : le lancement. Une seule commande suffit, avec TP=2 indiquant un parallélisme des tenseurs sur les deux cartes 3090.
CUDA_VISIBLE_DEVICES=1,2 python -m sglang.launch_server \
--model-path /mnt/models/Qwen3.8-27B-AWQ-INT4 \
--tp 2 \
--speculative-algorithm DFLASH \
--speculative-draft-model-path /mnt/models/Qwen3.8-27B-DFlash2 \
--speculative-num-draft-tokens 8 \
--mamba-radix-cache-strategy extra_buffer \
--disable-prefill-cuda-graph \
--cuda-graph-max-bs-decode 16 \
--mem-fraction-static 0.85 \
--context-length 65536 \
--reasoning-parser qwen3 \
--host 0.0.0.0 --port 30000
Faites attention à deux pièges. Un paramètre nommé --cuda-graph-max-bs empêche le démarrage, car les versions récentes l’ont renommé pour distinguer les variantes de décodage et de préremplissage. De plus, omettre --reasoning-parser qwen3 fait en sorte que l’API renvoie la réflexion brute du modèle, avec des balises think imbriquées, directement dans le champ de contenu normal. L’ajout de ce parseur divise la sortie en champs bien organisés reasoning_content et content, même format utilisé par les autres serveurs compatibles OpenAI, ce qui sera apprécié par quiconque écrit un client.
Étape 5 : attendre la fin du processus. Le démarrage lors de la première exécution a duré environ six minutes jusqu’à l’apparition du message d’indication prêt. Rien n’a échoué : SGLang compile les noyaux GPU en temps opportun via Triton et enregistre les graphes CUDA sur chaque carte. Les démarrages ultérieurs réutilisent le cache et se terminent en environ 35 secondes. La barre de progression qui semble bloquée pendant ce premier démarrage correspond simplement à la compilation en cours.
Résultats des mesures
Le serveur fonctionnait correctement. Il répondait aux demandes, diffusait reasoning_content et acceptait les connexions des agents. Voici les chiffres mesurés directement sur la machine :
| Metric | Min | Max | Average |
|-------------------------|----------|-----------|-----------|
| Decode speed | 47 t/s | ~100 t/s | ~55 t/s |
| Prompt prefill | 342 t/s | 466 t/s | ~428 t/s |
| Accept length (DFlash) | 3.2 | 6.8 | ~4 |
Et voici l’image de la capacité :
| Capability | Advertised limit | Reality |
|-------------------------|---------------------------|-----------------------|
| Max context per request | 65,536 (I set it) | fits, one at a time |
| Total KV pool | ~131K tokens max | 103K at default flags |
| Parallel slots | 48 concurrent requests | 3–6 with real prompts |
Cela représente en moyenne environ 55 tokens par seconde, et non 220. Le problème, c’est qu’une seule carte RTX 3090 Ti utilisant llama.cpp atteint environ 75 tokens par seconde avec la même famille de modèles. Deux cartes utilisant le parallélisme tensoriel ainsi qu’un outil de génération avancé sont inférieures à une seule carte employant l’approche plus ancienne.
Les taux d’acceptation expliquent en partie cette différence. Lorsque DFlash devine correctement, avec 6,8 sur 8 tokens acceptés, les lots atteignent temporairement environ 100 t/s. Lorsqu’il se trompe, la vitesse tombe à 47. Même les meilleurs lots étaient loin de 220, donc l’outil de génération n’était pas la principale cause. Quelque autre facteur ralentissait la performance.
Le indice dans le journal
L’indice se trouvait tout du long dans le journal de démarrage :
Setup Custom allreduce failed with CUDART error: peer access is not
supported between these two devices.
Sous le parallélisme tensoriel, chaque matrice de poids est divisée entre les deux GPU, et les sorties partielles doivent être fusionnées après chaque bloc d’attention et chaque bloc MLP. Dans un modèle de 27 milliards de paramètres, cela représente environ 100 allers-retours pour chaque token généré. Une telle fusion nécessite un lien rapide entre les cartes, or ce couple n’en disposait pas. Deux pannes distinctes se sont aggravées mutuellement.
Aucun fonctionnement pair-à-pair. Les cartes GeForce ne prennent plus officiellement en charge le P2P GPU-direct, de sorte que chaque transfert doit passer par la RAM du système : depuis le GPU 1, vers la mémoire, jusqu’au GPU 2 et retour. Cela double le nombre de copies et triple la latence.
Un lien limité, le véritable problème. Vérifier l’état du lien physique donne la réponse :
/sys/bus/pci/devices/05:00.0/current_link_width → 4 (max 16)
/sys/bus/pci/devices/09:00.0/current_link_width → 4 (max 16)
Les deux GPU utilisaient une liaison PCIe x4, et non x16. Avec quatre voies chacun, acheminées via le pont hôte, la vitesse de transfert entre les cartes n’atteint que 1 à 2 GB/s, or la charge de travail utilise cette liaison environ 100 fois par opération. Les GPU restaient inactifs en attendant les copies. Cela explique pourquoi le préremplissage fonctionne à seulement 428 t/s, alors que l’on s’attendait à 1 200 ou plus, et pourquoi le décodage stagne autour de 55.
Les GPU sont rapides. Le câble qui les relie est lent. Avec le parallélisme tensoriel, ce câble devient en fait le modèle lui-même.
Comment quelqu’un peut atteindre 220
Nul ne montre sa carte mère lors d’une démonstration. Une paire x16 correctement câblée avec un P2P fonctionnel offre une bande passante entre cartes environ dix fois supérieure à celle de cette configuration. Le chiffre indiqué était très probablement le résultat d’un court laps de temps avec du code prévisible, des caches chauds et un fonctionnement idéal lors du test, et non un taux de performance continu. La partie logicielle de cette solution est solide, mais elle suppose silencieusement l’existence d’un matériel que seules quelques machines domestiques possèdent.
Deux autres constatations pratiques
La salle de contexte est plus petite que prévu. Deux cartes de 24 GB semblent suffisantes au premier abord. Mais il faut ensuite déduire les poids INT4 (environ 10 GB par carte), le moteur de génération, les graphes CUDA, ainsi que la mémoire d’état récurrente, car Qwen3.8 utilise une attention hybride. Ce qui reste pour le cache KV s’élève à environ 103 K de tokens au total, ou 131 K avec des paramètres plus risqués. Une requête de 65 K de tokens convient parfaitement. Les 48 slots simultanés annoncés ne sont pas réalisables, car chaque conversation en cours occupe également une partie de la mémoire d’état Mamba ; en pratique, seules trois à six sessions peuvent être gérées. En revanche, llama.cpp consacre presque toute la VRAM à un seul modèle et gère des contextes volumineux sans problème.
Redémarrage dû à des problèmes d’alimentation. Lorsque les deux cartes 3090 fonctionnent ensemble, en exécutant des tâches de précharge et de création sous une charge complète en parallélisme tensoriel, la consommation électrique passe en quelques millisecondes de zéro à environ 700 watts. Les alimentations électriques ne parviennent pas à gérer une telle augmentation. L’un de ces pics a déclenché la protection et éteint le serveur au milieu du processus. Il s’est redémarré automatiquement, mais la leçon reste : l’utilisation de deux cartes pour ce type de charge exige une alimentation électrique de qualité disposant d’une marge suffisante pour faire face aux pics de consommation, et la puissance indiquée sur l’emballage ne suffit pas.
Faut-il essayer cela à la maison ?
SGLang plus DFlash2 représentent une ingénierie impressionnante tant que leurs hypothèses sont respectées, et ces hypothèses correspondent à une liste de vérification matérielle :
- Deux GPU dans des emplacements x16 réels. Les cartes mères destinées au gaming grand public offrent généralement un seul GPU, éventuellement accompagné d’un second connecté via le chipset en mode x4. Lisez le manuel avant d’acheter quoi que ce soit, car c’est ce critère qui fait la différence entre 220 et 55.
- Communication GPU-to-GPU fonctionnelle. Cela implique l’utilisation de Resizable BAR ainsi que des ajustements ACS dans le BIOS, ou encore des versions modifiées de pilotes pour GeForce ou NVLink utilisées en dehors du secteur grand public. En réalité, cela relève plutôt du domaine des cartes mères serveur.
- Alimentation électrique de haute qualité disposant d’une marge suffisante. Les pics de consommation générés par deux cartes de 350 W finiront tôt ou tard par mettre à rude épreuve une alimentation insuffisante.
- Quatre RTX 3090 pour Qwen3.8-27B avec un contexte complet. Avec un tel budget, la mémoire cache KV cesse d’être un frein, contrairement à un pool de 103K moins performant.
Un logiciel de ce type ne pardonne pas l’utilisation d’équipement bon marché. Il fonctionne plus lentement en raison de problèmes que personne n’a pensé à vérifier.
Même en l’absence des deux composants requis, à savoir le logiciel et le matériel auxquels il fait référence, llama.cpp reste le choix judicieux. Il suffit d’une seule carte, d’un seul fichier binaire, d’environ 75 tokens par seconde via un connecteur PCIe standard, sans outil Rust ni redémarrages inattendus. Les deux GPU peuvent fonctionner de nouveau de manière indépendante à une fréquence x4.
Le chiffre de 220 était exact. Il ne s’applique tout simplement pas à cette carte, et probablement pas non plus à beaucoup d’autres tant que le câble reliant les cartes n’aura pas été vérifié.