Depuis mon dernier article sur DeepSeek, j’ai continué à travailler sur les deux PGX que j’ai à disposition à l’université. Pour ceux qui découvrent le blog, ce sont deux petites stations de travail Lenovo, basées sur la même plateforme que les DGX Spark de NVIDIA, sur lesquelles je fais mes essais d’inférence locale. J’en avais parlé ici, avec les premiers résultats sur DeepSeek-V4-Flash.

À ce moment-là, j’avais déjà commencé à utiliser le modèle avec OpenCode, et c’est ce qui me donnait envie de poursuivre. Pouvoir travailler avec un modèle qui tourne sur les machines que l’on administre, sans surveiller un quota qui va se réinitialiser dans quelques heures, c’est quand même appréciable. Il restait cependant beaucoup de choses à explorer : les performances selon les demandes, les longs contextes, et le comportement du serveur quand plusieurs requêtes arrivent en même temps.

C’est dans cette continuité que j’ai travaillé sur GLM-5.3-Flash. Je voulais voir ce qu’on pouvait en tirer sur les mêmes machines, en allant assez loin dans les réglages pour comprendre ce qui améliorait réellement l’usage. Et comme souvent avec ce genre de projet, les essais ont fini par prendre plus de place que la simple installation du modèle.

J’ai regroupé ce travail dans un dépôt GitHub, avec les scripts de déploiement, les profils et les benchmarks. Dans cet article, je vais reprendre le cheminement : comment j’ai établi une base qui fonctionne, ce qui a permis d’accélérer le modèle, puis ce qui s’est passé lorsque je lui ai demandé de gérer plusieurs requêtes. Si vous n’avez jamais touché à ce type de matériel, vous devriez quand même pouvoir suivre ; on expliquera les notions au fur et à mesure.

Repartir des deux PGX

Pour commencer, il faut avoir en tête ce qu’on essaie de faire rentrer dans ces deux boîtiers. GLM-5.3-Flash est un modèle de 320 milliards de paramètres, et la version que j’utilise pour cette première configuration est le checkpoint NVFP4 publié par LibertAIDAI, à partir du modèle de Z.ai.

NVFP4 désigne un format de quantification sur 4 bits. L’idée est de réduire la précision d’une partie des poids pour diminuer leur taille en mémoire. Certains éléments restent stockés à plus haute précision, et il faut aussi compter les facteurs d’échelle nécessaires à la quantification. Au total, les fichiers de poids de ce checkpoint représentent environ 181,3 GiB. L’audit du dépôt détaille leur contenu.

En face, chaque PGX dispose d’une GB10 et de 128 Go de mémoire unifiée. Le CPU et le GPU partagent cette mémoire à l’intérieur de la machine. Avec les deux nœuds, on dispose donc d’assez de capacité pour envisager ce déploiement, à condition de répartir le travail entre eux et de conserver de la place pour le reste du moteur. Les caractéristiques de la plateforme sont disponibles chez NVIDIA.

C’est ce que permet le tensor parallelism, configuré ici avec TP=2 : les deux GPU participent au calcul d’une même réponse. Ils doivent donc échanger des données pendant l’inférence, par le lien à 200 Gb/s qui relie les machines. La mémoire reste répartie sur deux nœuds ; le réseau a toujours un coût.

On retrouve déjà le sujet de mon article sur la mémoire et l’inférence. Faire tenir les poids est une première étape. Il faut ensuite pouvoir les exploiter, conserver l’état des conversations et absorber les allocations temporaires du moteur.

D’abord, avoir une base sur laquelle travailler

Le choix du moteur a été un premier point à régler. Pour cette voie NVFP4, je me suis appuyé sur SGLang et sur le runtime adapté au GB10 par 0xSero. L’audit initial signalait un problème de sorties incorrectes avec le chemin GB10 de l’image vLLM disponible à la sortie du modèle. Il fallait donc partir d’une combinaison dont la compatibilité avait été vérifiée.

Cette précision concerne la configuration de départ. Le dépôt contient aussi une autre voie utilisant vLLM, avec des poids EXL3 ; on en reparlera à la fin pour le long contexte.

Autour de la base SGLang, j’ai regroupé les opérations nécessaires au déploiement : préparer les deux nœuds, vérifier les fichiers du modèle, démarrer le worker avant le nœud principal et contrôler que le service répond. J’ai aussi ajouté des garde-fous mémoire et un arrêt coordonné lorsqu’un des deux nœuds échoue pendant le lancement.

L’intérêt était de pouvoir revenir à un état connu entre les essais. Quand on change un profil et que le comportement devient différent, il faut savoir ce qu’on vient de modifier. Les poids, l’image du moteur et même le chat template, qui transforme les messages en entrée pour le modèle, sont donc suivis dans la recette.

Si vous voulez reproduire cette première étape, le parcours commence sur le nœud principal :

git clone https://github.com/Tutanka01/glm5.3-flash-2x-dgx-spark-nvfp4.git
cd glm5.3-flash-2x-dgx-spark-nvfp4

# Révision du dépôt prise comme référence pour cet article.
git checkout de99dccb116a0d5abc80c72e3e33b665d34d5934

cp .env.glm53.example .env.glm53
chmod 600 .env.glm53
nano .env.glm53

Il faut adapter l’accès SSH au worker, les chemins et les interfaces du lien entre les machines. Je vous renvoie au guide réseau du dépôt pour cette partie : les noms d’interfaces dépendent de votre configuration. Les deux nœuds doivent avoir Docker avec Compose v2, un driver compatible CUDA 13 et un accès GPU fonctionnel. Prévoyez au moins 205 GiB libres sur chaque machine, car chacune garde une copie complète du checkpoint sur disque.

Une fois la configuration renseignée, les scripts prennent le relais :

./validate-glm53.sh --config-only
./doctor-glm53.sh
./prepare-glm53.sh
./start-glm53.sh 32k

./smoke-glm53.sh --profile 32k --tools

On commence avec le profil 32k pour vérifier que l’ensemble fonctionne. Le démarrage préparé prend encore de l’ordre de 10 à 15 minutes sur le cluster de référence, en plus du téléchargement et des éventuelles constructions d’images. Une fois le modèle prêt et les tests de chat et d’appel d’outil passés, on peut enfin s’intéresser aux performances.

Voir ce que le modèle donne avant de l’optimiser

J’ai commencé avec un petit benchmark en streaming : une demande de vérification très simple, une fonction Python à écrire et un problème de raisonnement. Chaque demande est répétée trois fois, à température 0, ce qui donne neuf requêtes pour une campagne.

Le script mesure notamment la vitesse de génération et le temps avant le premier token, ou TTFT. Ces deux valeurs correspondent à des moments différents de l’expérience. Après l’envoi d’un prompt, le moteur doit d’abord le traiter : c’est le prefill. Il génère ensuite la réponse : c’est le decode. Un modèle peut être rapide pendant cette seconde phase tout en ayant mis du temps à commencer.

Si vous voulez reprendre ces bases plus tranquillement, elles sont expliquées dans l’introduction à l’inférence. Ici, les mesures sont faites côté client, sur le flux reçu ; le premier élément peut aussi être du raisonnement ou un appel d’outil, selon la réponse du modèle.

Sur le profil 128k-batch4, utilisé avec une seule requête à la fois et sans spéculation, j’obtiens 14,5 tok/s en médiane, avec un TTFT de 0,31 seconde. C’est ma référence pour la suite. Le nom du profil indique sa configuration, mais le benchmark utilise bien des prompts courts : cette mesure ne valide pas une entrée de 128 000 tokens.

La première accélération vient du MTP, pour Multi-Token Prediction. Cette voie exploite les capacités de prédiction de tokens supplémentaires du modèle pour accélérer le décodage. Avec le profil 128k-batch4-mtp, la médiane passe à 28,9 tok/s, pour un TTFT de 0,40 seconde. On obtient donc presque deux fois le débit, sur le même matériel et le même petit jeu de demandes. Ces mesures du 27 août sont dans le journal.

Pour un usage avec une seule requête, le gain est assez net. Mais ce qui m’intéressait avec OpenCode allait au-delà de cette situation : dès que plusieurs demandes partent en parallèle, il faut voir comment elles se partagent le serveur.

Ce qui change quand plusieurs demandes arrivent

J’ai repris le benchmark avec quatre requêtes simultanées. Sans MTP, le serveur atteignait 31,5 tok/s au total. Avec MTP, il montait à 41,8 tok/s, mais le temps avant le premier token augmentait fortement : 7,61 secondes en médiane, et jusqu’à 45,3 secondes sur la requête la plus lente de cette campagne.

C’est un résultat qui change la manière de lire le gain précédent. Le serveur produisait davantage de tokens, mais une partie des demandes attendait longtemps avant de commencer. Pour un outil avec lequel on travaille de manière interactive, cette attente compte beaucoup.

Le journal relie ce comportement à la façon dont le moteur admet les nouveaux prefills pendant le décodage spéculatif. Les réponses en cours progressent, tandis que certaines nouvelles demandes doivent patienter. On touche ici au rôle du scheduler, la partie du moteur qui organise le travail entre les requêtes.

J’ai ensuite testé DFlash2. Cette autre voie de décodage spéculatif utilise un petit modèle auxiliaire d’environ un milliard de paramètres, appelé drafter. Il propose un bloc de tokens, puis le modèle principal vérifie les propositions. Quand plusieurs sont acceptées, la génération avance de plusieurs tokens en amortissant un passage du gros modèle. Inco AI présente le mécanisme ici.

Le principe aide aussi à comprendre pourquoi les performances dépendent du texte demandé. Lorsque le drafter prédit bien la suite, davantage de propositions sont acceptées. Une sortie structurée et un texte libre peuvent donc se comporter très différemment.

Sur mes essais mono-flux, le profil 128k-dflash2 atteint 37,2 tok/s, avec un TTFT médian de 0,38 seconde. Il faut ensuite choisir un profil qui autorise réellement plusieurs requêtes en cours. Le premier profil DFlash2 était limité à une seule : lui envoyer quatre demandes les plaçait simplement en file d’attente.

Avec le profil adapté à la concurrence, la comparaison devient la suivante :

Configuration, à quatre requêtes simultanées Débit agrégé TTFT médian TTFT maximal du test
Sans spéculation — 128k-batch4 31,5 tok/s 0,55 s 0,77 s
MTP — 128k-batch4-mtp 41,8 tok/s 7,61 s 45,3 s
DFlash2 — 128k-dflash2-c4 67,2 tok/s 0,71 s 1,02 s

Mesures des 27–28 août, neuf requêtes par configuration. Le journal appelle la dernière colonne « p99 » ; avec neuf valeurs et la méthode utilisée par le script, elle correspond au maximum observé. Résultats et protocole.

Le débit agrégé représente les tokens produits par l’ensemble des requêtes, divisés par la durée de la campagne. Avec DFlash2, le serveur termine celle-ci en 70,3 secondes, contre 144,9 secondes sans spéculation, tout en conservant un temps d’attente initial assez court sur les demandes testées.

C’est cette combinaison qui rend le résultat intéressant pour mon usage. Le gain porte sur le travail accompli par le serveur, et les nouvelles demandes commencent à recevoir des tokens rapidement. Il reste bien sûr à le vérifier sur des sessions plus longues et des prompts plus variés ; ces neuf requêtes servent surtout à comparer les réglages dans un cadre simple.

Trouver jusqu’où pousser les deux machines

À partir de là, j’ai continué avec le profil 128k-dflash2-c8, qui permet de monter jusqu’à huit requêtes en cours. Le 28 août, le meilleur débit agrégé mesuré arrive à six : 86,0 tok/s, avec un TTFT médian de 0,75 seconde. Le décode médian par requête est alors de 17,6 tok/s.

Ce dernier chiffre permet de se représenter ce qui se passe. Les machines accomplissent plus de travail au total, mais chaque réponse partage les ressources disponibles avec les autres. Les 86 tok/s ne sont donc pas la vitesse à laquelle chaque utilisateur reçoit sa réponse.

Une seconde campagne, le 31 août, donne 80,5 tok/s à six requêtes et 83,1 à sept. À huit, on redescend à 78,2 tok/s. Le sommet exact bouge entre les campagnes ; sur ce jeu de demandes, la zone autour de six ou sept requêtes semble intéressante. Le balayage complet est conservé dans le journal.

Le batching permet notamment de mieux amortir certains accès mémoire et coûts de calcul entre les requêtes. Mais il augmente aussi la quantité de travail et d’état à gérer en parallèle. On finit donc par rencontrer une limite, et ajouter des demandes ne suffit plus à augmenter le débit.

Cette limite se retrouve dès le démarrage si l’on réserve trop de mémoire au moteur. Pendant un essai du profil DFlash2 à huit requêtes, avec une allocation statique réglée à 0,92, le garde mémoire a arrêté le conteneur principal pendant la capture des graphes CUDA. Le profil a ensuite démarré avec 0,90. Les graphes servent à rejouer plus efficacement des séquences d’opérations GPU, mais leur préparation demande elle aussi de la mémoire.

C’est un exemple assez concret de ce qu’il faut conserver autour des poids du modèle : de la marge pour que le moteur fonctionne, y compris pendant ses phases les plus gourmandes.

Et quand la conversation devient longue ?

Jusqu’ici, les demandes du benchmark étaient courtes. Or, avec un outil comme OpenCode, le contexte peut grandir à mesure qu’on ajoute des fichiers, des échanges et des résultats d’outils. Il fallait donc vérifier cette autre dimension séparément.

Le fait qu’un serveur démarre avec une fenêtre configurée à 256k ne prouve pas qu’il saura traiter un prompt proche de cette taille. Dans les essais, un profil avec graphes CUDA réussissait le benchmark court, tandis qu’une tentative longue avec MTP et graphes s’est figée pendant le prefill, avant l’arrêt du scheduler.

Pour mesurer la capacité réelle, j’ai utilisé un texte calibré avec le tokenizer, contenant trois informations à retrouver vers le début, le milieu et la fin. Le prompt est envoyé à froid, puis le test vérifie les trois réponses et s’assure que l’API est encore saine.

Sur la voie SGLang NVFP4, les résultats validés comprennent :

Profil Entrée réellement testée Temps avant le premier token Vérification
256k, sans spéculation 240 008 tokens 204,59 s 3/3 informations retrouvées, API saine
256k-dflash2-eager, avec DFlash2 200 012 tokens 162,56 s 3/3 informations retrouvées, API saine

Tests froids du 28 août. Les entrées ont des tailles différentes ; ces deux lignes ne constituent pas une comparaison de vitesse à charge égale. Journal des essais.

Avec DFlash2, une demande de 220 000 tokens a été refusée à l’admission : le pool disponible sur ce profil était d’environ 210 000 tokens. Le drafter et les allocations nécessaires à son fonctionnement occupent de la mémoire qui doit être prise en compte dans le budget global.

J’ai également exploré le long contexte avec la recette vLLM/EXL3 de MiaAI-Lab, intégrée dans le dépôt. Cette voie a réussi un test froid de 990 007 tokens, avec les trois informations retrouvées et l’API encore fonctionnelle. Le temps avant le premier token était cependant de 1 231,9 secondes, soit environ 20 minutes et 32 secondes, serveur déjà démarré. Le protocole et cette mesure du 29 août sont documentés ici.

Cela ouvre des possibilités, mais donne aussi une idée de leur coût en temps. Et retrouver trois informations dans un gros texte ne démontre pas que le modèle saura raisonner correctement sur tout un projet de cette taille. Pour mon usage, il faut donc regarder la quantité de contexte utile, le temps nécessaire pour la traiter et le comportement sur les tâches réellement demandées.

Là où j’en suis aujourd’hui

Ces essais m’ont permis de mieux comprendre ce que je pouvais attendre des deux PGX avec GLM-5.3-Flash. J’ai une recette pour préparer le modèle, revenir à des configurations connues et comparer leur comportement. Surtout, je peux choisir un réglage en fonction de l’usage : une demande à la fois, plusieurs requêtes en parallèle, ou un gros contexte à traiter.

Le dépôt a pris cette forme au fil du travail. Les scripts de lancement évitent de refaire les mêmes opérations à la main, les vérifications aident à repérer une configuration incohérente, et les benchmarks permettent de garder une trace de ce qui a changé. C’est cette partie que j’ai essayé de rendre réutilisable pour quelqu’un qui dispose de deux machines similaires.

Pour reprendre les essais DFlash2 après avoir validé le profil de base, les commandes sont les suivantes :

./stop-glm53.sh --profile 32k
./prepare-dflash2.sh 128k-dflash2-c8
./start-glm53.sh 128k-dflash2-c8

./bench-glm53.py --warmup-runs 1 --runs 3 --concurrency 1
./bench-glm53.py --warmup-runs 1 --runs 3 --concurrency 4
./bench-glm53.py --warmup-runs 1 --runs 3 --concurrency 6

Le warmup est exclu des statistiques et les résultats sont enregistrés en JSON. Les valeurs citées dans cet article proviennent des campagnes archivées du 27 au 31 août ; les commandes permettent de faire vos propres mesures avec la révision indiquée.

Il reste du travail avant de considérer le profil DFlash2 comme validé pour une utilisation prolongée. Il est toujours marqué expérimental, notamment pour la marge mémoire, les gros prefills concurrents et la stabilité sur la durée. La comparaison des sorties doit aussi être élargie : les trois sorties de code comparées étaient identiques à la référence sans spéculation, mais les deux autres catégories différaient entre les images. La cause de ces écarts reste à établir. Le journal conserve ces limites avec les résultats.

J’ai commencé d’autres mesures, notamment sur la consommation électrique, qui mériteront d’être traitées séparément. Pour l’instant, ce qui m’intéresse est de continuer à confronter ces configurations à l’usage quotidien avec les agents : des sessions longues, des appels d’outils, des demandes qui arrivent à des moments différents. C’est dans ces conditions que les compromis vus ici prendront tout leur sens.

Enfin, ce travail s’appuie sur les contributions de Z.ai, LibertAIDAI, 0xSero, Inco AI, MiaAI-Lab et de l’écosystème ExLlamaV3. Ma contribution concerne l’intégration sur ces deux PGX, les garde-fous, l’instrumentation et les retours issus des essais. Les crédits détaillés sont dans le dépôt.

Si vous avez des DGX Spark ou des PGX et que vous faites des essais similaires, vos retours m’intéressent. Le profil, les prompts et le temps avant le premier token sont aussi utiles que le débit pour comparer nos résultats. Vous trouverez tout ce qu’il faut pour commencer dans le dépôt GLM-5.3-Flash sur deux DGX Spark.


Deux précisions pour reproduire : le code de la recette est sous MIT, tandis que le drafter DFlash2 est publié sous CC BY-NC-ND 4.0 pour la recherche et l’évaluation, avec une licence commerciale proposée séparément par Inco AI. Le serveur écoute par défaut sur toutes les interfaces : configurez une clé API et le filtrage réseau avant de l’exposer hors du réseau de confiance. Si une clé est définie, exportez la même valeur dans API_KEY pour les benchmarks Python.