Depuis quelque temps, j’ai deux Lenovo ThinkStation PGX au bureau. Deux boîtiers, chacun avec une GB10, reliés à 200 Gb/s.

La question n’était pas “qu’est-ce que je peux faire tourner dessus” ça, je le savais déjà, tout le monde sait lancer un ollama run et regarder les tokens défiler. La vraie question, celle qui alimente la série que je prépare sur l’IA locale et souveraine (deux mots très à la mode), c’était :

Est-ce que ce matériel peut réellement remplacer un abonnement cloud, ou est-ce que je vais me retrouver avec un gadget de bureau très cher qui fait illusion pendant cinq minutes ?

Pour commencer à y répondre, j’ai choisi DeepSeek-V4-Flash-0731. Le modèle venait de sortir : 284 milliards de paramètres, mais une architecture MoE qui n’en active qu’environ 13 milliards par token, un contexte pouvant grimper jusqu’à 1M de tokens, et une version NVFP4 pensée pour le matériel que j’avais sous la main. Sur le papier, exactement ce qu’il fallait pour un cluster de deux GB10.

Le choix que j’ai failli regretter

Il y a plusieurs façons de déployer ce modèle sur ce genre de setup, mais j’avais retenu deux dépôts. Le premier proposait une approche confortable : image préconstruite, détection automatique du réseau, peu de manipulation. Le second, plus austère : image à construire soi-même, GID réseau à vérifier à la main, aucune détection automatique pour te rattraper si tu te trompes.

J’ai pris le second. Pas par purisme, mais parce qu’il embarquait un patch absent du premier je ne vais pas entrer dans le détail, mais il corrigeait un bug qui réduisait très fortement les performances en décodage. L’option la moins confortable, mais la plus intéressante.

L’obstacle que je n’avais pas vu venir

L’installation en général et la doc se sont très bien passées : étapes claires, faciles à suivre, et la construction de l’image a réussi du premier coup sur le nœud principal. Sur le worker, en revanche, échec systématique, toujours au même endroit : impossible de joindre le serveur d’authentification de GHCR.

Le réflexe, c’est de vérifier le proxy. Je suis à l’université et je n’ai pas encore accès au réseau recherche, donc je passe comme tout le monde derrière un réseau étudiant avec proxy. J’ai lancé docker run --rm ubuntu:24.04 env | grep -i proxy sur les deux machines : les variables étaient bien là, dans le conteneur. Le proxy fonctionnait. Et pourtant, le build échouait toujours au même endroit.

Le problème venait d’une confusion simple, la variable d’environnement que voit un conteneur lancé avec docker run n’a rien à voir avec celle que voit le daemon BuildKit qui orchestre la construction de l’image. Le premier tourne dans le conteneur applicatif, le second comme service systemd, avec son propre environnement, invisible depuis un simple docker run. J’avais vérifié le mauvais proxy pendant tout ce temps.

À ce stade, deux options : reconfigurer le proxy du daemon Docker au niveau systemd sur le worker, ou arrêter de me battre contre GHCR sur cette machine. J’ai choisi la seconde. L’image finale existait déjà, validée, sur le head. Il suffisait de la transférer telle quelle :

docker save vllm-dspark-runtime:dspark-nvfp4-stage-c \
  | pv \
  | ssh -o Compression=no [email protected] 'docker load'

Une commande, aucune dépendance à GHCR côté worker, et un bonus auquel je ne m’attendais pas : les deux nœuds tournent désormais avec une image strictement identique docker image inspect renvoie le même hash des deux côtés. Plus de dérive possible entre les deux rangs du cluster.

Ce que ça donne en vrai

Premier lancement propre, tmux, le script qui démarre le worker puis le head, puis plusieurs séries de benchmarks. Je vous laisse d’abord les résultats bruts, pour que vous puissiez juger par vous-mêmes :

=== pgx-2x-0731 @ http://127.0.0.1:8888/v1 ===

--- [pgx-2x-0731] DECODE by content (temp 0, warm) ---
prompt       tok    sec    tok/s
count300     600   6.99     85.9
mult12       900  11.17     80.6
json60       800  10.26     77.9
bst          600   9.01     66.6
story        267   8.43     31.7
  PEAK 85.9 (count300)   MEAN 68.5

--- [pgx-2x-0731] CONCURRENCY (same prompt, 400 tok each) ---
 conc   ok   agg tok/s  per-stream    wall
    1    1        61.3        61.3    6.52
    2    2        50.6        25.6   15.80
    4    4        50.7        12.8   31.58
    6    6       138.1        23.3   17.38

--- [pgx-2x-0731] PREFILL (TTFT method, 1 output token) ---
  target  prompt tok     sec     tok/s
    8000        6234     3.6      1712
   32000       24900    10.4      2391
  100000       77790    32.3      2408

===== SUMMARY [pgx-2x-0731] =====
decode peak 85.9 | mean 68.5
concurrency agg: c1=61  c2=51  c4=51  c6=138
prefill: 8K=1712t/s  32K=2391t/s  100K=2408t/s

68,5 tok/s de moyenne, mais pas dans toutes les situations. Le pic à 85,9 tok/s est le chiffre qui saute aux yeux, mais il masque une vraie variance selon le type de génération, sur du comptage ou du JSON, contraint, je dépasse facilement les 75 tok/s ; sur la génération d’une histoire, plus libre, je tombe à 31,7 tok/s. Le 68,5 est utile pour comparer des configurations entre elles, mais ce n’est pas une vitesse que je retrouverai dans tous les usages.

Ceci est a cause notamment que par defaut le repo, lance le modele avec une configuration MTP, en bref MTP permet de predire des tokens en parallele par un tout petit modele, et les faire valider en bulk par le gros modele. Avec des choses prvisibles comme du code ça va ultra vite, car le gros modele peut valider plusiers tokens d’un coup (moins de passage du modele par generation de token). Mais sur des choses moins previsible comme une histoire, le gros modele n’est pas d’accord avec les tokesn proposés donc on se retrouve avec un seul passage du modele par token, ce qui est beaucoup plus lent.

Le préfill ne s’effondre pas avec les longs prompts. Je passe d’environ 1712 tok/s sur un prompt de 6 234 tokens à plus de 2400 tok/s sur près de 78 000 tokens, le débit augmente avec la taille du prompt, pas l’inverse. Ça peut s’expliquer par une meilleure saturation du matériel et l’amortissement des coûts fixes sur les prompts longs. Un seul benchmark ne suffit pas à prouver que le lien RDMA à 200 Gb/s ne sera jamais le facteur limitant, mais jusqu’à 100 000 tokens de contexte, je n’observe aucun effondrement brutal plutôt rassurant pour une machine pensée pour les gros contextes.

Le batching devient intéressant à partir de six requêtes. À deux ou quatre requêtes simultanées, l’agrégat plafonne autour de 50 tok/s et chaque flux ralentit fortement. À six, ça change : l’agrégat grimpe à 138,1 tok/s, avec environ 23,3 tok/s par flux. Le scaling n’est pas linéaire il semble exister un seuil à partir duquel le scheduler arrive à mieux remplir les GPU. Un seul benchmark ne me permet pas de tirer une conclusion définitive, mais ça montre déjà que cette machine ne se juge pas seulement sur une requête isolée : elle a aussi de l’intérêt à plusieurs utilisateurs.

Pour ce que j’en fais au quotidien ? Depuis que j’ai installé ce modèle, je l’utilise avec Opencode, et il marche vraiment très bien : aucun problème de stabilité, aucune erreur bête. C’est la première fois depuis un moment que j’arrive à travailler pendant des heures sans surveiller un quota ou attendre la réinitialisation d’une limite de 5 heures et je peux vous dire que oh, ça fait vraiment du bien.

Le vrai problème : plus de 15 minutes de cold start

Le premier démarrage est long. Chargement des poids, initialisation des processus distribués, compilation, autotuning, capture des graphes CUDA, et tout le tralala : plus de 15 minutes avant le premier token. Pour une machine qui reste allumée et sert des requêtes toute la journée, ce n’est pas dramatique. Pour un usage occasionnel, ou une infra qui doit démarrer à la demande, c’est nettement plus embêtant.

C’est exactement le sujet sur lequel je travaille maintenant, le GPU checkpointing sur GB10. Un sujet qui m’intéresse depuis un moment, mais qui devient beaucoup plus concret avec 15 minutes de cold start devant moi. L’idée : capturer l’état d’un processus CUDA déjà chaud poids en VRAM, graphes capturés, KV cache prêt et le restaurer en quelques secondes via une version adaptée de CRIU à la mémoire unifiée du GB10, plutôt que de tout recharger à froid à chaque fois. Si ça aboutit, la ligne “cold start” de cet article devient obsolète. Je vous en reparle bientôt.

Et maintenant ?

Cet article de blog pose le premier chiffre de la série que je prépare sur l’IA locale et souveraine. La suite : consommation électrique réelle, coût par million de tokens, performances à plusieurs utilisateurs, intérêt des très longs contextes, stabilité sur plusieurs jours et la question qui fâche, rentabilité face au cloud ou FOMO matériel.

Le dépôt complet que j’ai utilisé, patches inclus, est ici : tonyd2wild/DeepSeek-v4-Flash-0731-DSpark-1M-NVFP4-KV-2x-DGX-Spark.

Vous avez tenté quelque chose de similaire sur du GB10 ou équivalent ? 68,5 tok/s en moyenne sur deux boîtiers de bureau, c’est un chiffre à saluer ou à relativiser ?