On-prem AI 19 min lezen Bijgewerkt

Nemotron-3 op de DGX Spark: BF16 vs FP8 vs NVFP4

Eén model, drie precisies, dezelfde Spark. Wat geheugen-budget, decode-snelheid en tail-latency doen wanneer je van 16 bit naar 8 bit naar 4 bit gaat.

Geschreven door Django de Vreng

In de vorige posts draaide ik Gemma-4 op de DGX Spark. Eerst alleen BF16 als baseline, daarna NVFP4 vs BF16 over dezelfde test-suite. Dat gaf één model in twee precisies. Nuttig, maar nog geen echt beeld van de keuze die je in productie moet maken.

Voor dit stuk draai ik drie varianten van hetzelfde model naast elkaar: BF16, FP8 en NVFP4 van Nemotron-3-Nano-Omni-30B-A3B-Reasoning. Zelfde Spark. Zelfde vLLM-versie. Zelfde prompts. Zelfde benchmark-suite. Zo dicht bij een eerlijke quantization-vergelijking als ik hem op deze machine kan krijgen.

De korte versie: NVFP4 wint op snelheid en throughput, FP8 wint vaker op tail-latency, BF16 is vooral nog nuttig als baseline. Dat is minder netjes dan “4 bit is altijd beter”. Gelukkig maar, anders was deze post kort geweest. Onderdeel van de gids LLMs draaien op de DGX Spark.

Waarom dit experiment

De Gemma-post liet vooral zien dat NVFP4 op de Spark werkt. Wel met pijn. Vijf vLLM-bugs, een nightly build en genoeg flags om een command-regel eruit te laten zien als een kleine bekentenis.

Maar Gemma beantwoordde niet de vraag die ik voor klanten nodig heb: wat kies je als je vandaag een lokaal model op een Spark wil draaien? BF16 omdat dat de originele weights zijn? FP8 omdat Blackwell daar native goed in is? Of NVFP4 omdat je veel meer model en KV-cache in hetzelfde geheugen krijgt?

Daarom deze run. Eén model in drie precisies. Geen leaderboard-score, maar workloads die lijken op kantoorwerk: chat, RAG, langere antwoorden, meerdere gebruikers tegelijk en een maandagochtend waarop iedereen ineens denkt dat AI toch handig is.

Wat BF16, FP8 en NVFP4 hier betekenen

BF16 is de baseline: 16 bits per parameter, ongeveer 2 bytes. Voor dit model betekent dat grofweg 61,5 GB aan checkpoint-size. Dat past op de Spark, maar het eet veel van je 128 GB unified memory op voordat er ook maar één gebruiker context in de KV-cache heeft staan.

FP8 halveert dat gewicht ongeveer. De checkpoint is 32,8 GB. Op Blackwell is FP8 een logische keuze: minder geheugen, native ondersteuning, en meestal weinig gedoe in vLLM.

NVFP4 gaat nog verder. De checkpoint is 20,9 GB. Niet vier keer kleiner dan BF16, omdat de vision- en audio-encoders in BF16 blijven, maar klein genoeg om de Spark anders te laten voelen. Meer ruimte voor KV-cache, meer batching, meer concurrency.

De nuance: de DGX Spark draait op desktop Blackwell SM12.1. Daar is NVFP4 niet hetzelfde feest als op datacenter-Blackwell. vLLM gebruikt Marlin om FP4 weights te decoderen richting FP16 tijdens compute. Je krijgt de geheugenwinst volledig. De compute-winst is minder zuiver.

Voor deze post maakt dat juist interessant. Dit is geen theoretische quantization-post. Dit is: wat gebeurt er op deze machine, met deze stack, als je de drie opties echt draait?

PrecisieModel sizeGeheugen-budget over van 128 GB
BF1661.5 GB~66 GB
FP832.8 GB~95 GB
NVFP420.9 GB~107 GB

De testopstelling

Alle runs draaien via Docker op de DGX Spark met vllm/vllm-openai:v0.20.0. Officiële release, geen patches.

docker run -d --name vllm-bench \
  --gpus all --ipc=host \
  -v appliance_hf-cache:/root/.cache/huggingface \
  -p 8000:8000 \
  -e HF_TOKEN="***" \
  vllm/vllm-openai:v0.20.0 \
  vllm serve nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-NVFP4 \
  --max-model-len 131072 \
  --gpu-memory-utilization 0.95 \
  --max-num-seqs 256 \
  --max-num-batched-tokens 8192 \
  --trust-remote-code \
  --video-pruning-rate 0.5 \
  --reasoning-parser nemotron_v3 \
  --enable-auto-tool-choice \
  --tool-call-parser qwen3_coder \
  --limit-mm-per-prompt '{"image":0,"audio":0}'

Voor FP8 gebruik ik hetzelfde profiel met --kv-cache-dtype fp8. BF16 draait zonder die KV-cache-flag. Verder blijft de test gelijk.

De benchmark-suite staat beschreven op de arena-methodologie. Kort gezegd: closed-loop tests voor decode en TTFT per gebruiker, plus open-loop tests met Poisson-aankomsten om te zien hoe de server zich gedraagt als requests niet netjes op elkaar wachten.

Setup

Ik begon verkeerd met nvcr.io/nvidia/vllm:26.02-py3, NVIDIA’s eigen vLLM-container. Die had vLLM 0.15.1 en kende de NemotronH_Nano_Omni_Reasoning_V3 architectuur nog niet.

De oplossing was saaier: vllm/vllm-openai:v0.20.0. Officiële release, juiste flashinfer-versies, eerste run werkend.

Onze eigen bench-spark CLI had nog twee kleine fixes nodig: de NVIDIA-entrypoint omzeilen met --entrypoint vllm, en HF_TOKEN automatisch doorgeven aan de container. Daarna liep de suite.

Les: begin met de stable release die de architectuur ondersteunt.

Run A: context-scaling

Deze run is de basis: wat gebeurt er als de prompt langer wordt, terwijl het aantal gebruikers oploopt van één naar tien? Dat raakt direct aan kantoorwerk. Een korte chat is makkelijk. Een RAG-vraag met 25k context en meerdere mensen tegelijk is waar de Spark laat zien hoeveel ruimte er echt over is.

Hier kijk ik naar twee dingen. Eerst decode per gebruiker: hoe snel komt tekst terug zodra de generatie loopt? Daarna TTFT: hoe lang wacht je op het eerste token? Bij lange context is TTFT vaak de pijn die gebruikers als eerste voelen. Ze zien geen tokens, dus het voelt alsof het systeem vastzit.

Single-user is vooral een pure snelheidsmeting. Daar verdubbelt NVFP4 bijna BF16. Bij tien gebruikers wordt het interessanter: de kleinere weights geven vLLM meer ruimte om te batchen, en dan wordt BF16 gewoon zwaar.

Decode/user (tg256), c=1

ContextBF16FP8NVFP4NVFP4 vs BF16
4k29.2351.6860.30+106%
8k28.5949.8255.72+95%
16k28.2447.5255.24+96%
25k28.2448.8554.98+95%

BF16 blijft netjes vlak rond 28-29 tokens per seconde. Dat is stabiel, maar niet snel. FP8 zet daar ongeveer 50 t/s tegenover. NVFP4 zit rond 55-60 t/s. Voor één gebruiker is dat het verschil tussen “prima” en “dit voelt lokaal maar niet lokaal-traag”.

Decode/user (tg256), c=10

ContextBF16FP8NVFP4NVFP4 vs BF16
4k7.7613.4519.69+154%
8k7.1311.1417.90+151%
16k6.3010.7314.99+138%
25k5.568.5912.99+134%

Bij tien gebruikers is NVFP4 niet “wat sneller”. Het is een andere klasse. Op 25k context doet BF16 5,56 tok/s/user. NVFP4 doet 12,99. Dat is nog steeds geen cloud-GPU-cluster, maar het verschil in gevoel is groot: BF16 wordt wachten, NVFP4 blijft werken.

TTFT (eerste token), c=10

ContextBF16FP8NVFP4
4k3.90s2.91s2.45s
8k6.49s5.93s4.03s
16k12.63s10.55s8.01s
25k19.82s16.89s12.71s

Dit is de tabel die ik voor echte gebruikers het meest serieus neem. Bij 25k context en tien gebruikers wacht je met BF16 bijna 20 seconden op het eerste token. Met NVFP4 is dat 12,7 seconden. Nog steeds lang, maar niet hetzelfde soort lang.

Run B: 25k context, concurrency tot 20

Run A laat zien hoe contextlengte schaalt. Run B houdt de context zwaar en verhoogt alleen de concurrency. Dit is de “iedereen stelt tegelijk een grote vraag”-test.

In de praktijk gebeurt dit niet elk uur. Tien tot twintig mensen klikken zelden exact tegelijk met 25k context op verzenden. Maar als je een lokale AI-machine voor een team neerzet, wil je weten hoe hij faalt. Rustig langzamer worden is acceptabel. Een queue die voelt alsof hij dood is, niet.

NVFP4 houdt hier de meeste lucht. Niet omdat het model slimmer wordt, maar omdat de server met kleinere weights meer ruimte heeft voor batching en KV-cache.

GebruikersBF16 d/uFP8 d/uNVFP4 d/uNVFP4 vs BF16
59.0615.3320.75+129%
105.659.1812.99+130%
203.705.977.79+110%
GebruikersBF16 TTFTFP8 TTFTNVFP4 TTFT
511.01s8.89s7.21s
1019.75s15.82s12.74s
2037.88s29.91s24.08s

Twintig gebruikers met 25k context is expres onaardig. Toch is het nuttig. BF16 zit op 37,88 seconden TTFT. Dat voelt stuk. NVFP4 zit op 24,08 seconden. Ook niet gezellig, maar nog steeds ruim dertien seconden sneller.

Aggregate decode laat hetzelfde beeld zien:

GebruikersBF16FP8NVFP4
534 t/s53 t/s71 t/s
1038 t/s59 t/s77 t/s
2044 t/s66 t/s84 t/s

Het plafond verschuift van 44 t/s naar 84 t/s. Voor een enkele gebruiker is dat abstract. Voor een team betekent het dat de queue sneller leegloopt.

Run C: korte prompt, lange output

Dit is de workload voor agents, code-generatie en langere antwoorden: weinig input, veel output. De prompt is maar 1024 tokens, dus prefill is hier niet het probleem. De vraag is vooral hoe snel het model blijft doortikken zodra de output lang wordt.

Daarom kijk ik hier naar decode per gebruiker. TTFT moet laag blijven, maar het echte verschil voel je pas na een paar honderd tokens. Een model dat snel begint maar daarna op 8 tok/s blijft hangen, voelt alsnog traag.

NVFP4 wint hier duidelijk. Bij tien parallelle gebruikers blijft het model op 22,90 tok/s/user zitten. BF16 zakt naar 7,84. Dat is nog leesbaar, maar voor een agent-flow voelt het alsof iemand met de hand meetypt.

GebruikersBF16 d/uFP8 d/uNVFP4 d/u
128.6549.8555.55
512.1921.3230.97
107.8415.2622.90

Voor deze workload is NVFP4 de logische default. FP8 is netjes, maar je levert hier vooral snelheid in zonder dat tail-latency de hoofdrol speelt.

Run E: multi-turn, depth 4

Multi-turn is dichter bij echt gebruik dan één losse prompt. Vijf beurten per gesprek, meerdere gesprekken parallel. Dat lijkt op een medewerker die niet één vraag stelt, maar doorvraagt, corrigeert en context meeneemt.

Hier wil ik niet alleen hoge throughput zien. Ik wil vooral dat de server niet elke beurt opnieuw voelt alsof hij uit een koude start komt. Bij tien gesprekken tegelijk wordt dat relevant: de context groeit per gesprek, de scheduler moet blijven delen, en de gebruiker verwacht dat de chat blijft lopen.

Dit is voor mij de belangrijkste kantoor-run. Niet omdat hij perfect echt is, maar omdat hij het dichtst in de buurt komt van “25 mensen gebruiken dit verspreid over de dag”.

GebruikersBF16 d/uFP8 d/uNVFP4 d/uNVFP4 TTFT
128.6949.7256.18596 ms
511.5020.8730.551032 ms
107.6814.8821.581359 ms

Bij tien parallelle gesprekken zit NVFP4 op 21,58 tok/s/user. FP8 zit op 14,88. BF16 op 7,68. Dat laatste werkt technisch, maar het voelt niet meer als een vlotte chat. NVFP4 blijft ruim boven de grens waar je antwoord als vloeiend ervaart.

Run F: RAG-mix met 8k prompt

RAG is meestal geen 25k context, maar ook geen korte chat. Deze run gebruikt 8k prompt en 512 outputtokens. Denk aan vier chunks van ongeveer 2k tokens, plus vraag en instructie.

Bij RAG telt prefill meer dan bij Run C. Je stopt elke keer een flinke lap context in het model voordat er iets terugkomt. Daarna wil je genoeg decode overhouden om het antwoord bruikbaar snel te maken.

De vraag is dus: blijft quantization helpen als de prompt zwaarder wordt? Ja. NVFP4 blijft duidelijk voor, ook bij twintig gebruikers.

GebruikersBF16 d/uFP8 d/uNVFP4 d/u
512.5021.0227.77
108.1114.3719.65
205.519.8214.09

Bij twintig gebruikers levert NVFP4 14,09 tok/s/user. BF16 zit op 5,51. Voor batch-processing kan dat nog. Voor real-time RAG op een kantoor voelt BF16 krap, zeker als documenten rommelig zijn en prompts langer worden dan je had gehoopt. Dat worden ze altijd.

Run G: korte instructie, 4096 outputtokens

Run G lijkt op Run C, maar trekt de output veel verder door: 4096 tokens. Dit is de shape van agents die plannen uitschrijven, code genereren, lange analyses maken of meerdere bestanden samenvatten.

Bij dit soort workloads is de eerste token bijna bijzaak. Als het antwoord lang is, bepaalt decode-snelheid de ervaring. Tien seconden verschil aan het begin is vervelend. Minutenlang op output wachten is erger.

NVFP4 blijft hier het sterkst. Belangrijker: het blijft ook bij tien gebruikers boven 25 tok/s/user. Dat is voor lokale hardware op een bureau-machine gewoon bruikbaar.

GebruikersBF16 d/uFP8 d/uNVFP4 d/uNVFP4 TTFT
128.6849.7555.44179 ms
514.3225.5634.63427 ms
109.5118.4025.18363 ms

Voor agent-flows is dit vrij hard: BF16 is niet stuk, maar je betaalt elke lange output dubbel. Eerst in geheugen, daarna in wachttijd.

Run H: open-loop kantoor-baseline

Vanaf hier verandert de interpretatie. De vorige runs sturen gecontroleerde batches door het model. Run H gebruikt open-loop traffic: requests komen binnen volgens een Poisson-verdeling. De server moet dus omgaan met aankomsten die niet netjes wachten tot de vorige klaar is.

Dit lijkt meer op een kantoor. Niet perfect, wel beter dan iedereen tegelijk of juist volledig sequentieel. De metrics zijn ook anders. TPOT vertelt hoe snel tokens komen zodra je aan de beurt bent. TTFT P50 vertelt de normale ervaring. TTFT P99 vertelt wat de pechvogel merkt.

Hier wordt FP8 interessant. NVFP4 wint de mediaan en TPOT, maar FP8 wint de tail. Dat is precies waarom ik niet wil eindigen met “NVFP4 is altijd beter”.

MetricBF16FP8NVFP4
Achieved RPS0.260.280.29
Peak concurrent421815
TTFT P501229 ms732 ms618 ms
TTFT P992996 ms2008 ms3235 ms
TPOT P50203 ms74 ms39 ms
Aggregate tok/s120312971329

Die peak concurrent van BF16 lijkt op papier goed, maar is het niet. De queue loopt op omdat BF16 hem minder snel leeg krijgt. NVFP4 verwerkt sneller, dus er staan minder requests tegelijk open. Dat is geen lagere capaciteit, dat is minder file.

De echte keuze zit tussen NVFP4 en FP8. Wil je de beste mediaan en snelste output, dan NVFP4. Wil je de netste P99 op deze workload, dan FP8.

Run I: ShareGPT replay

ShareGPT replay is rommeliger en daardoor nuttig. Echte gesprekken hebben wisselende lengtes, vervolgvragen, korte antwoorden, lange antwoorden en prompts die niet door een benchmark-auteur netjes zijn gladgestreken.

Dit is de run die ik het meest vertrouw voor chatgevoel. Niet voor bedrijfsdocumenten, wel voor de vraag: hoe voelt dit als meerdere mensen door de dag heen gesprekken voeren?

Het patroon uit Run H blijft staan. NVFP4 is het snelst voor de doorsnee gebruiker. FP8 heeft de betere P99.

MetricBF16FP8NVFP4
Peak concurrent171210
TTFT P50433 ms220 ms157 ms
TTFT P99713 ms422 ms1361 ms
TPOT P50118 ms38 ms26 ms

NVFP4 voelt instant voor de meeste gebruikers: 157 ms TTFT P50 en 26 ms TPOT P50. Maar de P99 is 1361 ms, waar FP8 op 422 ms blijft. Dat is een fors verschil.

Voor een interne chat waar een enkele tragere request geen ramp is, kies ik NVFP4. Voor een product-UI met harde latency-belofte zou ik FP8 serieuzer nemen.

Run J: maandagochtend-piek

Run J is oversubscribe. Het target is 1,5 requests per seconde met een concurrency-cap van 25. Dit is niet de normale werkdag. Dit is de test voor wat er gebeurt als de vraag groter is dan de server netjes kan bijhouden.

Bij oversubscribe kijk ik eerst naar achieved RPS. Niet naar configured RPS, want die is voor iedereen hetzelfde. De vraag is hoeveel requests de server daadwerkelijk verwerkt terwijl hij onder druk staat.

Daar wint NVFP4 duidelijk. FP8 houdt de tail netter, maar NVFP4 krijgt veel meer werk door de machine.

MetricBF16FP8NVFP4
Configured RPS1.501.501.50
Achieved RPS0.250.430.58
Peak concurrent282828
TTFT P501130 ms757 ms687 ms
TTFT P995184 ms3388 ms4462 ms
TPOT P50197 ms112 ms82 ms
Aggregate tok/s111819512622

Concreet: NVFP4 verwerkt ongeveer 35 requests per minuut. BF16 ongeveer 15. Dat is het verschil tussen een queue die langzaam leegloopt en een queue die gebruikers aan het twijfelen brengt of ze nog een keer moeten klikken. Niet klikken. Nooit helpen die tweede klikken.

De drie precisies naast elkaar

Als ik één realistische chat-run moet kiezen, pak ik ShareGPT replay. Daar zie je het onderscheid het schoonst: NVFP4 wint de normale ervaring, FP8 wint de tail, BF16 doet mee maar nergens overtuigend.

MetricBF16FP8NVFP4Beste keuze
TPOT P50118 ms38 ms26 msNVFP4
TTFT P50433 ms220 ms157 msNVFP4
TTFT P99713 ms422 ms1361 msFP8
Peak concurrent171210NVFP4
Achieved RPS0.300.300.30gelijk

Bij oversubscribe wordt het verschil harder:

MetricBF16FP8NVFP4Beste keuze
Achieved RPS0.250.430.58NVFP4
TTFT P501130 ms757 ms687 msNVFP4
TTFT P995184 ms3388 ms4462 msFP8
TPOT P50197 ms112 ms82 msNVFP4
Aggregate tok/s111819512622NVFP4

Dat maakt de keuze praktischer dan ik vooraf dacht. NVFP4 is de default als je throughput en normale gebruikerservaring wil. FP8 is de keuze als je P99 belangrijker vindt dan mediaan. BF16 is de baseline waarmee je checkt of quantization je accuracy sloopt.

Waarom FP8 de P99 wint

Mijn hypothese: NVFP4 geeft vLLM meer geheugenruimte en daarmee meer batchingruimte. Dat verhoogt throughput en verlaagt TPOT, maar individuele requests kunnen soms langer wachten voordat ze netjes in een batch vallen.

FP8 heeft minder headroom dan NVFP4, maar nog genoeg voor deze workload. Daardoor lijkt de scheduler voorspelbaarder. Minder agressief, minder snel in mediaan, beter in de tail.

BF16 heeft het slechtste van beide werelden: grote weights, minder KV-cache-headroom en lagere decode. De queue wordt voller, maar niet omdat de server zo veel tegelijk aankan. Hij komt er gewoon minder snel doorheen.

Dit wil ik nog verder uitzoeken met scheduler-instellingen en prefix caching. De ruwe cijfers en de testdefinities staan in de arena zodat ik toekomstige runs naast dezelfde lat kan leggen.

Vergelijking met Gemma-4-26B-A4B

Nemotron-NVFP4 is single-user bijna twee keer sneller dan Gemma-NVFP4. Bij multi-user wordt het verschil kleiner, maar blijft het meestal positief.

WorkloadGemma-NVFP4 d/uNemotron-NVFP4 d/uRatio
pp4096 c=130.0160.302.0×
pp8192 c=129.3555.721.9×
pp25000 c=128.0054.982.0×
pp4096 c=1017.0519.691.2×
pp25000 c=107.6112.991.7×

Dat patroon klopt bij wat het model is. Nemotron heeft 3B active params, Gemma 4B active params. Bij single-user helpt dat hard. Bij multi-user schuift de bottleneck richting geheugen-bandwidth en scheduling, en dan wordt het verschil kleiner.

Wat dit betekent voor on-prem AI

Mijn default keuze voor deze Spark is NVFP4. Niet omdat 4 bit principieel mooier is, maar omdat de cijfers bij deze workloads het dragen: hoogste throughput, snelste mediaan, laagste TPOT, kleinste footprint.

Ik kies FP8 wanneer tail-latency belangrijker is dan mediaan. Denk aan een UI waar je wil kunnen zeggen dat 99 procent van de requests binnen een bepaalde grens start. In Run H, I en J wint FP8 consequent op P99 TTFT.

Ik kies BF16 alleen nog als baseline of voor accuracy-kritische validatie. Niet als productie-default. Daarvoor is het op de Spark te duur: ongeveer drie keer zoveel geheugen als NVFP4 en grofweg de helft van de snelheid.

Voor een 25-persoons-kantoor met chat- en RAG-achtige workload zou ik NVFP4 draaien, met een eigen eval-suite ernaast. Voor een externe chatbot met strakke latency-belofte zou ik FP8 testen. Voor BF16 zou ik vooral een korte run bewaren om te zien wat quantization inhoudelijk verandert.

Wat deze runs niet zeggen

Geen accuracy-tests. FP8 en NVFP4 kunnen inhoudelijk afwijken van BF16. Voor productie moet je dat meten op je eigen documenten, je eigen prompts en je eigen fouttolerantie.

Geen multimodal-benchmarks. Nemotron-3-Nano-Omni is multimodal-aware, maar deze runs zijn text-only. Vision en audio blijven hier buiten beeld.

Geen vergelijking met dense modellen. Dit is een MoE-model. Dense modellen voelen anders, vooral bij output-snelheid en hoe vLLM ermee omgaat.

Geen definitieve scheduler-conclusie. De FP8-vs-NVFP4-tail is interessant genoeg om apart te testen met andere batching- en scheduling-instellingen.

Waar ik land

De precisie-keuze is geen detail. Op de Spark bepaalt hij of dezelfde machine voelt als een lokaal experiment of als iets dat je aan collega’s kunt geven zonder elke vijf minuten uitleg te moeten geven.

NVFP4 verdubbelt in veel runs de bruikbare ervaring ten opzichte van BF16. FP8 is minder spectaculair, maar voorspelbaarder in de tail. BF16 blijft nuttig als referentiepunt, niet als eindstation.

De praktische les uit deze drie posts samen: volg de vendor recipes, draai de stable image en meet je eigen workload. Niet zelf knutselen tenzij je daar een goede reden voor hebt. Ik had bij Gemma een reden. Achteraf was hij middelmatig.

Esc