Gemma-4 sur la DGX Spark : NVFP4 vs BF16
Neuf benchmarks identiques, deux précisions. NVFP4 est 22 à 92 pour cent plus rapide par token, et la capacité grimpe de 69 pour cent aux heures de pointe.
Dans la baseline BF16 de Gemma-4 sur la DGX Spark, j’ai fait neuf benchmarks avec Gemma-4-26B-A4B en BF16. La vitesse de décodage tenait très bien, le prefill décidait du moment où le mur arrivait, et le système faisait sagement la queue sous pression au lieu de planter. Cette histoire semblait bouclée, jusqu’à ce que NVIDIA sorte une version NVFP4-quantized de ce même modèle.
Même architecture et même fine-tune, même config serveur, seule la précision change. De BF16 (16 bits par paramètre) à NVFP4 (4 bits par paramètre, la variante de NVIDIA sur FP4). Quatre fois plus petit par poids, et si les kernels Blackwell jouent le jeu, nettement plus rapide aussi sur les tâches compute-heavy.
Sur le papier, c’est beau. En pratique : la release officielle vLLM v0.20.1 reconnaît ce checkpoint sans broncher, et les chiffres étaient plus rapides sur toute la ligne que la baseline BF16. Les deux tests tombent sous le guide faire tourner des LLMs sur la DGX Spark.
Pourquoi se pencher là-dessus
Pour un bureau avec une machine IA locale, le budget mémoire est la chose la plus limitante après la puissance de calcul. Un modèle 26B en BF16 prend ~48 Go de mémoire GPU rien que pour les weights. Sur une Spark avec 128 Go de unified memory, il reste environ 65 Go pour le KV-cache. Suffisant pour le scénario de bureau du premier blog, mais pas beaucoup de marge pour faire tourner, disons, 30+ utilisateurs avec un gros contexte côte à côte.
NVFP4 réduit ça à ~18 Go pour les weights. Pas quatre fois moins que BF16 (le vision-encoder reste en BF16, et les scale-factors coûtent aussi de la place), mais environ 2,7× moins. Ça te donne vers 95 Go de KV-cache headroom, ce qui en théorie devrait supporter une concurrency bien plus élevée. À ça s’ajoute qu’il faut moins de trafic mémoire par forward pass, donc par définition moins de pression sur la bandwidth, et c’était déjà le bottleneck en BF16 sous charge multi-utilisateurs. La question était donc simple : combien de ce gain théorique survit en pratique ?
Ce qu’est vraiment NVFP4
NVFP4 est la variante de NVIDIA sur FP4 : des nombres en virgule flottante avec 4 bits par valeur. Quatre bits, pas quatre octets, donc un facteur 4 de moins par paramètre que BF16. En stockant un scaling factor par groupe de weights, l’accuracy reste raisonnablement préservée.
Pour Blackwell, ça marche comme ça. Les cartes datacenter de NVIDIA (B100, B200, SM10.0) ont des tensor cores qui peuvent calculer nativement avec des valeurs 4-bit, et c’est bien plus rapide que le même calcul en FP16 ou BF16. La DGX Spark, en revanche, c’est du Blackwell desktop (GB10, SM12.1) et cette architecture n’a pas de compute FP4 natif. Sur un B200 datacenter (SM10.0), tu attendrais encore 2 à 3× par-dessus grâce aux tensor cores FP4 natifs. La Spark n’a pas ce chemin matériel, donc tout le gain vient de la bandwidth mémoire, pas du compute. Ce que tu obtiens dans ce cas, c’est du FP4 “weight-only” : les weights sont physiquement stockés en 4-bit (d’où le gain mémoire), mais pendant le compute ils sont décodés à la volée vers FP16 pour les matrix-multiplications. Un warning vLLM le dit explicitement :
Your GPU does not have native support for FP4 computation but FP4 quantization
is being used. Weight-only FP4 compression will be used leveraging the Marlin kernel.
This may degrade performance for compute-heavy workloads.
Tu obtiens donc le gain mémoire en entier, le gain compute seulement en partie. Le kernel Marlin INT4 GEMM est optimisé, mais pas aussi rapide que le FP4 natif sur SM10.0 le serait. Bon à intégrer dans le calcul quand tu regardes les chiffres plus bas.
Le montage de test
Config serveur identique au premier blog, seul le modèle change :
docker run -d --name vllm-bench \
--gpus all --ipc=host \
-v appliance_hf-cache:/root/.cache/huggingface \
-p 8000:8000 \
vllm/vllm-openai:v0.20.1 \
--model nvidia/Gemma-4-26B-A4B-NVFP4 \
--served-model-name gemma-4-26b-a4b-nvfp4 \
--max-model-len 131072 \
--gpu-memory-utilization 0.95 \
--kv-cache-dtype fp8 \
--limit-mm-per-prompt '{"image":0,"audio":0}' \
--async-scheduling \
--no-enable-prefix-caching \
--host 0.0.0.0 \
--port 8000
Les tests sont un à un identiques au premier blog : mêmes commandes, mêmes niveaux de concurrency, mêmes datasets pour les tests open-loop, même seed. C’est volontaire, car si tu veux mesurer l’effet d’une variable isolée (ici la précision), tout le reste autour doit rester pareil. La façon exacte dont je mesure ces niveaux de concurrency, ces seeds et ces arrivées open-loop est décrite dans la méthode de mesure de l’Arena.
| Comparaison | BF16 | NVFP4 |
|---|---|---|
| Modèle | google/gemma-4-26B-A4B-it | nvidia/Gemma-4-26B-A4B-NVFP4 |
| Active params | 4B | 4B |
| Total params | 26B | 26B |
| Model memory | ~48 Go | ~18 Go |
| KV-cache headroom | ~65 Go | ~95 Go |
| MoE backend | (default) | MARLIN (forcé) |
Trois chiffres résument où ça aboutit. Clique pour la run complète dans l’Arena, avec tous les seeds, niveaux de concurrency et commandes :
Une version interactive de tous les chiffres se trouve sur la page Arena de Gemma-4-26B-A4B-NVFP4, commandes et percentiles TTFT inclus pour les 9 tests.
Run A : scaling du contexte de 4k à 25k
Décodage par utilisateur quand le contexte grandit, c=1/5/10 :
| Context | Users | BF16 d/u | NVFP4 d/u | Gain |
|---|---|---|---|---|
| 4k | 1 | 24.08 | 29.80 | +24% |
| 4k | 5 | 12.55 | 22.01 | +75% |
| 4k | 10 | 9.48 | 16.94 | +79% |
| 8k | 1 | 23.69 | 29.31 | +24% |
| 8k | 5 | 11.48 | 19.28 | +68% |
| 8k | 10 | 8.52 | 14.35 | +68% |
| 16k | 1 | 23.34 | 28.55 | +22% |
| 16k | 5 | 10.05 | 15.67 | +56% |
| 16k | 10 | 6.79 | 10.06 | +48% |
| 25k | 1 | 22.75 | 27.70 | +22% |
| 25k | 5 | 8.46 | 12.46 | +47% |
| 25k | 10 | 5.40 | 7.55 | +40% |
À c=1, le gain est stable autour de +22-24% à travers tous les contextes. La bandwidth mémoire ne joue presque pas en single-user, donc le gain ici se trouve dans le compute-path lui-même. Le décodage INT4 de Marlin plus le matmul FP16 est un peu plus rapide que le matmul FP16 direct de BF16, malgré les deux étapes.
À c=10, l’écart scale beaucoup plus fort selon le type de workload, de +40% à 25k de contexte à +79% à 4k. C’est parce qu’en multi-utilisateurs la bandwidth mémoire devient le bottleneck, et NVFP4 lit moins d’octets par forward pass. Plus c’est concurrent, plus ça compte, jusqu’à ce que tu retombes sur les KV-cache memory limits (25k de contexte avec plusieurs utilisateurs) et que le gain s’aplatisse.
Le TTFT (premier token) est meilleur aussi :
| Context | Users | BF16 TTFT | NVFP4 TTFT |
|---|---|---|---|
| 4k | 10 | 4.46s | 4.20s |
| 8k | 10 | 7.99s | 7.84s |
| 16k | 10 | 18.92s | 18.69s |
| 25k | 10 | 35.67s | 35.65s |
Sur le TTFT, le gain est faible. C’est logique : le prefill est compute-heavy, et sur SM12.1 sans tensor cores FP4 natifs, Marlin doit décoder les weights à la volée pour le matmul. Ça reprend une partie de ce que la bandwidth mémoire avait rapporté. Pour le décodage, la bandwidth compte plus que le compute ; pour le prefill, c’est l’inverse.
Run B : 25k de contexte, concurrency jusqu’à 20
Le stress-test de la première partie :
| Users | BF16 d/u | NVFP4 d/u | BF16 TTFT | NVFP4 TTFT |
|---|---|---|---|---|
| 5 | 8.51 t/s | 12.43 t/s | 19.86s | 19.72s |
| 10 | 5.37 t/s | 7.56 t/s | 35.44s | 35.51s |
| 20 | 3.16 t/s | 4.26 t/s | 67.37s | 67.40s |
Le plateau de décodage agrégé passe de 32 t/s à 36 t/s à c=20 : un plafond 12% plus haut à 25k de contexte sous pression maximale. Le TTFT est pratiquement identique entre BF16 et NVFP4 parce que le prefill est le mur ici et qu’il n’accélère pas beaucoup sur SM12.1. Le décodage par utilisateur est par contre nettement meilleur : avec vingt prompts 25k en parallèle, tu obtiens 4.26 au lieu de 3.16 t/s, +35%. Toujours pas de la vitesse de chat, mais une différence perceptible dès que les tokens commencent à couler.
Run C : prompt 1k, output 1k
La workload prompt-court + réponse-longue, proche des agent-flows et de la génération de code :
| Users | BF16 d/u | NVFP4 d/u | Gain |
|---|---|---|---|
| 1 | 23.86 | 29.45 | +23% |
| 5 | 13.59 | 24.69 | +82% |
| 10 | 10.92 | 20.88 | +91% |
À c=10, le décodage par utilisateur est bien au-dessus de 20 t/s, au-dessus de la vitesse de lecture et proche d’une UI de streaming confortable. Le décodage agrégé à c=10 atteint 209 t/s au lieu de 86 t/s en BF16, presque le double.
Run E : multi-turn (depth 4)
Cinq tours consécutifs par conversation, dix conversations en parallèle : la shape de bureau la plus réaliste.
| Users | BF16 d/u | NVFP4 d/u | BF16 TTFT | NVFP4 TTFT |
|---|---|---|---|---|
| 1 | 23.97 | 29.61 | 0.53s | 0.33s |
| 5 | 13.07 | 23.98 | 1.32s | 1.11s |
| 10 | 10.43 | 19.51 | 2.13s | 1.94s |
Pour dix conversations 5-turn en parallèle : 1.94 seconde jusqu’au premier token, 19.51 t/s par utilisateur. Ça rentre confortablement dans ce qu’un lecteur ressent comme du chat, et c’est 87% plus rapide par token que BF16 dans le même test.
Run F : mix RAG (prompt 8k)
| Users | BF16 d/u | NVFP4 d/u | BF16 TTFT | NVFP4 TTFT |
|---|---|---|---|---|
| 5 | 12.11 | 20.91 | 4.32s | 4.28s |
| 10 | 9.31 | 15.96 | 7.99s | 8.00s |
| 20 | 6.05 | 10.57 | 14.61s | 14.45s |
8k de contexte, c’est à peu près ce qu’un RAG-flow avec quatre chunks de 2k tokens reçoit. À dix utilisateurs, tu attends 8 secondes jusqu’au premier token (quasi pareil que BF16, car bottleneck compute), puis 16 t/s en streaming. Pour les flows “pose une question sur tes documents”, c’est largement exploitable, et là où le gain se trouve : dans la vitesse de décodage, pas dans le TTFT.
Run G : instruction courte, 4096 tokens d’output
La shape agent / génération de code :
| Users | BF16 d/u | NVFP4 d/u | BF16 TTFT | NVFP4 TTFT |
|---|---|---|---|---|
| 1 | 24.17 | 29.59 | 0.24s | 0.11s |
| 5 | 14.32 | 25.79 | 0.38s | 0.23s |
| 10 | 11.75 | 22.54 | 0.48s | 0.37s |
Un TTFT de 110 millisecondes en single-user, c’est très bas, plus bas que ce que la plupart des hosted APIs atteignent à travers le réseau. Et 22.54 t/s par utilisateur à c=10, c’est largement assez pour des agent-streams. Le décodage agrégé à c=10 dans ce test sort à 225 t/s contre 84 t/s en BF16, presque 2,7× autant. Pour une équipe qui fait tourner dix agents simultanés produisant chacun de longues sorties structurées, c’est le chiffre le plus important.
Run H : open-loop, workload 4k aléatoire
La baseline de bureau synthétique avec des arrivées Poisson :
| Metric | BF16 | NVFP4 |
|---|---|---|
| Achieved RPS | 0.27 | 0.29 |
| Peak concurrent | 36 | 16 |
| TTFT P50 | 1286 ms | 1006 ms |
| TTFT P99 | 3316 ms | 2893 ms |
| TPOT P50 | 182 ms | 64 ms |
| Total tok/s | 1215 | 1302 |
Ce qui frappe, c’est que le peak concurrent tombe de 36 à 16 à arrival rate identique (0.3 rps) et prompts identiques. Comme NVFP4 traite chaque requête plus vite, la queue reste plus courte, et c’est un point important pour la planification de capacité : NVFP4 te donne non seulement une latency plus basse par requête, mais aussi moins de pression de queue au même arrival rate. En parallèle, le TPOT P50 tombe de 182ms à 64ms. La latency médiane inter-token est donc presque trois fois plus rapide. Pour une UI de chat qui montre le token-streaming, c’est la différence entre attendre artificiellement une réponse et simplement lire au fil de l’eau.
Run I : replay ShareGPT (vraies conversations)
De vraies données de conversation multi-turn :
| Metric | BF16 | NVFP4 |
|---|---|---|
| Peak concurrent | 17 | 10 |
| TTFT P50 | 353 ms | 152 ms |
| TTFT P99 | 637 ms | 265 ms |
| TPOT P50 | 95 ms | 39 ms |
Un P99 TTFT de 265 millisecondes, pour 99 pour cent des utilisateurs. Un TPOT de 39 ms revient à 25.6 t/s par utilisateur. Tu peux tranquillement appeler ça du chat en temps réel pour 25 collaborateurs avec des prompts réalistes de style ShareGPT.
Run J : pic du lundi matin
Le scénario le plus lourd de la première partie : serveur surchargé, target de 1.5 rps avec max 25 requêtes simultanées.
| Metric | BF16 | NVFP4 |
|---|---|---|
| Configured RPS | 1.50 | 1.50 |
| Achieved RPS | 0.26 | 0.44 |
| TTFT P50 | 1132 ms | 920 ms |
| TTFT P99 | 6157 ms | 6054 ms |
| TPOT P50 | 187 ms | 108 ms |
| Total tok/s | 1173 | 1984 |
Le chiffre le plus mesurable de toute la journée, c’est que l’achieved RPS passe de 0.26 à 0.44. Même target, même cap de concurrency, mêmes arrivées Poisson, et NVFP4 traite 69% de requêtes en plus par seconde avant que la queue ne se bouche.
Le P99 TTFT ne bouge que marginalement (6.16s à 6.05s). Ça colle au schéma : le prefill est compute-bound sur SM12.1, et NVFP4 n’y est pas beaucoup plus rapide. Mais le TPOT P50 tombe de 187ms à 108ms, et le throughput agrégé de tokens grimpe de 1173 à 1984 t/s. Pour un bureau de 25 personnes aux heures de pointe, c’est la différence entre du confortable et du serré : plus de requêtes par seconde traitées, avec un streaming plus rapide pour qui est servi.
Ce que ça veut dire pour l’IA on-prem
Si tu as une Spark et que tu fais tourner Gemma-4-26B, NVFP4 est l’upgrade. Sur les 9 tests, NVFP4 est le gagnant, et il libère 30 Go de mémoire pour d’autres usages comme plus de KV-cache, un deuxième petit modèle à côté, ou des batch-jobs. Chez Kamoo, cette config NVFP4 est maintenant à côté de la baseline BF16 dans bench-spark/, et une seule commande bascule entre les deux.
Pour un bureau de 25 personnes avec des prompts réalistes de style ShareGPT, tu le remarques tout de suite. Le TPOT P50 tombe de 95 ms à 39 ms, le P99 TTFT de 637 ms à 265 ms. Et quand un pic de charge arrive, le système délivre 69% de requêtes en plus par seconde avant de se remplir. Pour les agent-flows et la génération de code (shape Run G), la Spark en NVFP4 est à son meilleur : dix agents en parallèle, chacun 4096 tokens d’output, 22.5 t/s par utilisateur avec un TTFT sous 400 ms.
Pour le stress à 25k de contexte (Run B), ça reste le mur. NVFP4 ne le baisse presque pas (le TTFT diffère de moins d’une seconde), parce que le prefill reste le prefill, et dix prompts 25k en parallèle attendent 35 secondes le premier token. La quantization n’y change rien sur ce matériel. La vitesse de décodage, par contre, si : 7.56 t/s/utilisateur au lieu de 5.37, donc dès que les tokens arrivent, ils défilent plus vite.
Ce que cette run ne dit pas
Ce n’est pas du NVFP4 sur SM10.0 (Blackwell datacenter). Là, le compute FP4 natif rendrait la différence bien plus grande, avec l’attente d’un speedup supplémentaire de 2-3× par-dessus ce qu’on voit ici. Sur un H100 ou un B200, ces chiffres ne sont donc pas représentatifs ; la Spark a un handicap spécifique SM12.1 (pas de FP4 natif) qui n’existe pas dans le cloud.
Ce n’est pas non plus une comparaison avec Gemma-4-31B dense en NVFP4. Le dense passe par un autre code-path dans le loader de vLLM. Pour un blog de suite, le dense-NVFP4 avec la même suite de tests fournirait un troisième point de données.
Et ce n’est pas une comparaison d’accuracy à long terme. La quantization NVFP4 a des effets d’accuracy potentiellement petits. Pour les tâches typiques d’un bureau (résumé, classification de tickets, RAG) rarement perceptibles, pour les edge-cases peut-être bien.
Ce que NVIDIA a publié, lui, se trouve dans la model-card NVFP4 : sur MMLU-Pro, GPQA-Diamond et LiveCodeBench, NVFP4 reste à 0,2 à 0,7 point de leur propre baseline BF16. La propre baseline BF16 de NVIDIA s’écarte elle-même des chiffres de la card officielle Gemma-4 de Google. Les eval-harnesses diffèrent plus que la précision elle-même, donc une comparaison croisée entre vendors sans harness identique est branlante. Ça tombe dans la run-to-run-variance, pas de vraie dégradation. Ce qui est curieux dans ce même tableau, c’est que la baseline BF16 de NVIDIA s’écarte à son tour de ce que Google publie dans la card officielle Gemma-4 : MMLU-Pro 85.0 vs 82.6, GPQA 80.3 vs 82.3, LiveCodeBench 80.5 vs 77.1. Pas parce que la quantization devient meilleure que l’original, mais parce que l’eval-harness compte visiblement plus que la précision elle-même. Autres prompts, autre temperature, autres critères d’arrêt. Les comparaisons croisées entre vendors sont donc difficiles à établir solidement sans le même harness.
Ce qui reste
Le décodage vend le benchmark, le prefill décide de l’expérience. C’était vrai en première partie et ça l’est toujours. Ce que NVFP4 ajoute, c’est que le décodage devient plus rapide dans chaque workload, et le plus là où ça compte : à plus grand contexte et avec plus d’utilisateurs en même temps. Le TTFT reste à peu près pareil sur SM12.1 parce que le prefill est compute-bound et que la Spark n’a pas de tensor cores FP4 natifs. Pour ce que l’utilisateur ressent dès que les tokens commencent à couler, NVFP4 sur ce matériel est nettement meilleur que BF16, et ça ne coûte rien en douleur d’installation : une image officielle vLLM, un flag de modèle, et ça tourne.