Deux machines de classe DGX Spark, un seul modèle : une copie sur chacune, au lieu de le diviser
Opérations
Pendant sept semaines, notre modèle de langage local tournait réparti sur deux machines NVIDIA GB10 avec parallélisme tensoriel. Le 22 octobre, nous avons remplacé cette configuration par une copie complète du modèle sur chaque machine, avec un proxy à moindre connexion devant. Le débit est passé d'environ 180 à 210 jetons par seconde, et l'une ou l'autre machine peut désormais redémarrer sans interrompre le service du modèle.
Pourquoi nous l'avions divisé
Chaque machine dispose de 128 Go de mémoire partagée entre le CPU et le GPU. En septembre, le modèle tenait sur une machine, mais son cache y tenait à peine : la version vLLM que nous utilisions ne pouvait pas conserver le cache KV de ce modèle en FP8, le cache était donc en BF16, et une machine offrait de la place pour 499 110 jetons de cache. Cela représente moins de deux requêtes au contexte complet du modèle de 262 144 jetons. Réparti sur les deux machines avec parallélisme tensoriel via leur liaison réseau directe, le cache atteignait plus de trois millions de jetons. Nous avions divisé le modèle pour gagner de la place, pas pour la vitesse.
Ce qui a changé
Une version 4 bits plus récente du même modèle (NVFP4, obtenue par distillation consciente de la quantification) occupe environ 71 GiB de mémoire, et une version communautaire de vLLM pour GB10 (eugr/spark-vllm-b12x) exécute son cache en FP8. Désormais, une machine héberge l'ensemble du modèle et un cache de 0,76 à 1,08 million de jetons, soit de la place pour 2,9 à 4,1 requêtes au contexte complet, selon la mémoire que la machine lui alloue. Les deux copies utilisent le décodage spéculatif propre au modèle (MTP, quatre jetons de brouillon). Avant la bascule, nous avons lancé notre test d'appels d'outils sur la nouvelle version : elle a obtenu un score de 86, contre 75 pour la version qu'elle remplace.
Ce que nous avons mesuré
| Réparti sur deux machines | Une copie par machine | |
|---|---|---|
| Cache KV | plus de 3 millions de jetons | 0,76 et 1,08 million de jetons |
| Requêtes au contexte complet | environ 12 | 2,9 et 4,1 |
| Débit sous notre charge | environ 180 jetons/s | 210 jetons/s (111 + 99) |
| Démarrage | environ 20 minutes | 5 à 13 minutes par copie |
| Redémarrage d'une machine | le modèle est indisponible | l'autre copie sert les requêtes |
Une copie seule, selon le nombre de requêtes simultanées : avec des prompts courts, 1 requête donnait 36 tokens par seconde, 4 en donnaient 82, 8 en donnaient 109 et 16 en donnaient 150. Avec les prompts plus longs de notre agent e-mail, 4 en donnaient 81, 8 en donnaient 113 et 16 seulement 87 : au-delà de huit requêtes de ce type, en ajouter ralentissait le système.
Pendant un après-midi, une copie a traité au maximum 12 requêtes simultanées et l'autre 8. La première produisait 91 à 93 tokens par seconde et terminait 26 à 27 requêtes par minute ; la seconde 81 à 83 tokens par seconde et 22 requêtes par minute. Les deux prennent désormais 12 requêtes.
Ce que nous avons sacrifié
L'espace. Le modèle scindé disposait d'un cache de plus de trois millions de tokens ; les copies en ont deux plus petits, et une requête ne peut utiliser que le cache de la copie qui la sert. Nos agents envoient de nombreuses requêtes courtes et moyennes, et pour elles, le débit comptait davantage. Si vous servez quelques contextes très longs à la fois, la scission peut rester le meilleur choix. Nous avons conservé la configuration scindée, et le retour en arrière se fait par un simple script.
Ce qui a mal tourné lors de la bascule
- Onze minutes sans modèle, puis 30 secondes d'erreurs 502. Le modèle scindé devait s'arrêter avant qu'une copie puisse se charger sur les mêmes machines, si bien que nos agents ont attendu environ onze minutes. Quand la première copie s'est levée, le proxy a continué à répondre « 502 no live upstreams » pendant environ 30 secondes de plus : il avait marqué les backends comme défaillants pendant leur arrêt (max_fails=3, fail_timeout=30s) et ne les a pas réessayés avant la fin de ce délai.
- Un client qui contournait le proxy. L'un de nos traitements par lots avait l'adresse de la première machine dans sa configuration, si bien que toute sa charge allait vers une seule copie tandis que l'autre avait de la capacité disponible. Tous les clients passent désormais par le proxy ; les adresses propres aux machines ne servent plus qu'aux vérifications de santé.
- Des métriques qui sautaient. Notre moniteur de débit lisait les /metrics de vLLM via le proxy. Avec deux backends, les lectures consécutives provenaient de copies différentes, les compteurs faisaient des allers-retours et le moniteur signalait le modèle comme affamé. Il lit désormais chaque copie directement et additionne les résultats.
- Un ancien chien de garde qui aurait arrêté la nouvelle copie. La configuration divisée disposait d'un chien de garde qui redémarrait son service lorsque le modèle cessait de répondre. Le nouveau service déclare Conflicts= avec l'ancien, de sorte que ce redémarrage aurait arrêté la nouvelle copie. Il a fallu le désactiver avec
systemctl disable --now, et pas seulement l'arrêter, car notre couche d'opérations redémarre les unités activées mais non exécutées. - Mémoire sur la machine qui effectue d'autres tâches. Une machine exécute également nos bases de données et nos builds de sites. Avec gpu-memory-utilization de vLLM à 0,78, il ne restait que 5 GiB pour tout le reste, nous avons donc abaissé cette valeur à 0,74 et redémarré uniquement cette copie pendant que l'autre assurait la charge. Sur ces machines, la part de mémoire que vous attribuez à vLLM est prise sur la même mémoire que tout le reste.
Si vous disposez de deux de ces machines
- Si le modèle et le cache dont vous avez besoin tiennent sur une seule machine, exécutez une copie sur chacune derrière un proxy à moindre nombre de connexions. Ne divisez le modèle que lorsqu'une seule machine ne peut pas contenir le cache dont vous avez besoin.
- Dirigez tous les clients vers le proxy, y compris les tâches par lots et les anciennes configurations.
- Lisez les métriques directement depuis chaque copie.
- Attribuez à chaque machine sa propre part de mémoire, selon ce qui s'exécute par ailleurs sur celle-ci.
- Définissez le nombre de requêtes qu'une copie sert simultanément en fonction de votre propre trafic : pour notre mix, 12 était meilleur que 8.
- Prévoyez que la bascule retire le modèle pendant la durée de démarrage d'une copie, et planifiez en conséquence.
Factory Floor fournit le proxy et les configurations de lancement pour une ou deux machines, le contrôle de santé et le test d'utilisation d'outils que nous exécutons avant de changer de modèle, avec les chiens de garde et les garde-fous mémoire pour le reste de la machine.