<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Django de Vreng</title>
  <subtitle>Personal blog by Django de Vreng about production AI, agents, MCP and on-prem.</subtitle>
  <id>https://djangodevreng.nl/en/blog/</id>
  <link rel="self" type="application/atom+xml" href="https://djangodevreng.nl/en/atom.xml"/>
  <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/"/>
  <updated>2026-06-23T00:00:00.000Z</updated>
  <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
  <entry>
    <title>Gemma-4 v23 on the DGX Spark</title>
    <id>https://djangodevreng.nl/en/blog/gemma-4-v23-dgx-spark/</id>
    <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/gemma-4-v23-dgx-spark/"/>
    <published>2026-06-23T00:00:00.000Z</published>
    <updated>2026-06-23T00:00:00.000Z</updated>
    <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
    <category term="on-prem"/>
    <summary>New vLLM v0.23.0 runs for Gemma-4 on the DGX Spark: BF16, NVFP4 and MTP compared across decode, TTFT, tails and practical local-agent limits.</summary>
    <content type="html">&lt;p&gt;NVFP4 is still the practical default for Gemma-4 on the DGX Spark, but MTP is now the interesting middle position. In the new vLLM v0.23.0 runs, NVFP4 still leads on chat and multi-turn, while MTP clearly moves past the BF16 run without switching to NVIDIA&apos;s re-quant.&lt;/p&gt;
&lt;p&gt;I reran the same Gemma-4-26B-A4B family on the &lt;a href=&quot;/en/dgx-spark/&quot;&gt;DGX Spark&lt;/a&gt;, now with &lt;code&gt;vllm/vllm-openai:v0.23.0-aarch64-cu129-ubuntu2404&lt;/code&gt;. The raw data lives in the benchmark repo at commit &lt;a href=&quot;https://github.com/djangodevreng/dgx-spark-benchmarks/commit/605faab6a599d0a76aaf795ad54b3b46ed8f9aa8&quot;&gt;&lt;code&gt;605faab6a599&lt;/code&gt;&lt;/a&gt;. The Arena now has three new entries: BF16 v23, MTP v23 and NVFP4 v23.&lt;/p&gt;
&lt;p&gt;The earlier Gemma post was mostly about the price of context in BF16. This run answers a different question: what changes when the same machine, the same model family and the same workloads run on vLLM v0.23.0, with three serving profiles side by side?&lt;/p&gt;
&lt;h2&gt;The setup that stayed the same&lt;/h2&gt;
&lt;p&gt;All three runs use the same machine and benchmark shape:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;DGX Spark NVIDIA GB10, 128 GB unified memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/vllm-openai:v0.23.0-aarch64-cu129-ubuntu2404&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV-cache&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fp8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix caching&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max model length&lt;/td&gt;
&lt;td&gt;131072&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmark commit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;605faab6a599&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The three profiles:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Served name&lt;/th&gt;
&lt;th&gt;Generated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 v23&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-26B-A4B-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma-4-26b-a4b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-06-22T23:16:36+02:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP v23&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-26B-A4B-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma-4-26b-a4b-mtp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-06-23T03:29:52+02:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 v23&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvidia/Gemma-4-26B-A4B-NVFP4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma-4-26b-a4b-nvfp4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-06-23T01:35:33+02:00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MTP uses the same Google model path as BF16, but served with the MTP profile. NVFP4 uses the NVIDIA re-quant. That distinction matters, because otherwise you quietly compare two things at once: engine behavior and model artifact.&lt;/p&gt;
&lt;h2&gt;Chat: NVFP4 leads, MTP catches BF16&lt;/h2&gt;
&lt;p&gt;The first useful comparison is Run C: 1024 prompt tokens, 1024 output tokens, ten concurrent requests. That is a clean chat shape: not trivially short, not a context monster either.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;TTFT c10&lt;/th&gt;
&lt;th&gt;Decode/user c10&lt;/th&gt;
&lt;th&gt;Total decode c10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 v23&lt;/td&gt;
&lt;td&gt;1342.98 ± 449.90 ms&lt;/td&gt;
&lt;td&gt;11.47 ± 0.45 tok/s&lt;/td&gt;
&lt;td&gt;90.83 ± 7.87 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP v23&lt;/td&gt;
&lt;td&gt;1400.13 ± 142.07 ms&lt;/td&gt;
&lt;td&gt;17.79 ± 1.55 tok/s&lt;/td&gt;
&lt;td&gt;138.97 ± 6.68 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 v23&lt;/td&gt;
&lt;td&gt;1138.26 ± 385.15 ms&lt;/td&gt;
&lt;td&gt;21.59 ± 0.98 tok/s&lt;/td&gt;
&lt;td&gt;151.22 ± 15.96 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is the core. MTP gives roughly 55 percent more per-user decode than BF16 on this chat run. NVFP4 is still above that, but the gap between MTP and NVFP4 is much smaller than the gap between BF16 and MTP.&lt;/p&gt;
&lt;p&gt;The latency to first token stays in the same range. NVFP4 is fastest here, MTP is not faster in TTFT than BF16. That fits the pattern: these profiles mostly affect decode throughput. Prefill is still work.&lt;/p&gt;
&lt;h2&gt;Multi-turn is where NVFP4 opens up&lt;/h2&gt;
&lt;p&gt;Run E is the most production-shaped closed-loop test for me: five turns per conversation, ten conversations in parallel, 2048 starting tokens and 512 output tokens per turn.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;TTFT c10&lt;/th&gt;
&lt;th&gt;Decode/user c10&lt;/th&gt;
&lt;th&gt;Total decode c10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 v23&lt;/td&gt;
&lt;td&gt;2154.60 ± 858.63 ms&lt;/td&gt;
&lt;td&gt;10.69 ± 0.25 tok/s&lt;/td&gt;
&lt;td&gt;98.35 ± 3.95 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP v23&lt;/td&gt;
&lt;td&gt;2368.00 ± 789.47 ms&lt;/td&gt;
&lt;td&gt;16.57 ± 1.32 tok/s&lt;/td&gt;
&lt;td&gt;143.47 ± 4.67 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 v23&lt;/td&gt;
&lt;td&gt;1966.10 ± 735.30 ms&lt;/td&gt;
&lt;td&gt;20.01 ± 0.80 tok/s&lt;/td&gt;
&lt;td&gt;182.90 ± 6.67 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is where NVFP4 feels right. 182.90 tok/s total for ten multi-turn conversations on a Spark is not a demo number, it is a usable local inference profile.&lt;/p&gt;
&lt;p&gt;MTP stays useful. Not as the winner, but as an answer to: what if I want to keep serving the Google BF16 model artifact and still get more decode? Then 16.57 tok/s per user is a big difference from 10.69.&lt;/p&gt;
&lt;h2&gt;Long output: more tokens, not automatically more pain&lt;/h2&gt;
&lt;p&gt;For agents and code generation, Run G matters: 256 prompt tokens, 4096 output tokens, ten concurrent requests. This shape tells you whether long generations make the machine collapse.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;TTFT c10&lt;/th&gt;
&lt;th&gt;Decode/user c10&lt;/th&gt;
&lt;th&gt;Total decode c10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 v23&lt;/td&gt;
&lt;td&gt;490.95 ± 4.88 ms&lt;/td&gt;
&lt;td&gt;12.47 ± 0.94 tok/s&lt;/td&gt;
&lt;td&gt;87.16 ± 3.88 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP v23&lt;/td&gt;
&lt;td&gt;564.16 ± 14.86 ms&lt;/td&gt;
&lt;td&gt;17.67 ± 1.92 tok/s&lt;/td&gt;
&lt;td&gt;127.52 ± 9.05 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 v23&lt;/td&gt;
&lt;td&gt;368.83 ± 54.97 ms&lt;/td&gt;
&lt;td&gt;23.69 ± 1.65 tok/s&lt;/td&gt;
&lt;td&gt;120.96 ± 50.17 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Notice the odd shape: NVFP4 has the highest per-user decode, but total decode has much more spread. MTP is lower per user, but stabler in this specific run. So I would not only look at the tallest bar here. For agents you also want predictability, especially when multiple runs keep streaming for a long time.&lt;/p&gt;
&lt;h2&gt;25k context is still the wall&lt;/h2&gt;
&lt;p&gt;Quantization and MTP do not change the fact that large context is mostly prefill. At 25k prompt tokens and c10, it looks like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;TTFT c10&lt;/th&gt;
&lt;th&gt;Decode/user c10&lt;/th&gt;
&lt;th&gt;Total decode c10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 v23&lt;/td&gt;
&lt;td&gt;39281.43 ± 20075.74 ms&lt;/td&gt;
&lt;td&gt;5.28 ± 2.13 tok/s&lt;/td&gt;
&lt;td&gt;28.49 ± 0.62 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP v23&lt;/td&gt;
&lt;td&gt;45640.37 ± 23247.85 ms&lt;/td&gt;
&lt;td&gt;6.05 ± 3.24 tok/s&lt;/td&gt;
&lt;td&gt;27.62 ± 0.27 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 v23&lt;/td&gt;
&lt;td&gt;38575.15 ± 19624.30 ms&lt;/td&gt;
&lt;td&gt;7.40 ± 4.24 tok/s&lt;/td&gt;
&lt;td&gt;33.54 ± 0.03 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is no longer chat. At ten concurrent 25k prompts, you wait around 39 to 46 seconds on average for the first token. NVFP4 helps decode a little, but the user mostly feels an empty window before the stream starts.&lt;/p&gt;
&lt;p&gt;That is the same lesson as in the earlier &lt;a href=&quot;/en/blog/gemma-4-dgx-spark-benchmarks/&quot;&gt;Gemma-4 benchmark post&lt;/a&gt;, now with vLLM v0.23.0 added: context is not a free input box. If you make a local agent carry 25k tokens around, you pay for it in TTFT.&lt;/p&gt;
&lt;h2&gt;Open-loop: the office shape remains usable&lt;/h2&gt;
&lt;p&gt;The open-loop tests matter more for feel than the closed-loop tables. They dispatch requests according to an arrival pattern instead of starting everything at once.&lt;/p&gt;
&lt;h3&gt;H: office baseline&lt;/h3&gt;
&lt;p&gt;200 random prompts, request rate 0.3, burstiness 0.7.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;OK&lt;/th&gt;
&lt;th&gt;Output tok/s&lt;/th&gt;
&lt;th&gt;P95 TTFT&lt;/th&gt;
&lt;th&gt;P95 TPOT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 v23&lt;/td&gt;
&lt;td&gt;200/200&lt;/td&gt;
&lt;td&gt;129.92&lt;/td&gt;
&lt;td&gt;2835.43 ms&lt;/td&gt;
&lt;td&gt;197.57 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP v23&lt;/td&gt;
&lt;td&gt;200/200&lt;/td&gt;
&lt;td&gt;132.35&lt;/td&gt;
&lt;td&gt;3394.53 ms&lt;/td&gt;
&lt;td&gt;178.77 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 v23&lt;/td&gt;
&lt;td&gt;200/200&lt;/td&gt;
&lt;td&gt;139.05&lt;/td&gt;
&lt;td&gt;2393.78 ms&lt;/td&gt;
&lt;td&gt;77.98 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;NVFP4 is clearly nicer here. Not because of much higher output throughput, because 139.05 versus 129.92 tok/s is not a revolution. The difference is TPOT: 77.98 ms p95 versus 197.57 ms for BF16. The stream feels much faster once it starts.&lt;/p&gt;
&lt;h3&gt;I: ShareGPT replay&lt;/h3&gt;
&lt;p&gt;250 real conversations, same request rate.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;OK&lt;/th&gt;
&lt;th&gt;Output tok/s&lt;/th&gt;
&lt;th&gt;P95 TTFT&lt;/th&gt;
&lt;th&gt;P95 TPOT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 v23&lt;/td&gt;
&lt;td&gt;250/250&lt;/td&gt;
&lt;td&gt;60.93&lt;/td&gt;
&lt;td&gt;456.10 ms&lt;/td&gt;
&lt;td&gt;115.31 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP v23&lt;/td&gt;
&lt;td&gt;250/250&lt;/td&gt;
&lt;td&gt;61.47&lt;/td&gt;
&lt;td&gt;576.82 ms&lt;/td&gt;
&lt;td&gt;77.32 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 v23&lt;/td&gt;
&lt;td&gt;250/250&lt;/td&gt;
&lt;td&gt;61.99&lt;/td&gt;
&lt;td&gt;225.09 ms&lt;/td&gt;
&lt;td&gt;45.30 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is the best proxy for normal chat. Short, real conversations. NVFP4 gives p95 TTFT of 225.09 ms and p95 TPOT of 45.30 ms. Locally, that does not feel like a compromise.&lt;/p&gt;
&lt;h3&gt;J: Monday morning peak&lt;/h3&gt;
&lt;p&gt;300 random prompts, target 1.5 rps, max concurrency 25.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;OK&lt;/th&gt;
&lt;th&gt;Output tok/s&lt;/th&gt;
&lt;th&gt;P95 TTFT&lt;/th&gt;
&lt;th&gt;P95 TPOT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 v23&lt;/td&gt;
&lt;td&gt;300/300&lt;/td&gt;
&lt;td&gt;132.04&lt;/td&gt;
&lt;td&gt;3006.73 ms&lt;/td&gt;
&lt;td&gt;199.23 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP v23&lt;/td&gt;
&lt;td&gt;300/300&lt;/td&gt;
&lt;td&gt;172.32&lt;/td&gt;
&lt;td&gt;3870.47 ms&lt;/td&gt;
&lt;td&gt;235.91 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 v23&lt;/td&gt;
&lt;td&gt;300/300&lt;/td&gt;
&lt;td&gt;218.90&lt;/td&gt;
&lt;td&gt;2390.17 ms&lt;/td&gt;
&lt;td&gt;124.58 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Under overload, NVFP4 also stays the most usable. Every request succeeds, but the queue decides who feels the pain. BF16 and MTP produce less friendly tails here. MTP has more output throughput than BF16, but worse p95 TTFT and p95 TPOT. That is exactly why I want percentiles, not only tokens per second.&lt;/p&gt;
&lt;h2&gt;What I put into the Arena&lt;/h2&gt;
&lt;p&gt;I added three new Arena entries instead of overwriting the old Gemma-4 entries. The old v0.20.1 runs remain useful as historical comparison points. These new entries are explicitly v23:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/en/arena/gemma-4-26b-a4b-it-bf16-v23/&quot;&gt;&lt;code&gt;gemma-4-26b-a4b-it-bf16-v23&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/en/arena/gemma-4-26b-a4b-it-mtp-v23/&quot;&gt;&lt;code&gt;gemma-4-26b-a4b-it-mtp-v23&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/en/arena/gemma-4-26b-a4b-nvfp4-v23/&quot;&gt;&lt;code&gt;gemma-4-26b-a4b-nvfp4-v23&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The short ranking for my own use:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;NVFP4 v23 for local chat, agents and office load.&lt;/li&gt;
&lt;li&gt;MTP v23 if you want to keep the Google model artifact but BF16 decode is too slow.&lt;/li&gt;
&lt;li&gt;BF16 v23 as a control line and for comparisons where precision matters more than serving speed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For 25k context, none of the three solves the real problem. There you work on prompt budget, retrieval, memory compaction and agent architecture. Not on hoping a serving profile makes the wait disappear.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The three numbers behind a fast DGX Spark</title>
    <id>https://djangodevreng.nl/en/blog/three-numbers-dgx-spark-speed/</id>
    <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/three-numbers-dgx-spark-speed/"/>
    <published>2026-05-22T00:00:00.000Z</published>
    <updated>2026-05-22T00:00:00.000Z</updated>
    <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
    <category term="on-prem"/>
    <summary>Decode, prefill and queueing: three numbers decide whether a DGX Spark feels fast under a real workload, and those three are exactly what most reviews skip.</summary>
    <content type="html">&lt;p&gt;Can you seriously run large language models locally on a DGX Spark? Yes. That is the boring answer, and it is also the answer every review hands you: a model name, a number, tokens per second, done.&lt;/p&gt;
&lt;p&gt;The useful answer is harder. A model that handles one demo prompt nicely tells you nothing about a Monday morning with ten people, big context, agent flows and someone pasting half a novel into a ticket. That is where it starts to chafe, or it doesn&apos;t. And that does not depend on the Spark, it depends on your workload.&lt;/p&gt;
&lt;p&gt;I have a Spark sitting in the lab and ran a stack of models on it, in BF16, FP8 and NVFP4. Nine workloads, two measurement methods, and a few runs redone because the first ones looked suspiciously good. What was left after all that measuring is not a scoreboard. It is one way of looking at it that held up every time, and it is below. The hard numbers per model are in the separate posts, and the complete guide with the setup, the cost and who it works for is at &lt;a href=&quot;/en/dgx-spark/&quot;&gt;Running LLMs on the DGX Spark&lt;/a&gt;. This piece is about that one lens.&lt;/p&gt;
&lt;h2&gt;What the thing actually is&lt;/h2&gt;
&lt;p&gt;The DGX Spark is NVIDIA&apos;s smallest Blackwell machine. A &lt;a href=&quot;https://www.nvidia.com/en-us/products/workstations/dgx-spark/&quot;&gt;GB10 superchip&lt;/a&gt;, 128 GB unified memory, small enough for a server rack. No separate graphics card with its own memory pool, but one memory that the CPU and the GPU share together. Remember that number, 128 GB. It is your entire budget, and everything that follows is a division sum inside that 128.&lt;/p&gt;
&lt;p&gt;One thing you need to know up front, because it explains half the numbers later. The Spark runs on desktop Blackwell, SM12.1, and that chip cannot compute natively in 4-bit. The big datacenter Blackwell, the B200, can. The result: from 4-bit quantization you get the full memory gain on the Spark, but not the full compute gain. vLLM works around this by pulling 4-bit weights back up to higher precision during compute.&lt;/p&gt;
&lt;p&gt;That works fine. But it is exactly why you should not blindly stick the pretty FP4 numbers from a B200 onto your own Spark.&lt;/p&gt;
&lt;h2&gt;What fits in 128 GB&lt;/h2&gt;
&lt;p&gt;Short version: the weights go in first, the rest is KV-cache for all users together. Precision is therefore a design choice up front, not a knob afterward, and I wrote a &lt;a href=&quot;/en/blog/quantization-local-llms/&quot;&gt;separate post&lt;/a&gt; about it. The question is never whether a model fits, but what is left when it does. The full division sum is in the &lt;a href=&quot;/en/dgx-spark/&quot;&gt;guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How fast it really is&lt;/h2&gt;
&lt;p&gt;This is where most DGX Spark reviews go wrong. They grab one prompt, measure tokens per second, and call that &quot;the speed&quot;. But speed on this machine is not a number. It is three things, they feel different and they behave differently. Pull them apart and the whole Spark falls into place.&lt;/p&gt;
&lt;h3&gt;Decode is nearly free&lt;/h3&gt;
&lt;p&gt;Decode is the text that comes in once the model is actually generating. On the Spark that is boringly stable, and boring is a compliment here. One user on a 26B model gets between 23 and 24 tokens per second in BF16, whether you feed it 4k or 25k context. Ten users at once: about 9 to 12 each, and that is where it sticks. Decode therefore hangs on how many people are busy at the same time, not on how long their prompt is.&lt;/p&gt;
&lt;p&gt;And quantization lifts that whole line up. &lt;a href=&quot;/en/blog/gemma-4-nvfp4-vs-bf16-dgx-spark/&quot;&gt;NVFP4 won on decode in all nine tests&lt;/a&gt;, by 22 to 92 percent depending on the workload. On a lighter MoE model like Nemotron-3, single-user decode even brushes up against 60 t/s. Decode, in short, is not the problem.&lt;/p&gt;
&lt;h3&gt;Prefill is the bill&lt;/h3&gt;
&lt;p&gt;Prefill is. Prefill is the silence before the first token, and that is what a user experiences as &quot;slow&quot;, not the tokens after it.&lt;/p&gt;
&lt;p&gt;Prefill scales with your prompt size, and that hurts. A short prompt is processed within half a second, even with ten people at once. Throw 25k context at it with those same ten users and you wait 35 seconds for the first character. Same machine, same concurrency, just a longer prompt. Double the prompt, roughly double the wait.&lt;/p&gt;
&lt;p&gt;And quantization? Barely helps here. Prefill is compute, and compute is exactly where that SM12.1 handicap sits. NVFP4 makes your decode faster. Your prefill stays prefill.&lt;/p&gt;
&lt;h3&gt;Under pressure it queues, it doesn&apos;t crash&lt;/h3&gt;
&lt;p&gt;That leaves the question: what does it do when you simply throw too much at it? The answer is reassuringly boring. It does not fall over. It gets in line.&lt;/p&gt;
&lt;p&gt;In the heaviest test I wanted to push 1.5 requests per second through the machine. It managed almost six times less than that. And yet not a single one of the 300 requests failed. The slowdown also did not go to everyone, it went to the tail: the average user noticed little, the unlucky one percent waited six seconds for their first token.&lt;/p&gt;
&lt;p&gt;For on-prem that is the best outcome you can hope for. A crash is a phone call. A queue is a bit of patience. An office lives with the second, not the first.&lt;/p&gt;
&lt;p&gt;That is the whole model. Decode is nearly free, prefill is the bill, queueing is your safety net. The numbers underneath it, nine workloads per model and two measurement methods, are in the &lt;a href=&quot;/en/arena/&quot;&gt;arena&lt;/a&gt; and in the separate posts: the &lt;a href=&quot;/en/blog/gemma-4-dgx-spark-benchmarks/&quot;&gt;BF16 baseline&lt;/a&gt;, &lt;a href=&quot;/en/blog/gemma-4-nvfp4-vs-bf16-dgx-spark/&quot;&gt;NVFP4 against BF16&lt;/a&gt; and &lt;a href=&quot;/en/blog/nemotron-3-dgx-spark-precisions/&quot;&gt;Nemotron-3 in three precisions&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The rest is in the guide&lt;/h2&gt;
&lt;p&gt;Which engine I run (vLLM), &lt;a href=&quot;/en/dgx-spark-cost/&quot;&gt;what a Spark costs&lt;/a&gt;, and who this does or does not work for: that is the complete picture, and it belongs in the &lt;a href=&quot;/en/dgx-spark/&quot;&gt;guide&lt;/a&gt;, not in this one lens story. The short version of &quot;who for&quot;: local only gets interesting once the data is not allowed to leave the building. If you don&apos;t have that requirement and you just want the fastest, cheapest tokens, then a cloud API is the more honest answer.&lt;/p&gt;
&lt;p&gt;Running local is not a principle. It is a division: what has to stay in, and what is allowed to go out.&lt;/p&gt;
&lt;h2&gt;Do it yourself&lt;/h2&gt;
&lt;p&gt;Everything underneath is open. The models are on Hugging Face, vLLM is open source, and the raw benchmark output plus the scripts are on &lt;a href=&quot;https://github.com/djangodevreng/dgx-spark-benchmarks&quot;&gt;GitHub&lt;/a&gt;. The &lt;a href=&quot;/en/arena/methodology/&quot;&gt;methodology&lt;/a&gt; explains which nine workloads I run and why.&lt;/p&gt;
&lt;p&gt;If you have a Spark yourself, you should be able to walk the same route and get roughly the same numbers. If that doesn&apos;t work out, that is exactly what I want to know. &lt;a href=&quot;/en/contact/&quot;&gt;Feel free to email&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Why this blog and arena exist</title>
    <id>https://djangodevreng.nl/en/blog/why-this-blog-and-arena/</id>
    <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/why-this-blog-and-arena/"/>
    <published>2026-05-05T00:00:00.000Z</published>
    <updated>2026-05-05T00:00:00.000Z</updated>
    <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
    <category term="reflecties"/>
    <summary>I looked for concrete numbers on local AI on the DGX Spark and never found them. So I measure them myself, building the blog and arena as an open workbench.</summary>
    <content type="html">&lt;p&gt;For clients of &lt;a href=&quot;https://kamoo.ai&quot;&gt;Kamoo&lt;/a&gt; I set up AI systems that sometimes have to stay close to home. Accountants, administrative offices, firms with personal data and financial documents. Exactly the kind of data that does not make your auditor any calmer when you say: &quot;we&apos;ll just send it off to America&quot;.&lt;/p&gt;
&lt;p&gt;That is why we have a &lt;a href=&quot;https://www.nvidia.com/en-us/products/workstations/dgx-spark/&quot;&gt;DGX Spark&lt;/a&gt; standing here. 128 GB unified memory, small enough for a server cabinet, big enough to run serious local models through &lt;a href=&quot;https://docs.vllm.ai/&quot;&gt;vLLM&lt;/a&gt;. What practically fits on it, I collect on &lt;a href=&quot;/en/dgx-spark/&quot;&gt;the overview page about local models on the DGX Spark&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then the practical question started.&lt;/p&gt;
&lt;p&gt;Which model do you use for what on this machine? Which precision do you pick? How much context still fits? Where does concurrency fall over? What happens on an ordinary Monday with ten people who are not all running a benchmark at the same time, but just doing their work?&lt;/p&gt;
&lt;p&gt;I went looking for numbers on exactly those questions. Not a general leaderboard with a score that mostly looks good in a screenshot. Just: this chip, these models, these engines, these workloads, these limits.&lt;/p&gt;
&lt;p&gt;I did not find them.&lt;/p&gt;
&lt;p&gt;So I am building them myself.&lt;/p&gt;
&lt;h2&gt;The arena is the measuring bench&lt;/h2&gt;
&lt;p&gt;Right now there are ten benchmark profiles in the &lt;a href=&quot;/en/arena/&quot;&gt;arena&lt;/a&gt;, with runs for things like context scaling, concurrency, output throughput, RAG-like workloads and a Monday-morning peak.&lt;/p&gt;
&lt;p&gt;That arena has to do one thing well: show what you can practically expect on a DGX Spark. Not which model is &quot;the best&quot; in some abstract sense, but which model stays usable on this hardware under the workloads I run into in client work.&lt;/p&gt;
&lt;p&gt;For a few runs I already wrote down what went wrong and what I took away from it. For instance &lt;a href=&quot;/en/blog/gemma-4-dgx-spark-benchmarks/&quot;&gt;where Gemma-4 starts to grind on the Spark&lt;/a&gt;, &lt;a href=&quot;/en/blog/gemma-4-nvfp4-vs-bf16-dgx-spark/&quot;&gt;what NVFP4 wins over BF16 once the bugs are gone&lt;/a&gt;, and &lt;a href=&quot;/en/blog/nemotron-3-dgx-spark-precisions/&quot;&gt;how three precisions of Nemotron-3 compare&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The raw output is public on GitHub: &lt;a href=&quot;https://github.com/djangodevreng/dgx-spark-benchmarks&quot;&gt;djangodevreng/dgx-spark-benchmarks&lt;/a&gt;. That is on purpose. If you have a Spark yourself, you should be able to walk the same route and get roughly the same numbers. If that does not work out, that is interesting data too.&lt;/p&gt;
&lt;p&gt;So the arena is not a static little list. It is a workbench. New models added, other precisions next to them, workloads tightened up, odd results run again. Boring enough to actually be useful.&lt;/p&gt;
&lt;h2&gt;The blog is the context around it&lt;/h2&gt;
&lt;p&gt;Numbers are handy, but they do not tell the whole story.&lt;/p&gt;
&lt;p&gt;A benchmark can say that NVFP4 is faster than BF16. The blog can tell you that the first runs fell apart on vLLM bugs, that a parameter was set wrong, that a model only became usable after the context length went down, or that the tail latency felt worse than the average let on.&lt;/p&gt;
&lt;p&gt;That is the layer I missed myself when I started. Not just &quot;here is a score&quot;, but: this is what I tried, this broke, this is what I changed, and this is what I would do differently next time.&lt;/p&gt;
&lt;p&gt;That is why the blog and arena sit side by side. The arena gives the measuring points. The blog gives the reasoning, the mistakes and the practical choices behind them.&lt;/p&gt;
&lt;h2&gt;Why local&lt;/h2&gt;
&lt;p&gt;Privacy is usually the polite explanation. It is also true. The more practical reason: some clients have no choice.&lt;/p&gt;
&lt;p&gt;An accountancy firm cannot treat client data as if it were sample text in a demo. Municipalities have rules. Financial documents have rules. Personal data has rules. In practice it all comes down to the same question: can you set this up without legal, compliance and audit immediately slamming the door shut?&lt;/p&gt;
&lt;p&gt;Then you have two options. AI does not fit there, or you make it local.&lt;/p&gt;
&lt;p&gt;We choose local where it is needed. The Spark suddenly makes that less exotic. It is not cheap, but it is manageable for an SME office that wants to do something serious without immediately building its own data center.&lt;/p&gt;
&lt;p&gt;That is where the interesting work is for me: running models, measuring latency, testing prompts, pulling documents through a pipeline, and watching where it breaks.&lt;/p&gt;
&lt;p&gt;Usually it breaks somewhere boring. Those are the best spots.&lt;/p&gt;
&lt;h2&gt;What I want to be able to answer&lt;/h2&gt;
&lt;p&gt;The arena ultimately has to answer questions that keep coming back in projects.&lt;/p&gt;
&lt;p&gt;Which model is fast enough for internal document questions? Which precision gives enough room for several users at the same time? When is NVFP4 fine, when do you want FP8, and when is BF16 mostly an expensive default? How much context can you give before latency gets annoying? Which engine fits which workload better: vLLM, TensorRT-LLM or SGLang?&lt;/p&gt;
&lt;p&gt;These are not academic questions. They decide how you design an on-prem setup. How much hardware you need. Which data stays local. Which steps you might send off to a hosted model. And where you draw the line between &quot;works in a demo&quot; and &quot;holds up on Monday morning&quot;.&lt;/p&gt;
&lt;p&gt;That last line is the whole reason this site exists.&lt;/p&gt;
&lt;h2&gt;Why I write this in public&lt;/h2&gt;
&lt;p&gt;Everything I use for this is open or public: vLLM, models on Hugging Face, benchmark scripts, loose JSON, the site itself. The secret is not access to some magic dashboard. It is in hours of trying, measuring, running again, hunting bugs and then measuring once more because your first run was suspiciously good.&lt;/p&gt;
&lt;p&gt;That has cost me dozens of hours by now. Getting models running, repeating runs, figuring out odd results, and then measuring again because the first run was suspiciously good.&lt;/p&gt;
&lt;p&gt;If someone else walks the same route, they do not have to trip over all the same paving stones again. And if someone contradicts my numbers with better runs: great. Then the arena gets better.&lt;/p&gt;
&lt;p&gt;There is a second reason under it too. This site is itself part of the experiment. The blog, the arena, the flow from benchmark output to structured JSON to pages: that was largely built in a couple of weeks with agents that write and build along. I described the small version of that earlier in &lt;a href=&quot;/en/blog/openclaw-on-raspberry-pi/&quot;&gt;the OpenClaw setup on a Raspberry Pi&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That workflow is part of the work by now. I dump raw findings in Slack, let an agent read the repo and the writing guide, get a branch with a proposal back, run checks and review the diff myself. It does not save me any thinking. It does move a lot of preparation to a layer that just keeps working.&lt;/p&gt;
&lt;p&gt;Writing about that process forces me to make it less messy than my terminal history. That helps. Not always fun, but necessary.&lt;/p&gt;
&lt;h2&gt;What I want to build next&lt;/h2&gt;
&lt;p&gt;First, more benchmarks. vLLM was the starting point, because it works fast and is widely used. TensorRT-LLM is already on the bench for Nemotron-3. SGLang is what I want to put next to the same workloads after that. Only with multiple engines do you see whether your model is slow, your engine is fighting you, or you just did something dumb.&lt;/p&gt;
&lt;p&gt;After that I want to make &lt;code&gt;bench-spark&lt;/code&gt; public: the benchmark runner the way I use it now. Not a perfect framework. But something with which someone on the same hardware can ask the same questions without first rebuilding my mistakes.&lt;/p&gt;
&lt;p&gt;I also want to make a Dutch eval suite for local LLMs. Not another English reasoning benchmark, but office work: accountancy jargon, legal texts, financial documents, documents with odd formatting. Exactly the things local AI gets judged on in the Netherlands.&lt;/p&gt;
&lt;p&gt;And there is more work coming around local RAG on large document sets. No platform pitch. Just figuring out how to get more than a million documents through an on-prem setup without storage, retrieval or OCR slowly starting to hate you.&lt;/p&gt;
&lt;h2&gt;What I skip&lt;/h2&gt;
&lt;p&gt;No daily AI newsletter. There are enough places for that already, some of them on purpose.&lt;/p&gt;
&lt;p&gt;No general-purpose &quot;we do everything with AI&quot; story. Too broad, and usually it means nothing.&lt;/p&gt;
&lt;p&gt;No thought-leader act. I would rather build something that creaks than an opinion that sounds smooth.&lt;/p&gt;
&lt;p&gt;No building a platform like OpenClaw either. I use it, I write about it, I build flows with it. But that layer itself I leave to the people who live in it every day.&lt;/p&gt;
&lt;h2&gt;What this should become&lt;/h2&gt;
&lt;p&gt;For clients this has to show what local AI practically costs: hardware, latency, precision, maintenance, odd edge cases. For me it is the place where I pin down my own assumptions before the next benchmark knocks them out.&lt;/p&gt;
&lt;p&gt;I am trying to keep the rhythm. No promise per week. If there is nothing to report, nothing goes here. If there are bugs, runs and odd graphs, there is probably too much here.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Gemma-4 on the DGX Spark: NVFP4 vs BF16</title>
    <id>https://djangodevreng.nl/en/blog/gemma-4-nvfp4-vs-bf16-dgx-spark/</id>
    <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/gemma-4-nvfp4-vs-bf16-dgx-spark/"/>
    <published>2026-05-03T00:00:00.000Z</published>
    <updated>2026-05-08T00:00:00.000Z</updated>
    <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
    <category term="on-prem"/>
    <summary>Nine identical benchmarks, two precisions. NVFP4 runs 22 to 92 percent faster per token, and peak-hour capacity grows 69 percent on the Spark.</summary>
    <content type="html">&lt;p&gt;import BenchCard from &quot;../../../components/post/BenchCard.astro&quot;;
import BenchCardRow from &quot;../../../components/post/BenchCardRow.astro&quot;;
import Note from &quot;../../../components/post/Note.astro&quot;;&lt;/p&gt;
&lt;p&gt;In the &lt;a href=&quot;/en/blog/gemma-4-dgx-spark-benchmarks/&quot;&gt;BF16 baseline of Gemma-4 on the DGX Spark&lt;/a&gt; I ran nine benchmarks with Gemma-4-26B-A4B in BF16. Decode speed held up just fine, prefill decided when the wall came, and the system queued neatly under pressure instead of crashing. That story seemed done, until NVIDIA released an NVFP4-quantized version of that same model.&lt;/p&gt;
&lt;p&gt;Same architecture and fine-tune, same server config, only the precision changes. From BF16 (16 bits per parameter) to NVFP4 (4 bits per parameter, NVIDIA&apos;s take on FP4). Four times smaller per weight, and if the Blackwell kernels cooperate, also a lot faster on compute-heavy tasks.&lt;/p&gt;
&lt;p&gt;On paper, nice. In practice: the official vLLM v0.20.1 release recognizes this checkpoint without any fuss, and the numbers were faster across the board than the BF16 baseline. Both tests fall under the guide &lt;a href=&quot;/en/dgx-spark/&quot;&gt;running LLMs on the DGX Spark&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Why look into this at all&lt;/h2&gt;
&lt;p&gt;For an office with a local AI machine, memory budget is the most limiting thing after compute. A 26B model in BF16 takes ~48 GB of GPU memory for weights alone. On a Spark with 128 GB of unified memory, that leaves about 65 GB for KV-cache. Enough for the office scenario from the first blog, but not much room to run, say, 30+ users with large context side by side.&lt;/p&gt;
&lt;p&gt;NVFP4 reduces that to ~18 GB for weights. Not four times smaller than BF16 (the vision encoder stays BF16, and scale factors cost space too), but about 2.7× smaller. That gives you toward 95 GB of KV-cache headroom, which in theory should support much higher concurrency. On top of that, less memory traffic is needed per forward pass, so by definition less bandwidth pressure, and that was already the bottleneck in BF16 under multi-user load. So the question was simple: how much of that theoretical gain survives in practice?&lt;/p&gt;
&lt;h2&gt;What NVFP4 actually is&lt;/h2&gt;
&lt;p&gt;NVFP4 is NVIDIA&apos;s take on FP4: floating-point numbers with 4 bits per value. Four bits, not four bytes, so a factor of 4 less per parameter than BF16. By storing a scaling factor per group of weights, accuracy stays reasonably intact.&lt;/p&gt;
&lt;p&gt;For Blackwell it works like this. NVIDIA&apos;s datacenter cards (B100, B200, SM10.0) have tensor cores that can compute &lt;em&gt;natively&lt;/em&gt; with 4-bit values, and that is much faster than the same calculation in FP16 or BF16. The DGX Spark, on the other hand, is desktop Blackwell (GB10, SM12.1) and that architecture has no native FP4 compute.&amp;lt;Note&amp;gt;On a datacenter B200 (SM10.0) you&apos;d expect another 2 to 3× on top of this thanks to native FP4 tensor cores. The Spark lacks that hardware path, so all the gain comes from memory bandwidth, not from compute.&amp;lt;/Note&amp;gt; What you get in that case is &quot;weight-only&quot; FP4: the weights are physically stored as 4-bit (hence the memory gain), but during compute they get decoded on the fly to FP16 for the matrix multiplications. A vLLM warning makes that explicit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So you get the memory gain in full, the compute gain only partially. The Marlin INT4 GEMM kernel is optimized, but not as fast as native FP4 on SM10.0 would be. Worth factoring in when you look at the numbers further down.&lt;/p&gt;
&lt;h2&gt;The test setup&lt;/h2&gt;
&lt;p&gt;Server config identical to the first blog, only the model swaps:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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 &apos;{&quot;image&quot;:0,&quot;audio&quot;:0}&apos; \
  --async-scheduling \
  --no-enable-prefix-caching \
  --host 0.0.0.0 \
  --port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tests are one-to-one identical to the first blog: same commands, same concurrency levels, same datasets for the open-loop tests, same seed. That is on purpose, because if you want to measure the effect of an isolated variable (in this case the precision), everything around it has to stay the same. Exactly how I measure those concurrency levels, seeds and open-loop arrivals is described in the &lt;a href=&quot;/en/arena/methodology/&quot;&gt;Arena measurement method&lt;/a&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;NVFP4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;google/gemma-4-26B-A4B-it&lt;/td&gt;
&lt;td&gt;nvidia/Gemma-4-26B-A4B-NVFP4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active params&lt;/td&gt;
&lt;td&gt;4B&lt;/td&gt;
&lt;td&gt;4B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total params&lt;/td&gt;
&lt;td&gt;26B&lt;/td&gt;
&lt;td&gt;26B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model memory&lt;/td&gt;
&lt;td&gt;~48 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~18 GB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV-cache headroom&lt;/td&gt;
&lt;td&gt;~65 GB&lt;/td&gt;
&lt;td&gt;~95 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoE backend&lt;/td&gt;
&lt;td&gt;(default)&lt;/td&gt;
&lt;td&gt;MARLIN (forced)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three numbers sum up where this lands. Click through for the full run in the Arena, with all seeds, concurrency levels and commands:&lt;/p&gt;
&lt;p&gt;&amp;lt;BenchCardRow&amp;gt;
&amp;lt;BenchCard
model=&quot;gemma-4-26b-a4b-nvfp4&quot;
bench=&quot;long-output&quot;
label=&quot;Decode @ c=10 (256→4k)&quot;
value=&quot;22.5&quot;
unit=&quot;tok/s&quot;
delta=&quot;+92% vs BF16&quot;
/&amp;gt;
&amp;lt;BenchCard
model=&quot;gemma-4-26b-a4b-nvfp4&quot;
bench=&quot;monday-peak&quot;
label=&quot;Monday-peak RPS&quot;
value=&quot;0.44&quot;
delta=&quot;+69% vs BF16&quot;
/&amp;gt;
&amp;lt;BenchCard
model=&quot;gemma-4-26b-a4b-nvfp4&quot;
bench=&quot;sharegpt&quot;
label=&quot;ShareGPT TPOT P50&quot;
value=&quot;39&quot;
unit=&quot;ms&quot;
delta=&quot;−59% vs BF16&quot;
/&amp;gt;
&amp;lt;/BenchCardRow&amp;gt;&lt;/p&gt;
&lt;p&gt;An interactive version of all the numbers is on the &lt;a href=&quot;/en/arena/gemma-4-26b-a4b-nvfp4/&quot;&gt;Arena page for Gemma-4-26B-A4B-NVFP4&lt;/a&gt;, including commands and TTFT percentiles for all 9 tests.&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run A: context scaling from 4k to 25k&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Decode per user as context grows, c=1/5/10:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;24.08&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29.80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;12.55&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22.01&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9.48&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.94&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;23.69&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29.31&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;11.48&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.28&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8.52&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14.35&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;23.34&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28.55&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10.05&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15.67&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;6.79&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.06&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+48%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;22.75&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;27.70&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8.46&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.46&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+47%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5.40&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.55&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+40%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At c=1 the gain is stable around +22-24% across all contexts. Memory bandwidth barely matters for single-user, so the gain here sits in the compute path itself. Marlin&apos;s INT4 decode plus FP16 matmul is slightly faster than BF16&apos;s direct FP16 matmul, even though it&apos;s two steps.&lt;/p&gt;
&lt;p&gt;At c=10 the difference scales much more strongly with workload type, from +40% at 25k context to +79% at 4k. That&apos;s because under multi-user the memory bandwidth becomes the bottleneck, and NVFP4 reads fewer bytes per forward pass. The more concurrent, the more that counts, until you hit the KV-cache memory limits again (25k context with multiple users) and the gain flattens out.&lt;/p&gt;
&lt;p&gt;TTFT (first token) is better too:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 TTFT&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 TTFT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4.46s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.20s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7.99s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.84s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;18.92s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18.69s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;35.67s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35.65s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;On TTFT the gain is small. That makes sense: prefill is compute-heavy, and on SM12.1 without native FP4 tensor cores Marlin has to decode the weights on the fly for the matmul. That gives back some of what the memory bandwidth gained. For decode, bandwidth counts more than compute; for prefill, the other way around.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run B: 25k context, concurrency up to 20&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;The stress test from part one:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;BF16 TTFT&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 TTFT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8.51 t/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.43 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;19.86s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.72s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5.37 t/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.56 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;35.44s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35.51s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;3.16 t/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.26 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;67.37s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67.40s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The aggregate decode plateau shifts from 32 t/s to 36 t/s at c=20: a 12% higher ceiling at 25k context under maximum pressure. TTFT is practically identical between BF16 and NVFP4 because prefill is the wall here and that doesn&apos;t get much faster on SM12.1. Decode per user is clearly better though: at twenty parallel 25k prompts you get 4.26 instead of 3.16 t/s, +35%. Still not chat speed, but a noticeable difference once the tokens start flowing.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run C: 1k prompt, 1k output&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;The short-prompt + long-answer workload, close to agent flows and code generation:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;23.86&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29.45&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+23%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;13.59&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;24.69&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+82%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10.92&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20.88&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+91%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At c=10 per-user decode sits at well over 20 t/s, above reading speed and close to a comfortable streaming UI. Aggregate decode at c=10 hits 209 t/s instead of 86 t/s in BF16, almost a doubling.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run E: multi-turn (depth 4)&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Five consecutive turns per conversation, ten conversations in parallel: the most realistic office shape.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;BF16 TTFT&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 TTFT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;23.97&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29.61&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.53s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.33s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;13.07&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23.98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.32s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.11s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10.43&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.51&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.13s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.94s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For ten parallel 5-turn conversations: 1.94 seconds to first token, 19.51 t/s per user. That fits comfortably within what a reader experiences as chat, and is 87% faster per token than BF16 in the same test.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run F: RAG mix (8k prompt)&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;BF16 TTFT&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 TTFT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;12.11&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20.91&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.32s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.28s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9.31&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15.96&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.99s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.00s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;6.05&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.57&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14.61s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14.45s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;8k context is roughly what a RAG flow with four chunks of 2k tokens takes in. At ten users you wait 8 seconds to first token (almost the same as BF16, because of the compute bottleneck), then 16 t/s streaming. For &quot;ask something about your documents&quot; flows that&apos;s plenty workable, and where the gain sits: in decode speed, not in TTFT.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run G: short instruction, 4096 output tokens&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;The agent / code-generation shape:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;BF16 TTFT&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 TTFT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;24.17&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29.59&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.24s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.11s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;14.32&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25.79&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.38s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.23s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;11.75&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22.54&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.48s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.37s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A TTFT of 110 milliseconds at single-user is very low, lower than most hosted APIs manage over the network. And 22.54 t/s per user at c=10 is plenty for agent streams. Aggregate decode at c=10 in this test comes out at 225 t/s versus 84 t/s in BF16, almost 2.7× as much. For a team running ten concurrent agents that each produce long structured output, this is the most important number.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run H: open-loop, random 4k workload&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;The synthetic office baseline with Poisson arrivals:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Achieved RPS&lt;/td&gt;
&lt;td&gt;0.27&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.29&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak concurrent&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;1286 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1006 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;3316 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2893 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;182 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total tok/s&lt;/td&gt;
&lt;td&gt;1215&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1302&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;What stands out is that peak concurrent drops from 36 to 16 at an identical arrival rate (0.3 rps) and identical prompts. Because NVFP4 handles each request faster, the queue stays shorter, and that&apos;s an important insight for capacity planning: NVFP4 gives you not only lower latency per request, but also less queue pressure at the same arrival rate. At the same time TPOT P50 drops from 182ms to 64ms. Median inter-token latency almost three times faster, then. For a chat UI that shows token streaming, that&apos;s the difference between artificially waiting for an answer and just reading along.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run I: ShareGPT replay (real conversations)&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Real multi-turn conversation data:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Peak concurrent&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;353 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;152 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;637 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;265 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;95 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;39 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A P99 TTFT of 265 milliseconds, for 99 percent of users. A TPOT of 39 ms works out to 25.6 t/s per user. You can safely call that realtime chat for 25 employees with realistic ShareGPT-style prompts.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run J: Monday-morning peak&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;The heaviest scenario from part one: overloaded server, 1.5 rps target with max 25 concurrent requests.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Configured RPS&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Achieved RPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.26&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.44&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;1132 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;920 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;6157 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6054 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;187 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;108 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total tok/s&lt;/td&gt;
&lt;td&gt;1173&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1984&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The most measurable number of the whole day is that achieved RPS goes from 0.26 to 0.44. Same target, same concurrency cap, same Poisson arrivals, and NVFP4 processes 69% more requests per second before the queue clogs up.&lt;/p&gt;
&lt;p&gt;P99 TTFT shifts only marginally (6.16s to 6.05s). That fits the pattern: prefill is compute-bound on SM12.1, and NVFP4 isn&apos;t much faster there. But TPOT P50 drops from 187ms to 108ms, and aggregate token throughput grows from 1173 to 1984 t/s. For a 25-person office at peak hours, that&apos;s the difference between enough and a squeeze: more requests per second processed, with faster streaming for whoever&apos;s up next.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;What this means for on-prem AI&lt;/h2&gt;
&lt;p&gt;If you have a Spark and run Gemma-4-26B, NVFP4 is the upgrade. In all 9 tests NVFP4 is the winner, and it frees up 30 GB of memory for other purposes like more KV-cache, a second small model alongside it, or batch jobs. At Kamoo this NVFP4 config now sits next to the BF16 baseline in &lt;code&gt;bench-spark/&lt;/code&gt;, and one command switches between the two.&lt;/p&gt;
&lt;p&gt;For a 25-person office with realistic ShareGPT-like prompts you notice it right away. TPOT P50 drops from 95 ms to 39 ms, P99 TTFT from 637 ms to 265 ms. And when peak load comes, the system delivers 69% more requests per second before it fills up. For agent flows and code generation (Run G shape) the Spark in NVFP4 is at its strongest: ten parallel agents, each 4096 tokens of output, 22.5 t/s per user with TTFT under 400 ms.&lt;/p&gt;
&lt;p&gt;For 25k context stress (Run B) it stays the wall. NVFP4 barely lowers it (TTFT differs by less than a second), because prefill stays prefill, and ten parallel 25k prompts wait 35 seconds for the first token. Quantization changes nothing about that on this hardware. Decode speed it does change: 7.56 t/s/user instead of 5.37, so once the tokens come, they run faster.&lt;/p&gt;
&lt;h2&gt;What this run doesn&apos;t say&lt;/h2&gt;
&lt;p&gt;This is not NVFP4 on SM10.0 (datacenter Blackwell). There native FP4 compute would make the difference much bigger, with an expectation of a further 2-3× speedup on top of what we see here. On an H100 or B200 these numbers are therefore &lt;em&gt;not&lt;/em&gt; representative; the Spark has a specific SM12.1 handicap (no native FP4) that doesn&apos;t exist in the cloud.&lt;/p&gt;
&lt;p&gt;This is also not a comparison with dense Gemma-4-31B in NVFP4. Dense goes through a different code path in vLLM&apos;s loader. For a follow-up blog, dense NVFP4 with the same test suite would give a third data point.&lt;/p&gt;
&lt;p&gt;And this is not a long-term accuracy comparison. NVFP4 quantization has potentially small accuracy effects. For the typical tasks in an office (summarization, ticket classification, RAG) rarely noticeable, for edge cases possibly yes.&lt;/p&gt;
&lt;p&gt;What NVIDIA did publish is in the &lt;a href=&quot;https://huggingface.co/nvidia/Gemma-4-26B-A4B-NVFP4&quot;&gt;NVFP4 model card&lt;/a&gt;: on MMLU-Pro, GPQA-Diamond and LiveCodeBench, NVFP4 sits within 0.2 to 0.7 points of their own BF16 baseline.&amp;lt;Note&amp;gt;NVIDIA&apos;s own BF16 baseline itself deviates from Google&apos;s official Gemma-4 card numbers. Eval harnesses differ more than precision itself, so cross-comparing between vendors without an identical harness is shaky.&amp;lt;/Note&amp;gt; That falls within run-to-run variance, no real degradation. What&apos;s curious about that same table is that NVIDIA&apos;s BF16 baseline in turn deviates from what Google publishes in the official Gemma-4 card: MMLU-Pro 85.0 vs 82.6, GPQA 80.3 vs 82.3, LiveCodeBench 80.5 vs 77.1. Not because quantization gets better than the original, but because the eval harness apparently matters more than the precision itself. Different prompts, different temperature, different stop criteria. Cross-comparisons between vendors are therefore hard to pin down without the same harness.&lt;/p&gt;
&lt;h2&gt;What sticks&lt;/h2&gt;
&lt;p&gt;Decode sells the benchmark, prefill decides the experience. That held in part one and it still holds. What NVFP4 adds is that decode gets faster in every workload, and most where it matters: at larger context and more users at once. TTFT stays roughly the same on SM12.1 because prefill is compute-bound and the Spark has no native FP4 tensor cores. For what the user feels once the tokens start flowing, NVFP4 on this hardware is a lot better than BF16, and it costs nothing in setup pain: one official vLLM image, one model flag, and it runs.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Nemotron-3 on the DGX Spark: BF16 vs FP8 vs NVFP4</title>
    <id>https://djangodevreng.nl/en/blog/nemotron-3-dgx-spark-precisions/</id>
    <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/nemotron-3-dgx-spark-precisions/"/>
    <published>2026-05-03T00:00:00.000Z</published>
    <updated>2026-05-05T00:00:00.000Z</updated>
    <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
    <category term="on-prem"/>
    <summary>One model, three precisions, the same Spark. What memory budget, decode speed and tail-latency do when you go from 16 bit to 8 bit to 4 bit.</summary>
    <content type="html">&lt;p&gt;In the previous posts I ran Gemma-4 on the DGX Spark. First &lt;a href=&quot;/en/blog/gemma-4-dgx-spark-benchmarks/&quot;&gt;just BF16 as a baseline&lt;/a&gt;, then &lt;a href=&quot;/en/blog/gemma-4-nvfp4-vs-bf16-dgx-spark/&quot;&gt;NVFP4 vs BF16 across the same test suite&lt;/a&gt;. That gave one model in two precisions. Useful, but not yet a real picture of the choice you have to make in production.&lt;/p&gt;
&lt;p&gt;For this piece I run three variants of the same model side by side: &lt;strong&gt;BF16, FP8 and NVFP4&lt;/strong&gt; of Nemotron-3-Nano-Omni-30B-A3B-Reasoning. Same Spark. Same vLLM version. Same prompts. Same benchmark suite. As close to a fair quantization comparison as I can get on this machine.&lt;/p&gt;
&lt;p&gt;The short version: &lt;strong&gt;NVFP4 wins on speed and throughput, FP8 wins more often on tail-latency, BF16 is mostly still useful as a baseline&lt;/strong&gt;. That is less tidy than &quot;4 bit is always better&quot;. Lucky for us, otherwise this post would have been short. Part of the guide &lt;a href=&quot;/en/dgx-spark/&quot;&gt;running LLMs on the DGX Spark&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Why this experiment&lt;/h2&gt;
&lt;p&gt;The Gemma post mostly showed that NVFP4 works on the Spark. With some pain. Five vLLM bugs, a nightly build and enough flags to make a command line look like a small confession.&lt;/p&gt;
&lt;p&gt;But Gemma did not answer the question I need for clients: what do you pick if you want to run a local model on a Spark today? BF16 because those are the original weights? FP8 because Blackwell is natively good at it? Or NVFP4 because you fit much more model and KV-cache in the same memory?&lt;/p&gt;
&lt;p&gt;So here is this run. One model in three precisions. No leaderboard score, but workloads that resemble office work: chat, RAG, longer answers, multiple users at once, and a Monday morning where everyone suddenly decides AI is handy after all.&lt;/p&gt;
&lt;h2&gt;What BF16, FP8 and NVFP4 mean here&lt;/h2&gt;
&lt;p&gt;BF16 is the baseline: 16 bits per parameter, roughly 2 bytes. For this model that means about 61.5 GB of checkpoint size. That fits on the Spark, but it eats a lot of your 128 GB unified memory before a single user has any context in the KV-cache.&lt;/p&gt;
&lt;p&gt;FP8 roughly halves that weight. The checkpoint is 32.8 GB. On Blackwell, FP8 is a logical choice: less memory, native support, and usually little hassle in vLLM.&lt;/p&gt;
&lt;p&gt;NVFP4 goes further. The checkpoint is 20.9 GB. Not four times smaller than BF16, because the vision and audio encoders stay in BF16, but small enough to make the Spark feel different. More room for KV-cache, more batching, more concurrency.&lt;/p&gt;
&lt;p&gt;The nuance: the DGX Spark runs on desktop Blackwell SM12.1. There NVFP4 is not the same party as on datacenter Blackwell. vLLM uses Marlin to decode FP4 weights toward FP16 during compute. You get the memory win fully. The compute win is less pure.&lt;/p&gt;
&lt;p&gt;For this post that is exactly what makes it interesting. This is not a theoretical quantization post. This is: what happens on this machine, with this stack, when you actually run the three options?&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Model size&lt;/th&gt;
&lt;th&gt;Memory budget left of 128 GB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;61.5 GB&lt;/td&gt;
&lt;td&gt;~66 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;32.8 GB&lt;/td&gt;
&lt;td&gt;~95 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;td&gt;20.9 GB&lt;/td&gt;
&lt;td&gt;~107 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;The test setup&lt;/h2&gt;
&lt;p&gt;All runs go through Docker on the DGX Spark with &lt;code&gt;vllm/vllm-openai:v0.20.0&lt;/code&gt;. Official release, no patches.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name vllm-bench \
  --gpus all --ipc=host \
  -v appliance_hf-cache:/root/.cache/huggingface \
  -p 8000:8000 \
  -e HF_TOKEN=&quot;***&quot; \
  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 &apos;{&quot;image&quot;:0,&quot;audio&quot;:0}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For FP8 I use the same profile with &lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt;. BF16 runs without that KV-cache flag. Everything else stays equal.&lt;/p&gt;
&lt;p&gt;The benchmark suite is described in the &lt;a href=&quot;/en/arena/methodology/&quot;&gt;arena methodology&lt;/a&gt;. In short: closed-loop tests for decode and TTFT per user, plus open-loop tests with Poisson arrivals to see how the server behaves when requests do not neatly wait for each other.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;I started wrong with &lt;code&gt;nvcr.io/nvidia/vllm:26.02-py3&lt;/code&gt;, NVIDIA&apos;s own vLLM container. It had vLLM 0.15.1 and did not yet know the &lt;code&gt;NemotronH_Nano_Omni_Reasoning_V3&lt;/code&gt; architecture.&lt;/p&gt;
&lt;p&gt;The fix was more boring: &lt;code&gt;vllm/vllm-openai:v0.20.0&lt;/code&gt;. Official release, correct flashinfer versions, first run working.&lt;/p&gt;
&lt;p&gt;Our own &lt;code&gt;bench-spark&lt;/code&gt; CLI still needed two small fixes: bypass the NVIDIA entrypoint with &lt;code&gt;--entrypoint vllm&lt;/code&gt;, and pass &lt;code&gt;HF_TOKEN&lt;/code&gt; to the container automatically. After that the suite ran.&lt;/p&gt;
&lt;p&gt;Lesson: start with the stable release that supports the architecture.&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run A: context-scaling&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;This run is the foundation: what happens when the prompt gets longer, while the number of users climbs from one to ten? That touches office work directly. A short chat is easy. A RAG question with 25k context and several people at once is where the Spark shows how much room is really left.&lt;/p&gt;
&lt;p&gt;Here I look at two things. First decode per user: how fast does text come back once generation is running? Then TTFT: how long do you wait for the first token? With long context TTFT is often the pain users feel first. They see no tokens, so it feels like the system is stuck.&lt;/p&gt;
&lt;p&gt;Single-user is mostly a pure speed measurement. There NVFP4 nearly doubles BF16. At ten users it gets more interesting: the smaller weights give vLLM more room to batch, and then BF16 just gets heavy.&lt;/p&gt;
&lt;h3&gt;Decode/user (tg256), c=1&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;NVFP4 vs BF16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;29.23&lt;/td&gt;
&lt;td&gt;51.68&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60.30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+106%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;28.59&lt;/td&gt;
&lt;td&gt;49.82&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55.72&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;28.24&lt;/td&gt;
&lt;td&gt;47.52&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55.24&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;28.24&lt;/td&gt;
&lt;td&gt;48.85&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;54.98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+95%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;BF16 stays neatly flat around 28-29 tokens per second. That is stable, but not fast. FP8 puts about 50 t/s against it. NVFP4 sits around 55-60 t/s. For a single user that is the difference between &quot;fine&quot; and &quot;this feels local but not local-slow&quot;.&lt;/p&gt;
&lt;h3&gt;Decode/user (tg256), c=10&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;NVFP4 vs BF16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;7.76&lt;/td&gt;
&lt;td&gt;13.45&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.69&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+154%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;7.13&lt;/td&gt;
&lt;td&gt;11.14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+151%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;6.30&lt;/td&gt;
&lt;td&gt;10.73&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14.99&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+138%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;5.56&lt;/td&gt;
&lt;td&gt;8.59&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.99&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+134%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At ten users NVFP4 is not &quot;a bit faster&quot;. It is a different class. At 25k context BF16 does 5.56 tok/s/user. NVFP4 does 12.99. That is still no cloud-GPU cluster, but the difference in feel is large: BF16 becomes waiting, NVFP4 keeps working.&lt;/p&gt;
&lt;h3&gt;TTFT (first token), c=10&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;3.90s&lt;/td&gt;
&lt;td&gt;2.91s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.45s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;6.49s&lt;/td&gt;
&lt;td&gt;5.93s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.03s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;12.63s&lt;/td&gt;
&lt;td&gt;10.55s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.01s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;19.82s&lt;/td&gt;
&lt;td&gt;16.89s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.71s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is the table I take most seriously for real users. At 25k context and ten users you wait almost 20 seconds for the first token with BF16. With NVFP4 that is 12.7 seconds. Still long, but not the same kind of long.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run B: 25k context, concurrency up to 20&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Run A shows how context length scales. Run B keeps the context heavy and only raises the concurrency. This is the &quot;everyone asks a big question at the same time&quot; test.&lt;/p&gt;
&lt;p&gt;In practice this does not happen every hour. Ten to twenty people rarely click send at exactly the same moment with 25k context. But if you put a local AI machine in front of a team, you want to know how it fails. Calmly getting slower is acceptable. A queue that feels dead is not.&lt;/p&gt;
&lt;p&gt;NVFP4 keeps the most air here. Not because the model gets smarter, but because the server with smaller weights has more room for batching and KV-cache.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;FP8 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;NVFP4 vs BF16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;9.06&lt;/td&gt;
&lt;td&gt;15.33&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20.75&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+129%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5.65&lt;/td&gt;
&lt;td&gt;9.18&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.99&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+130%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;3.70&lt;/td&gt;
&lt;td&gt;5.97&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.79&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+110%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 TTFT&lt;/th&gt;
&lt;th&gt;FP8 TTFT&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 TTFT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;11.01s&lt;/td&gt;
&lt;td&gt;8.89s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.21s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;19.75s&lt;/td&gt;
&lt;td&gt;15.82s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.74s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;37.88s&lt;/td&gt;
&lt;td&gt;29.91s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;24.08s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Twenty users with 25k context is deliberately unkind. Still, it is useful. BF16 sits at 37.88 seconds TTFT. That feels broken. NVFP4 sits at 24.08 seconds. Also not cozy, but still a good thirteen seconds faster.&lt;/p&gt;
&lt;p&gt;Aggregate decode shows the same picture:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;34 t/s&lt;/td&gt;
&lt;td&gt;53 t/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;71 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;38 t/s&lt;/td&gt;
&lt;td&gt;59 t/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;77 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;44 t/s&lt;/td&gt;
&lt;td&gt;66 t/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;84 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The ceiling shifts from 44 t/s to 84 t/s. For a single user that is abstract. For a team it means the queue drains faster.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run C: short prompt, long output&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;This is the workload for agents, code generation and longer answers: little input, a lot of output. The prompt is only 1024 tokens, so prefill is not the problem here. The question is mostly how fast the model keeps ticking once the output gets long.&lt;/p&gt;
&lt;p&gt;So here I look at decode per user. TTFT has to stay low, but the real difference you feel only after a few hundred tokens. A model that starts fast but then hangs at 8 tok/s still feels slow.&lt;/p&gt;
&lt;p&gt;NVFP4 clearly wins here. At ten parallel users the model stays at 22.90 tok/s/user. BF16 drops to 7.84. That is still readable, but for an agent flow it feels like someone is typing along by hand.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;FP8 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;28.65&lt;/td&gt;
&lt;td&gt;49.85&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55.55&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;12.19&lt;/td&gt;
&lt;td&gt;21.32&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30.97&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7.84&lt;/td&gt;
&lt;td&gt;15.26&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22.90&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For this workload NVFP4 is the logical default. FP8 is fine, but here you mostly give up speed without tail-latency playing the lead role.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run E: multi-turn, depth 4&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Multi-turn is closer to real use than one isolated prompt. Five turns per conversation, several conversations in parallel. That resembles an employee who does not ask one question, but keeps asking, corrects, and carries context along.&lt;/p&gt;
&lt;p&gt;Here I do not just want to see high throughput. I mostly want the server not to feel like it comes out of a cold start every turn. With ten conversations at once that becomes relevant: the context grows per conversation, the scheduler has to keep sharing, and the user expects the chat to keep running.&lt;/p&gt;
&lt;p&gt;This is the most important office run for me. Not because it is perfectly real, but because it comes closest to &quot;25 people use this spread across the day&quot;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;FP8 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 TTFT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;28.69&lt;/td&gt;
&lt;td&gt;49.72&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56.18&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;596 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;11.50&lt;/td&gt;
&lt;td&gt;20.87&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30.55&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1032 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7.68&lt;/td&gt;
&lt;td&gt;14.88&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21.58&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1359 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At ten parallel conversations NVFP4 sits at 21.58 tok/s/user. FP8 sits at 14.88. BF16 at 7.68. That last one works technically, but it no longer feels like a snappy chat. NVFP4 stays well above the line where you experience an answer as fluent.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run F: RAG mix with 8k prompt&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;RAG is usually not 25k context, but not a short chat either. This run uses an 8k prompt and 512 output tokens. Think four chunks of about 2k tokens, plus question and instruction.&lt;/p&gt;
&lt;p&gt;With RAG prefill counts more than in Run C. You push a sizeable slab of context into the model each time before anything comes back. After that you want enough decode left to make the answer usefully fast.&lt;/p&gt;
&lt;p&gt;So the question is: does quantization keep helping when the prompt gets heavier? Yes. NVFP4 stays clearly ahead, even at twenty users.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;FP8 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;12.50&lt;/td&gt;
&lt;td&gt;21.02&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;27.77&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8.11&lt;/td&gt;
&lt;td&gt;14.37&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.65&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;5.51&lt;/td&gt;
&lt;td&gt;9.82&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14.09&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At twenty users NVFP4 delivers 14.09 tok/s/user. BF16 sits at 5.51. For batch processing that can still work. For real-time RAG in an office BF16 feels tight, certainly when documents are messy and prompts get longer than you had hoped. They always do.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run G: short instruction, 4096 output tokens&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Run G resembles Run C, but pulls the output much further: 4096 tokens. This is the shape of agents that write out plans, generate code, make long analyses or summarize multiple files.&lt;/p&gt;
&lt;p&gt;For this kind of workload the first token is almost a side issue. If the answer is long, decode speed determines the experience. Ten seconds of difference at the start is annoying. Waiting on output for minutes is worse.&lt;/p&gt;
&lt;p&gt;NVFP4 stays strongest here. More important: it also stays above 25 tok/s/user at ten users. For local hardware on a desk machine that is simply usable.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;BF16 d/u&lt;/th&gt;
&lt;th&gt;FP8 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4 TTFT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;28.68&lt;/td&gt;
&lt;td&gt;49.75&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55.44&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;179 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;14.32&lt;/td&gt;
&lt;td&gt;25.56&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34.63&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;427 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9.51&lt;/td&gt;
&lt;td&gt;18.40&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25.18&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;363 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For agent flows this is fairly hard: BF16 is not broken, but you pay for every long output twice. First in memory, then in waiting time.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run H: open-loop office baseline&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;From here the interpretation changes. The previous runs push controlled batches through the model. Run H uses open-loop traffic: requests come in according to a Poisson distribution. So the server has to deal with arrivals that do not neatly wait for the previous one to finish.&lt;/p&gt;
&lt;p&gt;This resembles an office more. Not perfect, but better than everyone at once or fully sequential. The metrics are different too. TPOT tells how fast tokens come once it is your turn. TTFT P50 tells the normal experience. TTFT P99 tells what the unlucky one notices.&lt;/p&gt;
&lt;p&gt;Here FP8 gets interesting. NVFP4 wins the median and TPOT, but FP8 wins the tail. That is exactly why I do not want to end with &quot;NVFP4 is always better&quot;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Achieved RPS&lt;/td&gt;
&lt;td&gt;0.26&lt;/td&gt;
&lt;td&gt;0.28&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.29&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak concurrent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;42&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;1229 ms&lt;/td&gt;
&lt;td&gt;732 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;618 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;2996 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2008 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3235 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;203 ms&lt;/td&gt;
&lt;td&gt;74 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;39 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate tok/s&lt;/td&gt;
&lt;td&gt;1203&lt;/td&gt;
&lt;td&gt;1297&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1329&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That peak concurrent of BF16 looks good on paper, but it is not. The queue grows because BF16 drains it less quickly. NVFP4 processes faster, so fewer requests are open at the same time. That is not lower capacity, that is less of a line.&lt;/p&gt;
&lt;p&gt;The real choice is between NVFP4 and FP8. Want the best median and fastest output, then NVFP4. Want the cleanest P99 on this workload, then FP8.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run I: ShareGPT replay&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;ShareGPT replay is messier and therefore useful. Real conversations have varying lengths, follow-up questions, short answers, long answers and prompts that have not been neatly smoothed out by a benchmark author.&lt;/p&gt;
&lt;p&gt;This is the run I trust most for chat feel. Not for company documents, but for the question: how does this feel when several people hold conversations throughout the day?&lt;/p&gt;
&lt;p&gt;The pattern from Run H holds. NVFP4 is fastest for the average user. FP8 has the better P99.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Peak concurrent&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;433 ms&lt;/td&gt;
&lt;td&gt;220 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;157 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;713 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;422 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1361 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;118 ms&lt;/td&gt;
&lt;td&gt;38 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;NVFP4 feels instant for most users: 157 ms TTFT P50 and 26 ms TPOT P50. But the P99 is 1361 ms, where FP8 stays at 422 ms. That is a hefty difference.&lt;/p&gt;
&lt;p&gt;For an internal chat where a single slower request is no disaster, I pick NVFP4. For a product UI with a hard latency promise I would take FP8 more seriously.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Run J: Monday morning peak&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Run J is oversubscribe. The target is 1.5 requests per second with a concurrency cap of 25. This is not the normal workday. This is the test for what happens when demand is bigger than the server can neatly keep up with.&lt;/p&gt;
&lt;p&gt;With oversubscribe I look at achieved RPS first. Not at configured RPS, because that is the same for everyone. The question is how many requests the server actually processes while it is under pressure.&lt;/p&gt;
&lt;p&gt;There NVFP4 wins clearly. FP8 keeps the tail cleaner, but NVFP4 gets much more work through the machine.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Configured RPS&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Achieved RPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;td&gt;0.43&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.58&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak concurrent&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;1130 ms&lt;/td&gt;
&lt;td&gt;757 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;687 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;5184 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3388 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4462 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;197 ms&lt;/td&gt;
&lt;td&gt;112 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;82 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate tok/s&lt;/td&gt;
&lt;td&gt;1118&lt;/td&gt;
&lt;td&gt;1951&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2622&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Concretely: NVFP4 processes about 35 requests per minute. BF16 about 15. That is the difference between a queue that slowly drains and a queue that makes users wonder whether they should click again. Do not click. That second click never helps.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;The three precisions side by side&lt;/h2&gt;
&lt;p&gt;If I have to pick one realistic chat run, I take ShareGPT replay. There you see the distinction cleanest: NVFP4 wins the normal experience, FP8 wins the tail, BF16 takes part but convinces nowhere.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;NVFP4&lt;/th&gt;
&lt;th&gt;Best choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;118 ms&lt;/td&gt;
&lt;td&gt;38 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;433 ms&lt;/td&gt;
&lt;td&gt;220 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;157 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;713 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;422 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1361 ms&lt;/td&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak concurrent&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Achieved RPS&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;tie&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;With oversubscribe the difference gets harder:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;th&gt;FP8&lt;/th&gt;
&lt;th&gt;NVFP4&lt;/th&gt;
&lt;th&gt;Best choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Achieved RPS&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;td&gt;0.43&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.58&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;1130 ms&lt;/td&gt;
&lt;td&gt;757 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;687 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;5184 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3388 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4462 ms&lt;/td&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;197 ms&lt;/td&gt;
&lt;td&gt;112 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;82 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate tok/s&lt;/td&gt;
&lt;td&gt;1118&lt;/td&gt;
&lt;td&gt;1951&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2622&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That makes the choice more practical than I thought beforehand. NVFP4 is the default if you want throughput and normal user experience. FP8 is the choice if you find P99 more important than median. BF16 is the baseline you use to check whether quantization wrecks your accuracy.&lt;/p&gt;
&lt;h2&gt;Why FP8 wins the P99&lt;/h2&gt;
&lt;p&gt;My hypothesis: NVFP4 gives vLLM more memory room and therefore more batching room. That raises throughput and lowers TPOT, but individual requests can sometimes wait longer before they fall neatly into a batch.&lt;/p&gt;
&lt;p&gt;FP8 has less headroom than NVFP4, but still enough for this workload. That makes the scheduler seem more predictable. Less aggressive, less fast in median, better in the tail.&lt;/p&gt;
&lt;p&gt;BF16 has the worst of both worlds: large weights, less KV-cache headroom and lower decode. The queue gets fuller, but not because the server can handle so much at once. It just gets through it less quickly.&lt;/p&gt;
&lt;p&gt;I want to dig into this further with scheduler settings and prefix caching. The raw numbers and the test definitions are in the &lt;a href=&quot;/en/arena/&quot;&gt;arena&lt;/a&gt; so I can hold future runs against the same bar.&lt;/p&gt;
&lt;h2&gt;Comparison with Gemma-4-26B-A4B&lt;/h2&gt;
&lt;p&gt;Nemotron-NVFP4 is single-user almost twice as fast as Gemma-NVFP4. At multi-user the difference gets smaller, but it usually stays positive.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Gemma-NVFP4 d/u&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Nemotron-NVFP4 d/u&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pp4096 c=1&lt;/td&gt;
&lt;td&gt;30.01&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60.30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.0×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pp8192 c=1&lt;/td&gt;
&lt;td&gt;29.35&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55.72&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pp25000 c=1&lt;/td&gt;
&lt;td&gt;28.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;54.98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pp4096 c=10&lt;/td&gt;
&lt;td&gt;17.05&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.69&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.2×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pp25000 c=10&lt;/td&gt;
&lt;td&gt;7.61&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.99&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.7×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That pattern matches what the model is. Nemotron has 3B active params, Gemma 4B active params. At single-user that helps a lot. At multi-user the bottleneck shifts toward memory bandwidth and scheduling, and then the difference gets smaller.&lt;/p&gt;
&lt;h2&gt;What this means for on-prem AI&lt;/h2&gt;
&lt;p&gt;My default choice for this Spark is &lt;strong&gt;NVFP4&lt;/strong&gt;. Not because 4 bit is principally nicer, but because the numbers on these workloads carry it: highest throughput, fastest median, lowest TPOT, smallest footprint.&lt;/p&gt;
&lt;p&gt;I pick &lt;strong&gt;FP8&lt;/strong&gt; when tail-latency matters more than median. Think of a UI where you want to be able to say that 99 percent of requests start within a certain bound. In Run H, I and J, FP8 consistently wins on P99 TTFT.&lt;/p&gt;
&lt;p&gt;I pick &lt;strong&gt;BF16&lt;/strong&gt; only as a baseline or for accuracy-critical validation. Not as a production default. For that it is too expensive on the Spark: roughly three times as much memory as NVFP4 and roughly half the speed.&lt;/p&gt;
&lt;p&gt;For a 25-person office with chat and RAG-like workload I would run NVFP4, with a custom eval suite alongside it. For an external chatbot with a tight latency promise I would test FP8. For BF16 I would mostly keep a short run to see what quantization changes in substance.&lt;/p&gt;
&lt;h2&gt;What these runs do not say&lt;/h2&gt;
&lt;p&gt;No accuracy tests. FP8 and NVFP4 can differ in substance from BF16. For production you have to measure that on your own documents, your own prompts and your own error tolerance.&lt;/p&gt;
&lt;p&gt;No multimodal benchmarks. Nemotron-3-Nano-Omni is multimodal-aware, but these runs are text-only. Vision and audio stay out of frame here.&lt;/p&gt;
&lt;p&gt;No comparison with dense models. This is an MoE model. Dense models feel different, especially in output speed and how vLLM handles them.&lt;/p&gt;
&lt;p&gt;No definitive scheduler conclusion. The FP8-vs-NVFP4 tail is interesting enough to test separately with other batching and scheduling settings.&lt;/p&gt;
&lt;h2&gt;Where I land&lt;/h2&gt;
&lt;p&gt;The precision choice is not a detail. On the Spark it determines whether the same machine feels like a local experiment or like something you can hand to colleagues without explaining it every five minutes.&lt;/p&gt;
&lt;p&gt;NVFP4 in many runs doubles the usable experience compared to BF16. FP8 is less spectacular, but more predictable in the tail. BF16 stays useful as a reference point, not as an end station.&lt;/p&gt;
&lt;p&gt;The practical lesson from these three posts together: follow the vendor recipes, run the stable image and measure your own workload. Do not tinker yourself unless you have a good reason for it. With Gemma I had a reason. In hindsight it was mediocre.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Gemma-4 on the DGX Spark: the price of context</title>
    <id>https://djangodevreng.nl/en/blog/gemma-4-dgx-spark-benchmarks/</id>
    <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/gemma-4-dgx-spark-benchmarks/"/>
    <published>2026-05-01T00:00:00.000Z</published>
    <updated>2026-05-02T00:00:00.000Z</updated>
    <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
    <category term="on-prem"/>
    <summary>Nine benchmarks of Gemma-4-26B-A4B-it on the DGX Spark with llama-benchy and vLLM. Decode holds up; prefill and queueing decide how it feels.</summary>
    <content type="html">&lt;p&gt;I wanted to know how well a DGX Spark holds up as a local AI machine for an office environment.&lt;/p&gt;
&lt;p&gt;Not in theory. Just: load Gemma-4-26B-A4B-it into vLLM, throw llama-benchy at it, make context windows bigger, output longer, concurrency higher, add multi-turn, and watch where it stays pleasant and where the wait starts to hurt. And once that story started taking shape, a second question came up: what if I stop testing in lockstep and let requests arrive organically, the way they would in a real office? For that I pulled in vLLM&apos;s own benchmark suite, which does what llama-benchy does not: Poisson arrivals, percentiles, real conversation data. How I measure all of this is in the &lt;a href=&quot;/en/arena/methodology/&quot;&gt;methodology&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The short version: for normal office use this looks good. Short to medium prompts, longer outputs, and even conversations across multiple turns keep feeling fast, even with ten users at once. With large context windows the problem is not tokens per second, but how long someone stares at an empty chat window before the first token arrives. And if you really overload the machine, it does not scale, it queues.&lt;/p&gt;
&lt;p&gt;That makes this not a &quot;can the DGX Spark do it or not&quot; story. It makes it a workload story. Nine tests, two methods, one machine. It is one of the build logs under the guide &lt;a href=&quot;/en/dgx-spark/&quot;&gt;running LLMs on the DGX Spark&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Why this test&lt;/h2&gt;
&lt;p&gt;With on-prem AI you quickly end up talking about privacy, keeping data closer, and being less dependent on hosted models. That is all true, but eventually a flatter question follows.&lt;/p&gt;
&lt;p&gt;Can the machine handle it?&lt;/p&gt;
&lt;p&gt;A local model that neatly answers one demo prompt is nice. But production rarely looks like that. There you have multiple users, larger context, agent flows, tool-calls, retries, and sometimes someone who pastes half a novel into a ticket.&lt;/p&gt;
&lt;p&gt;So I did not want to measure only tokens per second on one prompt. I wanted to see what happens when you load the machine from different angles: from &quot;ten users, short prompts, long answers&quot; to &quot;ten users, five-turn conversations, growing memory&quot; to &quot;requests that arrive organically like in a real office, not all at once and not all the same size&quot;.&lt;/p&gt;
&lt;p&gt;For these benchmarks I tested one model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;google/gemma-4-26B-A4B-it&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;BF16&lt;/li&gt;
&lt;li&gt;DGX Spark, NVIDIA GB10, 128 GB unified memory&lt;/li&gt;
&lt;li&gt;vLLM as OpenAI-compatible endpoint&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dense comes later. MoE vs dense too. This piece is only about Gemma-4-26B-A4B-it on the DGX Spark. This run is on BF16; what happens to the same Gemma-4 when you &lt;a href=&quot;/en/blog/gemma-4-nvfp4-vs-bf16-dgx-spark/&quot;&gt;quantize to NVFP4&lt;/a&gt; is a separate story.&lt;/p&gt;
&lt;h2&gt;What I expected up front&lt;/h2&gt;
&lt;p&gt;My expectation was simple: MoE would stay reasonably good under concurrent requests, but I thought the DGX Spark would hit its limits faster once the context grew large.&lt;/p&gt;
&lt;p&gt;Especially at 25k context.&lt;/p&gt;
&lt;p&gt;Context is expensive. You pay not only for the prompt coming in, but also for the KV-cache that vLLM has to keep around. Multiply that by multiple users and it suddenly becomes a memory problem and a queueing problem.&lt;/p&gt;
&lt;p&gt;I was curious about five things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;does decode stay usable as context grows?&lt;/li&gt;
&lt;li&gt;how much does prefill add to the time to first token?&lt;/li&gt;
&lt;li&gt;what happens when the prompt is short but the output long?&lt;/li&gt;
&lt;li&gt;how does it behave with multi-turn conversations, where context thickens per turn?&lt;/li&gt;
&lt;li&gt;and (added only later) what does all of this look like when requests do not come in lockstep, but organically?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last question turned out to be half the story.&lt;/p&gt;
&lt;h2&gt;The test setup&lt;/h2&gt;
&lt;p&gt;The server ran in Docker with the official vLLM image:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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 google/gemma-4-26B-A4B-it \
  --served-model-name gemma-4-26b-a4b-bf16 \
  --max-model-len 131072 \
  --gpu-memory-utilization 0.95 \
  --kv-cache-dtype fp8 \
  --limit-mm-per-prompt &apos;{&quot;image&quot;:0,&quot;audio&quot;:0}&apos; \
  --async-scheduling \
  --no-enable-prefix-caching \
  --host 0.0.0.0 \
  --port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few details matter.&lt;/p&gt;
&lt;p&gt;Prefix caching is deliberately off. I wanted to see the raw prefill cost first, not a benchmark that looks nicer because the prompts resemble each other.&lt;/p&gt;
&lt;p&gt;The KV-cache runs on fp8. Without it, 128k context with multiple concurrent requests quickly becomes a memory exercise that gets you nowhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;All nine tests below use exactly this server config.&lt;/strong&gt; No restart, no mid-run change. What varies is the workload: prompt size, output size, concurrency, depth, and for the open-loop tests also arrival rate and burstiness.&lt;/p&gt;
&lt;p&gt;What the Spark makes of this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model weights (BF16)&lt;/td&gt;
&lt;td&gt;~48 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV-cache headroom (fp8)&lt;/td&gt;
&lt;td&gt;~65 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theoretical parallel @ 128k&lt;/td&gt;
&lt;td&gt;~4 requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theoretical parallel @ 8k&lt;/td&gt;
&lt;td&gt;~50 requests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At full context per request, memory is tight. In practice no test uses 128k at once per user, so the bottleneck shifts to prefill compute and scheduler batching. We see that below.&lt;/p&gt;
&lt;h2&gt;Run A: making the context bigger&lt;/h2&gt;
&lt;p&gt;The first run grew the context from 4k to 25k. Concurrency went along from 1 to 5 and 10. Closed-loop, so N users in lockstep.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx llama-benchy \
  --base-url http://localhost:8000/v1 \
  --model gemma-4-26b-a4b-bf16 \
  --pp 4096 8192 16384 25000 \
  --tg 256 \
  --depth 0 \
  --concurrency 1 5 10 \
  --runs 3 \
  --latency-mode generation \
  --format md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pp&lt;/code&gt; is prefill, that is how many prompt tokens go in. &lt;code&gt;tg&lt;/code&gt; is decode, that is how many tokens the model generates afterwards. llama-benchy reports mean ± stddev. No p95. That is important to remember, because with latency you otherwise quickly fool yourself into optimism.&lt;/p&gt;
&lt;p&gt;This is the summary from Run A:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Prefill total&lt;/th&gt;
&lt;th&gt;Decode/user&lt;/th&gt;
&lt;th&gt;Decode total&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3677.85 ± 1259.27 tok/s&lt;/td&gt;
&lt;td&gt;24.08 ± 0.02 tok/s&lt;/td&gt;
&lt;td&gt;24.08 ± 0.02 tok/s&lt;/td&gt;
&lt;td&gt;1.37 ± 0.52s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5722.96 ± 94.70 tok/s&lt;/td&gt;
&lt;td&gt;12.55 ± 0.49 tok/s&lt;/td&gt;
&lt;td&gt;57.07 ± 2.64 tok/s&lt;/td&gt;
&lt;td&gt;2.29 ± 0.82s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5475.53 ± 888.14 tok/s&lt;/td&gt;
&lt;td&gt;9.48 ± 0.73 tok/s&lt;/td&gt;
&lt;td&gt;84.40 ± 3.08 tok/s&lt;/td&gt;
&lt;td&gt;4.46 ± 2.38s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;6121.87 ± 62.31 tok/s&lt;/td&gt;
&lt;td&gt;23.69 ± 0.02 tok/s&lt;/td&gt;
&lt;td&gt;23.69 ± 0.02 tok/s&lt;/td&gt;
&lt;td&gt;1.39 ± 0.01s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5444.57 ± 12.82 tok/s&lt;/td&gt;
&lt;td&gt;11.48 ± 0.92 tok/s&lt;/td&gt;
&lt;td&gt;49.42 ± 1.60 tok/s&lt;/td&gt;
&lt;td&gt;4.34 ± 1.91s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5478.98 ± 11.48 tok/s&lt;/td&gt;
&lt;td&gt;8.52 ± 1.10 tok/s&lt;/td&gt;
&lt;td&gt;67.72 ± 0.91 tok/s&lt;/td&gt;
&lt;td&gt;7.99 ± 4.03s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4607.64 ± 23.05 tok/s&lt;/td&gt;
&lt;td&gt;23.34 ± 0.05 tok/s&lt;/td&gt;
&lt;td&gt;23.34 ± 0.05 tok/s&lt;/td&gt;
&lt;td&gt;3.42 ± 0.00s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4466.35 ± 27.19 tok/s&lt;/td&gt;
&lt;td&gt;10.05 ± 1.75 tok/s&lt;/td&gt;
&lt;td&gt;38.41 ± 0.12 tok/s&lt;/td&gt;
&lt;td&gt;10.43 ± 4.69s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4453.92 ± 18.19 tok/s&lt;/td&gt;
&lt;td&gt;6.79 ± 1.62 tok/s&lt;/td&gt;
&lt;td&gt;45.76 ± 0.43 tok/s&lt;/td&gt;
&lt;td&gt;18.92 ± 9.43s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3621.25 ± 18.50 tok/s&lt;/td&gt;
&lt;td&gt;22.75 ± 0.08 tok/s&lt;/td&gt;
&lt;td&gt;22.75 ± 0.08 tok/s&lt;/td&gt;
&lt;td&gt;6.39 ± 0.05s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3561.78 ± 9.23 tok/s&lt;/td&gt;
&lt;td&gt;8.46 ± 2.36 tok/s&lt;/td&gt;
&lt;td&gt;27.93 ± 0.08 tok/s&lt;/td&gt;
&lt;td&gt;19.63 ± 8.87s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;3565.35 ± 8.21 tok/s&lt;/td&gt;
&lt;td&gt;5.40 ± 2.00 tok/s&lt;/td&gt;
&lt;td&gt;30.73 ± 0.12 tok/s&lt;/td&gt;
&lt;td&gt;35.67 ± 18.00s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/run-a-ttfr.webp&quot; width=&quot;1425&quot; height=&quot;878&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Run A: TTFT vs context, one line per concurrent users (1, 5, 10). TTFT climbs from ~1.4 seconds at 4k to 36 seconds at 25k context with 10 users.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;Run A: Wait time for the first token, per concurrent users. Double the prompt and you double the wait.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/run-a-decode.webp&quot; width=&quot;1425&quot; height=&quot;878&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Run A: Decode speed per user vs context. At c=1 decode stays between 22.7 and 24.1 tokens per second, at c=10 it drops from 9.5 to 5.4 tokens per second.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;Run A: Decode per user. With one user it stays almost flat; only with multiple users and large context does it collapse.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;h2&gt;Run B: holding 25k context, concurrency up&lt;/h2&gt;
&lt;p&gt;After that I ran the same 25k context harder. No longer varying the context, only adding users.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx llama-benchy \
  --base-url http://localhost:8000/v1 \
  --model gemma-4-26b-a4b-bf16 \
  --pp 25000 \
  --tg 256 \
  --depth 0 \
  --concurrency 5 10 20 \
  --runs 3 \
  --latency-mode generation \
  --exit-on-first-fail \
  --format md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No OOM. No crash. The DGX Spark survived 20 concurrent requests at 25k context.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Prefill total&lt;/th&gt;
&lt;th&gt;Decode/user&lt;/th&gt;
&lt;th&gt;Decode total&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3559.17 ± 6.72 tok/s&lt;/td&gt;
&lt;td&gt;8.51 ± 2.40 tok/s&lt;/td&gt;
&lt;td&gt;27.88 ± 0.05 tok/s&lt;/td&gt;
&lt;td&gt;19.86 ± 9.00s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;3569.77 ± 2.99 tok/s&lt;/td&gt;
&lt;td&gt;5.37 ± 1.99 tok/s&lt;/td&gt;
&lt;td&gt;30.68 ± 0.09 tok/s&lt;/td&gt;
&lt;td&gt;35.44 ± 17.95s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;3563.64 ± 8.78 tok/s&lt;/td&gt;
&lt;td&gt;3.16 ± 1.41 tok/s&lt;/td&gt;
&lt;td&gt;32.26 ± 0.10 tok/s&lt;/td&gt;
&lt;td&gt;67.37 ± 36.44s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/run-b-prefill-wall.webp&quot; width=&quot;1522&quot; height=&quot;843&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Run B: TTFT grows linearly with concurrency: 19.9s at 5 users, 35.4s at 10, 67.4s at 20. Aggregate decode sticks around 30 tok/s.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;Run B: Aggregate decode sticks at ~30 tok/s; all the extra wait goes into TTFT.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;This is the stress edge of the benchmark. Aggregate decode sticks around 30 tok/s, regardless of whether you put 5, 10 or 20 users on it. Per user it drops from 8.51 to 3.16 tok/s. But the real problem is TTFT: at 20 users the average request waits 67 seconds before the first token arrives. The server is not broken then. The workload just no longer fits a realtime chat expectation.&lt;/p&gt;
&lt;h2&gt;Run C: short prompt, long output&lt;/h2&gt;
&lt;p&gt;Run C flipped the shape. Not 25k context with short output, but 1024 prompt tokens and 1024 output tokens.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Prefill total&lt;/th&gt;
&lt;th&gt;Decode/user&lt;/th&gt;
&lt;th&gt;Decode total&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4627.12 ± 374.91 tok/s&lt;/td&gt;
&lt;td&gt;23.86 ± 0.03 tok/s&lt;/td&gt;
&lt;td&gt;23.86 ± 0.03 tok/s&lt;/td&gt;
&lt;td&gt;0.31 ± 0.02s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5701.55 ± 561.36 tok/s&lt;/td&gt;
&lt;td&gt;13.59 ± 1.05 tok/s&lt;/td&gt;
&lt;td&gt;54.67 ± 4.90 tok/s&lt;/td&gt;
&lt;td&gt;0.76 ± 0.11s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;6346.87 ± 64.52 tok/s&lt;/td&gt;
&lt;td&gt;10.92 ± 0.73 tok/s&lt;/td&gt;
&lt;td&gt;86.46 ± 1.74 tok/s&lt;/td&gt;
&lt;td&gt;1.26 ± 0.40s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/run-c-grouped.webp&quot; width=&quot;1227&quot; height=&quot;777&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Run C: per-user decode drops from 23.9 (c=1) to 10.9 (c=10), aggregate decode climbs to 86.5 tok/s.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;Run C: short prompt, long output. Aggregate decode scales neatly to 86 tok/s, per-user stays comfortably readable.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;At ten users at once, TTFT stays at 1.3 seconds. That feels like chat.&lt;/p&gt;
&lt;h2&gt;Run G: even longer output&lt;/h2&gt;
&lt;p&gt;Runs A, B and C showed enough to make the &quot;decode is stable, prefill decides the wait&quot; story plausible. But one scenario stayed open: what if the output is much longer still? An agent generating code. A tool-call with structured output. A long summary.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Prefill total&lt;/th&gt;
&lt;th&gt;Decode/user&lt;/th&gt;
&lt;th&gt;Decode total&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1993.94 ± 262.05 tok/s&lt;/td&gt;
&lt;td&gt;24.17 ± 0.02 tok/s&lt;/td&gt;
&lt;td&gt;24.17 ± 0.02 tok/s&lt;/td&gt;
&lt;td&gt;0.24 ± 0.01s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3048.28 ± 496.15 tok/s&lt;/td&gt;
&lt;td&gt;14.32 ± 2.18 tok/s&lt;/td&gt;
&lt;td&gt;46.11 ± 11.57 tok/s&lt;/td&gt;
&lt;td&gt;0.38 ± 0.07s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4800.80 ± 50.75 tok/s&lt;/td&gt;
&lt;td&gt;11.75 ± 0.68 tok/s&lt;/td&gt;
&lt;td&gt;83.77 ± 4.04 tok/s&lt;/td&gt;
&lt;td&gt;0.48 ± 0.01s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/run-g-grouped.webp&quot; width=&quot;1227&quot; height=&quot;777&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Run G: per-user decode 24.2 (c=1), 14.3 (c=5), 11.8 (c=10); aggregate 24.2, 46.1, 83.8 tok/s.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;Run G: 4k output: long generations are only longer, not slower. Per-user sits close to Run C.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;Decode/user over 4096 tokens barely drops compared to C&apos;s 1024 tokens. At c=1 it is 24.17 (G) vs 23.86 (C). At c=10 it is 11.75 (G) vs 10.92 (C). Long generations do not compound, they just take proportionally longer. And TTFT is lowest here: under half a second at ten users at once.&lt;/p&gt;
&lt;h2&gt;Run F: medium context, more users&lt;/h2&gt;
&lt;p&gt;Between Run C (1k context) and Run B (25k context) sat a gap that is closer to reality. A typical RAG flow with four chunks of ~2k tokens comes out around 8k.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Prefill total&lt;/th&gt;
&lt;th&gt;Decode/user&lt;/th&gt;
&lt;th&gt;Decode total&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5439.51 ± 32.60 tok/s&lt;/td&gt;
&lt;td&gt;12.11 ± 0.51 tok/s&lt;/td&gt;
&lt;td&gt;55.21 ± 1.49 tok/s&lt;/td&gt;
&lt;td&gt;4.32 ± 1.90s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5466.71 ± 15.65 tok/s&lt;/td&gt;
&lt;td&gt;9.31 ± 0.77 tok/s&lt;/td&gt;
&lt;td&gt;78.36 ± 1.61 tok/s&lt;/td&gt;
&lt;td&gt;7.99 ± 4.02s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;5532.74 ± 5.39 tok/s&lt;/td&gt;
&lt;td&gt;6.05 ± 0.62 tok/s&lt;/td&gt;
&lt;td&gt;97.35 ± 3.50 tok/s&lt;/td&gt;
&lt;td&gt;14.61 ± 7.72s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/run-f-ttfr.webp&quot; width=&quot;1522&quot; height=&quot;843&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Run F: 8k context. TTFT climbs from 4.3s (c=5) to 8.0s (c=10) to 14.6s (c=20); aggregate decode reaches 97.4 tok/s.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;Run F: 8k context. TTFT grows linearly with concurrency, aggregate decode keeps scaling to almost 100 tok/s.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;Three observations.&lt;/p&gt;
&lt;p&gt;Prefill throughput sits at a flat 5.5k tok/s, regardless of whether it is 5, 10 or 20 users. At 8k context the machine is already saturated at the prefill level. Aggregate decode keeps scaling: in Run B (25k) this plateaued at ~30 t/s, here it runs up to 97.4 t/s. And most importantly: TTFT at 8k context is roughly a quarter of what it is at 25k. Same concurrency, same machine, different prompt size.&lt;/p&gt;
&lt;h2&gt;Run E: multi-turn as realistic office work&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--depth 4&lt;/code&gt; means: five turns in a row per request (initial + four follow-ups). Concurrency at 10 means: ten such conversations in parallel.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Prefill total&lt;/th&gt;
&lt;th&gt;Decode/user&lt;/th&gt;
&lt;th&gt;Decode total&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4716.21 ± 542.88 tok/s&lt;/td&gt;
&lt;td&gt;23.97 ± 0.10 tok/s&lt;/td&gt;
&lt;td&gt;23.97 ± 0.10 tok/s&lt;/td&gt;
&lt;td&gt;0.53 ± 0.06s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5693.39 ± 128.08 tok/s&lt;/td&gt;
&lt;td&gt;13.07 ± 0.16 tok/s&lt;/td&gt;
&lt;td&gt;59.48 ± 2.26 tok/s&lt;/td&gt;
&lt;td&gt;1.32 ± 0.39s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;6096.81 ± 56.92 tok/s&lt;/td&gt;
&lt;td&gt;10.43 ± 0.35 tok/s&lt;/td&gt;
&lt;td&gt;92.42 ± 3.33 tok/s&lt;/td&gt;
&lt;td&gt;2.13 ± 0.83s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/run-e-multiturn.webp&quot; width=&quot;1242&quot; height=&quot;777&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Run E: multi-turn. Per-user 24.0/13.1/10.4 tok/s, aggregate 24.0/59.5/92.4 tok/s, highest aggregate of all closed-loop runs.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;Run E: multi-turn (depth = 4) at 2k starting context. Aggregate of 92 tok/s is the highest number across all six closed-loop runs.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;Three things stood out that I had not expected up front.&lt;/p&gt;
&lt;p&gt;Per-user decode with multi-turn is identical to single-turn. Multi-turn does not make the tokens slower, only the number of prefills goes up. Aggregate decode at c=10 is 92.42 t/s, the highest of any closed-loop run. With multi-turn, vLLM gets a denser stream of dependent requests fed to it, and can batch those more efficiently than ten separate single-shot prompts. And TTFT at c=10 averages 2.13 seconds across all five turns. Under three seconds still feels like chat.&lt;/p&gt;
&lt;h2&gt;What the six closed-loop runs show together&lt;/h2&gt;
&lt;p&gt;One table that puts everything at c=10 side by side:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Depth&lt;/th&gt;
&lt;th&gt;TTFT (c=10)&lt;/th&gt;
&lt;th&gt;Decode/user (c=10)&lt;/th&gt;
&lt;th&gt;Aggregate decode (c=10)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;4096&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.48s&lt;/td&gt;
&lt;td&gt;11.75 t/s&lt;/td&gt;
&lt;td&gt;83.8 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.26s&lt;/td&gt;
&lt;td&gt;10.92 t/s&lt;/td&gt;
&lt;td&gt;86.5 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;2048&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2.13s&lt;/td&gt;
&lt;td&gt;10.43 t/s&lt;/td&gt;
&lt;td&gt;92.4 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7.99s&lt;/td&gt;
&lt;td&gt;9.31 t/s&lt;/td&gt;
&lt;td&gt;78.4 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;16384&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;18.92s&lt;/td&gt;
&lt;td&gt;6.79 t/s&lt;/td&gt;
&lt;td&gt;45.8 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A/B&lt;/td&gt;
&lt;td&gt;25000&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;35.67s&lt;/td&gt;
&lt;td&gt;5.40 t/s&lt;/td&gt;
&lt;td&gt;30.7 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/summary-c10.webp&quot; width=&quot;1569&quot; height=&quot;944&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Scatter of all six closed-loop runs at c=10. Y-axis decode/user (5 to 12 tok/s), X-axis TTFT logarithmic (0.5s to 49s). G and C top left, A-25k bottom right.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;All six closed-loop runs at 10 concurrent users. Decode per user barely moves up to 8k context. TTFT moves everywhere.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;Two patterns jump out.&lt;/p&gt;
&lt;p&gt;Decode/user barely moves up to 8k context. Between Run G and Run F there is a factor of 32 in prompt size and a factor of 8 in output size. Yet decode/user there sits between 9.3 and 11.8 tok/s. Only at 16k+ does that band collapse.&lt;/p&gt;
&lt;p&gt;TTFT moves everywhere and is almost a function of prompt size alone. Double the prompt and the TTFT roughly doubles along with it. Output size and depth matter almost nothing for TTFT.&lt;/p&gt;
&lt;p&gt;That is the closed-loop conclusion. It holds, and it tells a real part of the story. But there is a gap in it.&lt;/p&gt;
&lt;h2&gt;But these are synthetic tests&lt;/h2&gt;
&lt;p&gt;The six runs above test capacity. &lt;em&gt;Ceilings.&lt;/em&gt; All in the same shape: N users in lockstep, all the same prompt size, all hitting send buttons at the same time. That is a fine way to measure where it breaks. It is a bad way to measure how a real office feels.&lt;/p&gt;
&lt;p&gt;Because a real office has 25 employees, of whom on average a few are doing something at the same time. One colleague asks a short question. Another is mid-RAG with 8k context. The third is in turn 4 of a conversation. And requests do not arrive in lockstep. They arrive as a Poisson process with the occasional burst, because someone just finished an email and three colleagues all want coffee at once.&lt;/p&gt;
&lt;p&gt;That is what &lt;strong&gt;vLLM&apos;s own &lt;code&gt;vllm bench serve&lt;/code&gt;&lt;/strong&gt; can do and llama-benchy cannot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Open-loop&lt;/strong&gt; with arrival rate. Dispatch requests according to a Poisson or Gamma distribution, instead of lockstep.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Percentiles.&lt;/strong&gt; P50, P90, P95, P99 on TTFT, TPOT (time per output token), ITL (inter-token latency) and E2E. No more mean ± stddev.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Realistic datasets.&lt;/strong&gt; ShareGPT replay of 94k+ real conversations with naturally varying prompt lengths and multi-turn structure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mixed workloads.&lt;/strong&gt; Sample prompts from a distribution instead of testing one fixed shape.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three tests below, same server (no restart), but with those other glasses on.&lt;/p&gt;
&lt;h2&gt;Test H: realistic office baseline&lt;/h2&gt;
&lt;p&gt;The scenario: 25 people active on average, each sends a prompt roughly once per 1-2 minutes, prompts vary widely in length. Arrivals are slightly clumpy.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec vllm-bench vllm bench serve \
  --backend openai-chat \
  --base-url http://localhost:8000 \
  --endpoint /v1/chat/completions \
  --model google/gemma-4-26B-A4B-it \
  --tokenizer google/gemma-4-26B-A4B-it \
  --served-model-name gemma-4-26b-a4b-bf16 \
  --dataset-name random \
  --random-input-len 4000 \
  --random-output-len 500 \
  --random-range-ratio 0.9 \
  --num-prompts 200 \
  --request-rate 0.3 \
  --burstiness 0.7 \
  --percentile-metrics ttft,tpot,itl,e2el \
  --metric-percentiles 50,90,95,99 \
  --seed 42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;--random-range-ratio 0.9&lt;/code&gt;, input lengths vary from 399 to 7600 tokens, outputs from 49 to 950. &lt;code&gt;--burstiness 0.7&lt;/code&gt; is slightly clumpier than pure Poisson. People often hit enter in little bursts, not like a metronome. Target rate of 0.3 req/s = ~18 prompts/min across 25 users.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Successful requests&lt;/td&gt;
&lt;td&gt;200 / 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Achieved RPS&lt;/td&gt;
&lt;td&gt;0.27 (target 0.30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Peak concurrent requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total token throughput&lt;/td&gt;
&lt;td&gt;1215 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;P50&lt;/th&gt;
&lt;th&gt;P90&lt;/th&gt;
&lt;th&gt;P95&lt;/th&gt;
&lt;th&gt;P99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTFT (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1395&lt;/td&gt;
&lt;td&gt;1286&lt;/td&gt;
&lt;td&gt;2284&lt;/td&gt;
&lt;td&gt;2644&lt;/td&gt;
&lt;td&gt;3316&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TPOT (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;177&lt;/td&gt;
&lt;td&gt;182&lt;/td&gt;
&lt;td&gt;193&lt;/td&gt;
&lt;td&gt;202&lt;/td&gt;
&lt;td&gt;214&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E2E (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;85921&lt;/td&gt;
&lt;td&gt;85306&lt;/td&gt;
&lt;td&gt;150192&lt;/td&gt;
&lt;td&gt;162375&lt;/td&gt;
&lt;td&gt;171351&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The median user gets the first token in 1.29s. Still feels like chat. The tail stays within bounds: P99 waits 3.3 seconds, comfortably under twice the average.&lt;/p&gt;
&lt;p&gt;And look at peak concurrent: &lt;strong&gt;36&lt;/strong&gt;. At a target rate of just 0.3 req/s. No closed-loop run came near that. The Poisson burstiness alone, combined with an average response time of ~86 seconds, produces peaks heavier than any Run B stress test had. That is the thing closed-loop literally cannot show.&lt;/p&gt;
&lt;h2&gt;Test I: real conversations (ShareGPT replay)&lt;/h2&gt;
&lt;p&gt;Identical arrival pattern to Test H, but now with 250 real multi-turn conversations from ShareGPT V3 as prompts. Some are 1 turn of 200 tokens, others are 15 turns with ever-growing context.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec vllm-bench vllm bench serve \
  ... \
  --dataset-name sharegpt \
  --dataset-path /tmp/ShareGPT_V3.json \
  --num-prompts 250 \
  --request-rate 0.3 \
  --burstiness 0.7
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Successful requests&lt;/td&gt;
&lt;td&gt;250 / 250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Achieved RPS&lt;/td&gt;
&lt;td&gt;0.30 (target 0.30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Peak concurrent requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total token throughput&lt;/td&gt;
&lt;td&gt;133 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;P50&lt;/th&gt;
&lt;th&gt;P90&lt;/th&gt;
&lt;th&gt;P95&lt;/th&gt;
&lt;th&gt;P99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTFT (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;376&lt;/td&gt;
&lt;td&gt;353&lt;/td&gt;
&lt;td&gt;469&lt;/td&gt;
&lt;td&gt;509&lt;/td&gt;
&lt;td&gt;637&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TPOT (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;117&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;135&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E2E (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;19600&lt;/td&gt;
&lt;td&gt;10923&lt;/td&gt;
&lt;td&gt;49525&lt;/td&gt;
&lt;td&gt;63036&lt;/td&gt;
&lt;td&gt;82596&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is a different universe than Test H. &lt;strong&gt;TTFT P99 = 637 ms.&lt;/strong&gt; 99% of users see the first token within 650 milliseconds. That is genuine chat speed.&lt;/p&gt;
&lt;p&gt;Identical arrival pattern to Test H, completely different experience. The difference is entirely in prompt size: ShareGPT conversations average 228 tokens, not 4000. Short prompt = cheap prefill = no queue pressure = sub-second TTFT.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Test H (random 4k)&lt;/th&gt;
&lt;th&gt;Test I (ShareGPT)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Achieved RPS&lt;/td&gt;
&lt;td&gt;0.27&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak concurrent&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;1286 ms&lt;/td&gt;
&lt;td&gt;353 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;3316 ms&lt;/td&gt;
&lt;td&gt;637 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;182 ms&lt;/td&gt;
&lt;td&gt;95 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is also a warning: the synthetic workload of Test H &lt;em&gt;overstates&lt;/em&gt; how heavy an average office prompt is. Real-world conversations are lighter than our 4k random baseline, so the real-world numbers probably sit closer to Test I than to Test H.&lt;/p&gt;
&lt;h2&gt;Test J: Monday morning peak&lt;/h2&gt;
&lt;p&gt;What if everyone comes in at the same time and starts hitting send buttons? Fivefold load, max 25 concurrent requests to model a real office.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec vllm-bench vllm bench serve \
  ... \
  --dataset-name random \
  --random-input-len 4000 \
  --random-output-len 500 \
  --random-range-ratio 0.9 \
  --num-prompts 300 \
  --request-rate 1.5 \
  --burstiness 1.0 \
  --max-concurrency 25
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Successful requests&lt;/td&gt;
&lt;td&gt;300 / 300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configured RPS&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Achieved RPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.26&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak concurrent requests&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total token throughput&lt;/td&gt;
&lt;td&gt;1173 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;P50&lt;/th&gt;
&lt;th&gt;P90&lt;/th&gt;
&lt;th&gt;P95&lt;/th&gt;
&lt;th&gt;P99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTFT (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1370&lt;/td&gt;
&lt;td&gt;1132&lt;/td&gt;
&lt;td&gt;1932&lt;/td&gt;
&lt;td&gt;2961&lt;/td&gt;
&lt;td&gt;6157&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TPOT (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;185&lt;/td&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;td&gt;195&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E2E (ms)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;92752&lt;/td&gt;
&lt;td&gt;91099&lt;/td&gt;
&lt;td&gt;165179&lt;/td&gt;
&lt;td&gt;172073&lt;/td&gt;
&lt;td&gt;179139&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is the key number: &lt;strong&gt;achieved rate 0.26 at target 1.5&lt;/strong&gt;. The system is &lt;strong&gt;throttled almost 6x&lt;/strong&gt;. Not because it crashes (all 300 requests succeed, no failures), but because the queue fills up to 25 and holds requests there until there is room.&lt;/p&gt;
&lt;p&gt;Compare Test H (target 0.3) and Test J (target 1.5):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Test H (0.3 rps)&lt;/th&gt;
&lt;th&gt;Test J (1.5 rps)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Achieved RPS&lt;/td&gt;
&lt;td&gt;0.27&lt;/td&gt;
&lt;td&gt;0.26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P50&lt;/td&gt;
&lt;td&gt;1286 ms&lt;/td&gt;
&lt;td&gt;1132 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P95&lt;/td&gt;
&lt;td&gt;2644 ms&lt;/td&gt;
&lt;td&gt;2961 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT P99&lt;/td&gt;
&lt;td&gt;3316 ms&lt;/td&gt;
&lt;td&gt;6157 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPOT P50&lt;/td&gt;
&lt;td&gt;182 ms&lt;/td&gt;
&lt;td&gt;187 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The median experience is even &lt;em&gt;slightly better&lt;/em&gt; in Test J than in Test H (1.13s vs 1.29s). The cap creates a smoother stream. But the tail is dramatically worse: P99 doubles from 3.3s to 6.2s.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;breakout-wide&quot;&amp;gt;
&amp;lt;img src=&quot;/blog/gemma-4-dgx-spark/open-loop-ttft.webp&quot; width=&quot;1425&quot; height=&quot;882&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; alt=&quot;Open-loop TTFT percentiles for H (random 4k 0.3 rps), I (ShareGPT 0.3 rps) and J (random 4k 1.5 rps). I stays sub-second everywhere; H climbs to 6.4s P99; J shoots up to 14.8s P99.&quot; /&amp;gt;
&amp;lt;figcaption&amp;gt;Open-loop TTFT percentiles. The median says little; the tail tells you where overload hurts.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;The Spark does not scale under oversubscribe, it &lt;strong&gt;queues&lt;/strong&gt;. That is good news: graceful degradation instead of crashes. For on-prem AI that is really the best failure mode.&lt;/p&gt;
&lt;h2&gt;What closed-loop hides, what open-loop overstates&lt;/h2&gt;
&lt;p&gt;The two methods each tell a different part of the story. Both true, both incomplete.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Closed-loop underestimates queue depth.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In Run F I tested c=10 as &quot;ten users at once&quot;. That sounds like a reasonably busy office situation. But Test H shows that an organic 0.3 req/s arrival rate is already enough to produce peaks of 36 concurrent requests. So the closed-loop &quot;10 users&quot; claim is more optimistic than practice shows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open-loop with synthetic overstates the real load.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At the same time: Test H uses random 4k prompts. A real office does not pose 25 average 4k prompts per minute. ShareGPT (Test I) is a much better proxy for &quot;what people type&quot;, averaging 228 tokens. With that workload shape, peak concurrent is 17 instead of 36, and P99 TTFT 637ms instead of 3.3s.&lt;/p&gt;
&lt;p&gt;So reality sits between Run F and Test I:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;TTFT (P50 or mean)&lt;/th&gt;
&lt;th&gt;Peak concurrent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run F (closed-loop, 10 users, 8k)&lt;/td&gt;
&lt;td&gt;7.99 s&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test H (open-loop, 0.3 rps, 4k random)&lt;/td&gt;
&lt;td&gt;1.29 s P50 / 3.3s P99&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test I (open-loop, 0.3 rps, ShareGPT)&lt;/td&gt;
&lt;td&gt;0.35 s P50 / 0.64s P99&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test J (open-loop, 1.5 rps, 4k random, cap 25)&lt;/td&gt;
&lt;td&gt;1.13 s P50 / 6.2s P99&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For an office with realistic prompts and a realistic arrival pattern, Test I is closest to what people feel. For capacity planning (&quot;what if everyone asks an 8k RAG question at once?&quot;), Run F is closest to what the machine can chew through.&lt;/p&gt;
&lt;h2&gt;The tail tells what the average hides&lt;/h2&gt;
&lt;p&gt;llama-benchy gave only mean ± stddev. That sounds like a lot of information, but it hides the part that matters most to your users: the tail.&lt;/p&gt;
&lt;p&gt;Test I&apos;s mean TTFT is 376ms. Sounds fine. But what does that say about the 1% of users where the queue just spiked? Nothing. For that you need P99, and that sits at 637ms. In this case no problem (both sub-second), but the &lt;em&gt;principle&lt;/em&gt; you need to know.&lt;/p&gt;
&lt;p&gt;Test H&apos;s mean TTFT is 1395ms. P99 is 3316ms. Comfortably more than twice as bad as the average for the unlucky 1%.&lt;/p&gt;
&lt;p&gt;Test J&apos;s mean TTFT is 1370ms. P99 is &lt;strong&gt;6157ms&lt;/strong&gt;. Comfortably four times the average.&lt;/p&gt;
&lt;p&gt;For SLA decisions (&quot;our system answers within 3 seconds for 95% of requests&quot;) you need these percentiles. Mean ± stddev can suggest an SLA you do not hit at the moments that matter most, namely when it is busy.&lt;/p&gt;
&lt;p&gt;That is why the blog cannot land on llama-benchy alone. Testing capacity is one thing. Reporting tail latency is another.&lt;/p&gt;
&lt;h2&gt;Decode is not the problem&lt;/h2&gt;
&lt;p&gt;With one user, decode stays almost flat.&lt;/p&gt;
&lt;p&gt;4k context gets 24.08 tok/s per user. 25k context gets 22.75 tok/s. 4096 output tokens (Run G, c=1) gets 24.17 tok/s. Multi-turn with depth 4 (Run E, c=1) gets 23.97 tok/s. Four different workloads, all within 6 percent of each other.&lt;/p&gt;
&lt;p&gt;At ten users at once something similar happens, only on a lower line. Run G: 11.75 tok/s/user. Run C: 10.92. Run E: 10.43. Run F: 9.31. And in the open-loop tests: Test I gives TPOT P50 = 95ms = ~10.5 tok/s/user. Test H and J give TPOT P50 = ~185ms = ~5.4 tok/s/user (because peaks there hit 25+ concurrent).&lt;/p&gt;
&lt;p&gt;In short: per-token decode speed is a function of &lt;strong&gt;average concurrent load&lt;/strong&gt;, not of prompt length, output length, multi-turn, or arrival pattern. Only at 16k+ context combined with multiple users (Run A) does it really drop below 7 t/s/user.&lt;/p&gt;
&lt;p&gt;Concurrency on its own is not the problem. Long output isn&apos;t either. Multi-turn isn&apos;t either. Only large context together with multiple users eats decode.&lt;/p&gt;
&lt;h2&gt;Prefill is the wall&lt;/h2&gt;
&lt;p&gt;What you feel first is waiting.&lt;/p&gt;
&lt;p&gt;With one user at 25k context it takes a good 6 seconds before the first response comes. At five users that becomes 19.9 seconds. At ten it becomes 35.4 seconds. At twenty it becomes 67.4 seconds.&lt;/p&gt;
&lt;p&gt;Run F shows that this is linear in &lt;em&gt;both&lt;/em&gt; concurrency and context. 8k context at 20 users gives 14.6 seconds, roughly a quarter of the 67.4 seconds at 25k context, for the same concurrency. Halve the prompt, halve the wait.&lt;/p&gt;
&lt;p&gt;And Test J shows: as soon as you push the system past its throughput ceiling, all that extra wait goes into the &lt;strong&gt;tail&lt;/strong&gt;. Median TTFT stays stable around 1.1-1.3s, but P99 shoots to 6 seconds. The pain of overload falls on a small group, not on everyone.&lt;/p&gt;
&lt;p&gt;That is where the real limit sits.&lt;/p&gt;
&lt;p&gt;Not: can the DGX Spark generate tokens? Yes.&lt;/p&gt;
&lt;p&gt;Not: can the KV-cache handle 20 × 25k? Also yes.&lt;/p&gt;
&lt;p&gt;Not: does it stop under overload? No, it queues along nicely.&lt;/p&gt;
&lt;p&gt;But: does this still feel like chat? Not for 25k. For 8k it is already borderline. For 2k with multi-turn just fine. For ShareGPT-realistic prompts with 25 users spread organically: a crystal-clear yes.&lt;/p&gt;
&lt;h2&gt;Where this does fit&lt;/h2&gt;
&lt;p&gt;These benchmarks make the on-prem choice more concrete.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Yes for an office environment where 10 to 25 people use local AI spread across the day.&lt;/strong&gt; Test I is the proof: 250 real ShareGPT conversations, 0.3 req/s arrival rate, P99 TTFT of 637ms. The median user sees the first token in 353 milliseconds. That is exactly the office scenario, and this is what it feels like.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Yes for RAG flows with medium context.&lt;/strong&gt; Run F gave the numbers up front: 8k prompt, 10 users, 8s TTFT, 9.3 tok/s streaming. Test H confirms the open-loop variant is still workable: P99 TTFT 3.3s. Not realtime, but within waitable bounds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Yes for agents and code generation.&lt;/strong&gt; Run G is the confirmation: short instruction, 4k+ tokens output, ten parallel tasks. TTFT under half a second, 11.75 tok/s/user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Yes for multi-turn conversations.&lt;/strong&gt; Run E gives 2.1s TTFT at 10 parallel 5-turn conversations. Decode the same as single-turn.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Careful with 5+ users at 25k context at once.&lt;/strong&gt; 19.9 seconds TTFT is no longer chat, but workable for analysis.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Careful with SLA claims based on averages.&lt;/strong&gt; Test H&apos;s mean TTFT of 1.4s could sound acceptable, but P99 sits at 3.3s. Decisions based on percentiles, not on mean.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No for support chat where ten to twenty users send 25k context per session at once and all expect a realtime answer.&lt;/strong&gt; Or: support chat under Test J-like load (1.5 rps of 4k prompts). That can technically run (no failures), but P99 TTFT of 6 seconds is a borderline case for chat.&lt;/p&gt;
&lt;h2&gt;What these tests do not say&lt;/h2&gt;
&lt;p&gt;This is not a MoE-vs-dense comparison. I want to test that separately, and then not only with throughput. If you compare MoE and dense, you also have to test prompts: summarizing, code questions, tool choice, ticket classification, a long context piece with follow-up steps. Otherwise you only measure how hard the engine spins, not whether it is driving the right way.&lt;/p&gt;
&lt;p&gt;This is also not a test with prefix caching on. That is deliberate. I wanted to see the raw prefill cost, not a benchmark that looks nicer because the prompts resemble each other. A next piece will add it: those same 8k and 25k context runs and the open-loop tests with &lt;code&gt;--enable-prefix-caching&lt;/code&gt;. My hunch: Test H and J benefit modestly (random data, little overlap), Test I benefits considerably (real conversations have overlapping system prompts and context), and Run F gets substantially faster. But that needs measuring.&lt;/p&gt;
&lt;h2&gt;Where I land&lt;/h2&gt;
&lt;p&gt;My expectation up front was that the DGX Spark with this MoE model would fill up sooner at large context windows. That happened, but differently than I thought.&lt;/p&gt;
&lt;p&gt;Memory was not the showstopper. Run B managed 20 users at 25k context without OOM. Test J survived 1.5 req/s without a single failed request. The practical limit always sat in prefill latency, not in capacity.&lt;/p&gt;
&lt;p&gt;And after nine tests it turns out: that is really the only limit you feel.&lt;/p&gt;
&lt;p&gt;Decode/user is almost a constant for this machine. Between 9 and 12 tokens per second at ten concurrent users, across six different closed-loop workloads. In open-loop with realistic ShareGPT prompts: 10.5 t/s/user. Only at 16k context or at synthetic peaks of 25+ concurrent does that drop below 7 t/s.&lt;/p&gt;
&lt;p&gt;What varies is how long someone waits before the text begins. At 256 prompt tokens that is half a second, even with ten users. At 2048 prompt tokens with five turns an average of 2.1 seconds. At 8192 prompt tokens with ten users eight seconds. At 25k with ten users 35 seconds. Under realistic 0.3 rps ShareGPT load: 353 milliseconds for the median, 637 milliseconds for the unlucky 1%.&lt;/p&gt;
&lt;p&gt;And as soon as you push the system above its capacity, it does not scale, it queues. Test J showed that a 1.5 req/s target gets throttled to 0.26 achieved, with the pain entirely in the tail (P99 6.2s) while the median stays stable. For on-prem AI that is the best failure mode you can hope for: nobody crashes, some wait longer.&lt;/p&gt;
&lt;p&gt;That is not a &quot;can this machine do it or not&quot;. That is &quot;pick the workload that fits what the user expects, and accept that 1% of requests has an unpleasant wait at peak moments&quot;.&lt;/p&gt;
&lt;p&gt;For one to three users with large context it is usable. For ten users with medium context it is fine. For ten users with multi-turn conversations it is actually at its best. For a 25-person office with realistic prompts and an organic arrival pattern it is astonishingly good: sub-second TTFT for 99% of requests, measured on real conversation data.&lt;/p&gt;
&lt;p&gt;For agent flows with long outputs it is strong. For twenty concurrent 25k prompts or for 1.5 rps oversubscribe it is no longer realtime chat. There you have to queue, turn on prefix caching, or route that kind of work differently.&lt;/p&gt;
&lt;p&gt;Two methods measure two things. Closed-loop benchmarks show what the machine can do. Open-loop replay shows what the user feels. The DGX Spark is a strong local AI machine for office work, as long as you know which knob decides what you feel.&lt;/p&gt;
&lt;p&gt;Decode sells the benchmark. Prefill decides the experience. And as soon as you go past the limit, the Spark queues instead of breaking, and that is the third number an on-prem choice has to be able to read.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>I put a 24/7 assistant on a Raspberry Pi</title>
    <id>https://djangodevreng.nl/en/blog/openclaw-on-raspberry-pi/</id>
    <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/openclaw-on-raspberry-pi/"/>
    <published>2026-05-01T00:00:00.000Z</published>
    <updated>2026-05-05T00:00:00.000Z</updated>
    <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
    <category term="on-prem"/>
    <summary>A build-log about OpenClaw on a Raspberry Pi 5: Slack as the interface, GPT-5.5 as the model, and the Pi as an always-on agent layer next to the DGX Spark.</summary>
    <content type="html">&lt;p&gt;I didn&apos;t want a better chatbot. I wanted an agent that picks up work on its own: hop on the internet, read tickets, dive into a repo, draft a first proposal for code changes and then report back where my team already works anyway.&lt;/p&gt;
&lt;p&gt;The entry point had to be Slack. That&apos;s where the questions, threads, files and half-finished ideas live. The agent had to be able to use tools, read files, stage branches and keep running when my laptop closes.&lt;/p&gt;
&lt;p&gt;So now there&apos;s a Raspberry Pi 5 with 4 GB RAM in my network. It runs &lt;a href=&quot;https://openclaw.ai/&quot;&gt;OpenClaw&lt;/a&gt;. Slack in front, GPT-5.5 behind it, &lt;a href=&quot;https://tailscale.com/&quot;&gt;Tailscale&lt;/a&gt; as the gateway when I&apos;m not home.&lt;/p&gt;
&lt;p&gt;That sounds bigger than it is. The Pi doesn&apos;t run a local language model. OpenClaw uses the Pi as an always-on Gateway: the layer that receives Slack messages, manages sessions and workspace context, starts an agent-run, makes tools available and sends the answer back to the same thread. In this setup the model runs through OpenAI.&lt;/p&gt;
&lt;p&gt;That distinction matters. For fully local inference I use &lt;a href=&quot;/en/dgx-spark/&quot;&gt;the DGX Spark&lt;/a&gt;, and I wrote about that earlier in &lt;a href=&quot;/en/blog/quantization-local-llms/&quot;&gt;the quantization post&lt;/a&gt;. This Pi is the agent layer next to it: always on, reachable in Slack, close to my files and workflows.&lt;/p&gt;
&lt;h2&gt;The thing I was missing&lt;/h2&gt;
&lt;p&gt;I already use plenty of AI tools. Claude Code for building. ChatGPT for one-off questions. For client projects I work with model APIs or local models, depending on what the data and infrastructure allow.&lt;/p&gt;
&lt;p&gt;The missing layer sat in between those tools: an agent that sees work come in and gets started. In Slack you can start small. I type a messy instruction, the agent reads the repo, pulls in the right tone-of-voice rules and comes back with something I can review.&lt;/p&gt;
&lt;p&gt;Publishing stays manual. So does trust. The first bit of groundwork is allowed to happen automatically.&lt;/p&gt;
&lt;p&gt;The direction is bigger than writing drafts. Eventually I want to point at a ticket and say: figure out what&apos;s needed here. The agent reads the context, checks documentation, looks in the codebase, proposes an approach and maybe stages a branch already.&lt;/p&gt;
&lt;p&gt;That kind of work often gets left behind because it doesn&apos;t fit anywhere neatly. Too small for a sprint. Too big to do &quot;on the side&quot;. Before you know it, that ticket is still open a week later with the same three vague comments under it.&lt;/p&gt;
&lt;h2&gt;What runs on the Pi&lt;/h2&gt;
&lt;p&gt;The base is small:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Raspberry Pi 5, 4 GB RAM&lt;/li&gt;
&lt;li&gt;OpenClaw Gateway locally on the Pi&lt;/li&gt;
&lt;li&gt;OpenAI GPT-5.5 as the model in this setup&lt;/li&gt;
&lt;li&gt;Slack as the interface&lt;/li&gt;
&lt;li&gt;Tailscale for remote access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Pi is mostly just available here. That&apos;s its talent.&lt;/p&gt;
&lt;p&gt;OpenClaw ties the layers together: channel, session, agent-runtime, model-provider and tools. A Slack message comes in through the channel layer. OpenClaw stages an agent-turn from it, with the right context and tools. The runtime runs that turn with the chosen model. Then OpenClaw delivers the answer back through Slack.&lt;/p&gt;
&lt;p&gt;That way the same agent can read files, run shell commands, fetch web pages, check git status or prepare a PR, depending on which tools you allow. So the Pi isn&apos;t a mini GPU. It&apos;s the local control layer.&lt;/p&gt;
&lt;p&gt;Tailscale keeps it practical. I can reach the Pi when I&apos;m out. Opening a public port for a build-log would be a bit much honour.&lt;/p&gt;
&lt;h2&gt;Slack as the shop floor&lt;/h2&gt;
&lt;p&gt;Slack was the easiest choice because I&apos;m in it all day already. My companies have workspaces, channels, threads, files and notifications. An extra dashboard would mostly collect extra tab dust.&lt;/p&gt;
&lt;p&gt;For me this is the core: the agent has to be available where the team works. If it figures something out based on a ticket, I want the answer back in the same flow. The analysis belongs next to the question, in the same thread.&lt;/p&gt;
&lt;p&gt;OpenClaw supports more entry points than Slack. It also works through, among others, Telegram, Microsoft Teams, Google Chat, WhatsApp, Discord and iMessage. Slack is my entry point. The broader idea is agents on existing communication channels, with tools and memory behind them.&lt;/p&gt;
&lt;h2&gt;The install was less exciting than I&apos;d hoped&lt;/h2&gt;
&lt;p&gt;The install was less dramatic than I&apos;d expected. That&apos;s nice for me and bad for the genre &quot;build-log with fireworks&quot;.&lt;/p&gt;
&lt;p&gt;Most of the time went into reading. OpenClaw has a lot of documentation, and you have to work out which part fits your setup. Slack, Gateway, agents, runtimes, channels, tools: they&apos;re separate layers that eventually form one assistant together.&lt;/p&gt;
&lt;p&gt;Setting up Slack took attention too. You decide which users may DM the bot, in which channels it may talk and whether it reacts to every message in group channels or only on an @mention. Those aren&apos;t details for later. You have to pick those rules up front and share them with your team, otherwise nobody gets when the agent does or doesn&apos;t join in.&lt;/p&gt;
&lt;p&gt;After about two hours it worked. I typed in Slack, the Pi caught the message, OpenClaw started a run, GPT-5.5 thought along and the answer came back in the same thread.&lt;/p&gt;
&lt;p&gt;A lot of plumbing for a text message. Except that text message can now use tools.&lt;/p&gt;
&lt;h2&gt;First test: this site&lt;/h2&gt;
&lt;p&gt;The first place I use this for is djangodevreng.nl.&lt;/p&gt;
&lt;p&gt;The content has to come from real work: what we built, what broke, which choices stuck, where a tool looked nice until it started to sweat under load. The agent gets to help with form and execution.&lt;/p&gt;
&lt;p&gt;Once that raw input is there, it can do a lot. Structure a dump. Make a first outline. Rewrite a draft in my tone. Strip out marketing language. Check whether a post sounds like it fell out of a generic LinkedIn carousel.&lt;/p&gt;
&lt;p&gt;The workflow for this site usually starts messy. I dump in Slack what I want to say: a few observations, a half idea, sometimes just feedback on an existing post. The agent then finds the right repo, reads the relevant files and grabs the writing guide from the workspace.&lt;/p&gt;
&lt;p&gt;Then I ask it for a concrete change: &quot;rewrite the intro&quot;, &quot;strip out the marketing language&quot; or &quot;make this technical explanation more precise&quot;. The sharper the instruction, the more usable the diff. It edits the markdown on a branch, runs the checks and pushes the change to a PR.&lt;/p&gt;
&lt;p&gt;That&apos;s where my part starts again. I read the diff, give feedback in Slack and let it process the next round. Only when the post is right do I merge it myself. The agent does the prep work. I stay responsible for what goes live.&lt;/p&gt;
&lt;p&gt;An agent that publishes without me looking isn&apos;t a workflow. That&apos;s a slot machine with commit rights.&lt;/p&gt;
&lt;h2&gt;Why this feels different from chat&lt;/h2&gt;
&lt;p&gt;A lot of AI tools feel like you have to bring your work to a chat window. You copy context, paste logs, explain for the third time where the repo path is and hope the model acts like it was there.&lt;/p&gt;
&lt;p&gt;This setup runs closer to the context. The agent can start on its own because it sees the workspace, knows the branch, can read the rules for the site and knows which checks need to run.&lt;/p&gt;
&lt;p&gt;That still doesn&apos;t make it an autonomous developer. It mostly pushes the first boring bit forward.&lt;/p&gt;
&lt;p&gt;For me that&apos;s the interesting agent layer: reading along ahead of time, making a first version, pointing out where it chafes. A junior colleague with infinite patience, no agenda and sometimes a worrying confidence in its own sentences.&lt;/p&gt;
&lt;p&gt;I&apos;m going to write a separate post about this, because OpenClaw really deserves more explanation than fits in this build-log. Which channels it supports. Which tools you hang on it. And above all: why this gets interesting.&lt;/p&gt;
&lt;p&gt;We&apos;re slowly shifting from AI as a sparring partner to AI as an executing layer. The past few years we mostly talked to models: brainstorming, summarizing, rewriting, thinking along. That stays useful, but the real difference is in agents that can carry out work in existing systems.&lt;/p&gt;
&lt;p&gt;Agents don&apos;t take over people&apos;s work one-to-one. It&apos;s not that simple, luckily. The shift is in workflows: figuring out tickets, gathering context, preparing drafts, proposing code changes, running checks, reporting back. Work you&apos;d normally ask someone for because it takes time, while it needs little deep human judgement.&lt;/p&gt;
&lt;h2&gt;Next step: tickets and MCP&lt;/h2&gt;
&lt;p&gt;The next step is &lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;MCP&lt;/a&gt;. I want to hang tools onto this workflow neatly, starting with Linear.&lt;/p&gt;
&lt;p&gt;The scenario is simple: a ticket comes in, the agent reads the relevant repo context, finds the likely files, writes a short analysis and comes back with a proposal or a list of questions.&lt;/p&gt;
&lt;p&gt;Autonomous merging I&apos;m skipping. First I want to know where the line is between useful preparation and dangerous eagerness to act.&lt;/p&gt;
&lt;p&gt;After that come GitHub, repo context and maybe a local knowledge base. Some context should just be available, without me pasting it into a prompt every time again.&lt;/p&gt;
&lt;h2&gt;Workflow by workflow&lt;/h2&gt;
&lt;p&gt;This Pi setup is small. That&apos;s exactly why I like it.&lt;/p&gt;
&lt;p&gt;Small enough to understand. Real enough to learn something from. Cheap enough to leave on all the time. Local enough to sit close to my work, without pretending the model itself runs locally.&lt;/p&gt;
&lt;p&gt;For production AI at clients this is at most one layer in the architecture. For my own workflow it already works fine: Slack as the entry point, OpenClaw as the Gateway, OpenAI as the model provider, GitHub as the place where work ends up staged.&lt;/p&gt;
&lt;p&gt;For the time being I&apos;m going to happily tinker with this. First this site. Then tickets. Then MCP tools. Then probably something I currently still think is too specific to automate.&lt;/p&gt;
&lt;p&gt;That&apos;s the interesting route: replacing workflow by workflow with an agent that does the groundwork, gathers context and stages proposals. Step by step I build out my OpenClaw setup. Just as a practical assistant that takes a bit more work off my hands each time.&lt;/p&gt;
&lt;p&gt;And if it breaks down, it&apos;s close enough to pull the plug.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>What quantization turned out to be</title>
    <id>https://djangodevreng.nl/en/blog/quantization-local-llms/</id>
    <link rel="alternate" type="text/html" href="https://djangodevreng.nl/en/blog/quantization-local-llms/"/>
    <published>2026-05-01T00:00:00.000Z</published>
    <updated>2026-05-05T00:00:00.000Z</updated>
    <author><name>Django de Vreng</name><uri>https://djangodevreng.nl/en/about/</uri></author>
    <category term="on-prem"/>
    <summary>A practical look back at quantization on the DGX Spark: what BF16, FP8 and NVFP4 do to memory, speed and tail latency, after three rounds with vLLM.</summary>
    <content type="html">&lt;p&gt;This was the first blog post I put live on this site. When I wrote it, I had just gotten two models running on the DGX Spark: Gemma-4-26B-A4B-it, a MoE model, and a 31B dense model. Both local, both through &lt;a href=&quot;https://docs.vllm.ai/&quot;&gt;vLLM&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At that point, quantization was still mostly a question for me. I knew the term, I roughly understood what it was about, but I had too few measurements of my own to say anything firm about it.&lt;/p&gt;
&lt;p&gt;By now we&apos;re a few benchmark rounds further. First &lt;a href=&quot;/en/blog/gemma-4-dgx-spark-benchmarks/&quot;&gt;Gemma-4 on the DGX Spark&lt;/a&gt;. Then &lt;a href=&quot;/en/blog/gemma-4-nvfp4-vs-bf16-dgx-spark/&quot;&gt;NVFP4 vs BF16 on that same model&lt;/a&gt;. And after that &lt;a href=&quot;/en/blog/nemotron-3-dgx-spark-precisions/&quot;&gt;Nemotron-3 in BF16, FP8 and NVFP4&lt;/a&gt;. Together they make up the guide &lt;a href=&quot;/en/dgx-spark/&quot;&gt;running LLMs on the DGX Spark&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That changed this post, really. It&apos;s less about &quot;what is quantization?&quot; and more about what happens when quantization stops being a model-card term and becomes an architecture choice.&lt;/p&gt;
&lt;h2&gt;The first question was simply: does it fit?&lt;/h2&gt;
&lt;p&gt;With hosted models you often start at quality. Which one is smarter, which follows instructions better, which writes better code?&lt;/p&gt;
&lt;p&gt;Locally you start blunter: does it fit?&lt;/p&gt;
&lt;p&gt;That sounds almost too simple, but on your own hardware that&apos;s the first wall. A model name and a model card are paperwork; the weights have to actually fit in memory. After that you still want room for context, you want to handle several requests at once, and ideally see something come back within seconds.&lt;/p&gt;
&lt;p&gt;On the DGX Spark you feel that immediately. You watch vLLM at work: downloading, loading, reserving memory, warming up. Only then does the discussion about throughput, latency and usability begin.&lt;/p&gt;
&lt;p&gt;That&apos;s a different feeling than an API call to Claude or GPT-5.5. There the infrastructure mostly exists as an abstraction. You send text in and get text back. Running locally, you see the back end. Sometimes that&apos;s fun. Sometimes it mostly takes a while.&lt;/p&gt;
&lt;p&gt;That&apos;s exactly where quantization comes in.&lt;/p&gt;
&lt;h2&gt;My first picture was too narrow&lt;/h2&gt;
&lt;p&gt;My first working definition was tidy enough: quantization stores model weights more compactly. FP16 or BF16 uses more space than 8-bit or 4-bit. Fewer bits means less memory. Less memory means a model fits sooner, loads faster, or leaves room for more context and more requests.&lt;/p&gt;
&lt;p&gt;That&apos;s correct, but it&apos;s too small.&lt;/p&gt;
&lt;p&gt;After the benchmarks I look at it differently. The question &quot;does this model fit on this machine?&quot; is only the start. After that comes the question of what you can do with that machine once the model fits.&lt;/p&gt;
&lt;p&gt;Running one request is the demo. Running multiple requests is the workflow.&lt;/p&gt;
&lt;p&gt;That&apos;s where the difference sits for me. A local model that answers one prompt neatly is nice. A local model that can handle several users, agents or tasks at once without latency collapsing becomes useful.&lt;/p&gt;
&lt;p&gt;So quantization decides how much room to move you have left.&lt;/p&gt;
&lt;h2&gt;vLLM makes it concrete&lt;/h2&gt;
&lt;p&gt;I use vLLM because one request at a time isn&apos;t the situation I&apos;m heading for. Starting a local chatbot is fine for testing, but the moment you talk about agents you get different traffic.&lt;/p&gt;
&lt;p&gt;An agent fetches context, calls tools, splits up work, sometimes asks for things in parallel and waits for results in between. Meanwhile you want a second request not to have to wait until the first is completely done.&lt;/p&gt;
&lt;p&gt;That&apos;s where serving matters.&lt;/p&gt;
&lt;p&gt;vLLM is the layer that makes this concrete: batching, scheduling, using memory more efficiently and handling multiple concurrent requests. It also makes visible that running locally is a system. The model, the precision, the context length, the number of simultaneous requests and the scheduler all pull on the same hardware.&lt;/p&gt;
&lt;p&gt;That was the first real lesson for me. Quantization isn&apos;t a separate trick at the bottom of the stack. It influences how the whole stack behaves.&lt;/p&gt;
&lt;h2&gt;BF16 felt like the safe choice at first&lt;/h2&gt;
&lt;p&gt;If you haven&apos;t measured yet, higher precision quickly feels safer. BF16 sounds solid. More detail, less quality risk, less chance the model starts behaving oddly.&lt;/p&gt;
&lt;p&gt;That was my first reflex too. If the hardware can handle it, why sit lower?&lt;/p&gt;
&lt;p&gt;The measurements made that less obvious. On the DGX Spark, BF16 often turned out to be the least practical choice in the later runs. BF16 isn&apos;t &quot;bad&quot;; it&apos;s just that the hardware and workload weigh more heavily than the tidy feeling of higher precision.&lt;/p&gt;
&lt;p&gt;If a lower precision gives much more room for concurrency, context or throughput, then in practice that can be better. Certainly for workloads where speed and concurrency count for more than the last bit of model quality.&lt;/p&gt;
&lt;p&gt;That&apos;s the twist I found interesting. The highest precision intuitively feels like the serious choice. On this machine it was often mostly the most expensive one.&lt;/p&gt;
&lt;h2&gt;NVFP4 changed the Spark&lt;/h2&gt;
&lt;p&gt;The biggest shift came with NVFP4. In the benchmark posts and the &lt;a href=&quot;/en/arena/&quot;&gt;arena&lt;/a&gt; you can see that NVFP4 nearly doubles the DGX Spark for many workloads. That&apos;s not a small optimization anymore. It changes what you dare to try on the same machine.&lt;/p&gt;
&lt;p&gt;For on-prem AI that&apos;s exactly the point. You buy hardware for a workflow, not for one pretty prompt. You want to know how much real work you can fit on that box.&lt;/p&gt;
&lt;p&gt;If NVFP4 means you can run more requests at once, keep more headroom and bump into memory limits less quickly, then that&apos;s not a detail in a table. Then your architecture changes.&lt;/p&gt;
&lt;p&gt;You can divide tasks differently. You can keep more local. You can experiment faster with agent steps that would otherwise go straight to a hosted model.&lt;/p&gt;
&lt;p&gt;That made quantization more practical for me than I&apos;d expected beforehand. It stopped being about a smaller model and became about enabling a different workflow.&lt;/p&gt;
&lt;h2&gt;FP8 had a different kind of upside&lt;/h2&gt;
&lt;p&gt;FP8 didn&apos;t simply sit &quot;between BF16 and NVFP4&quot;. In the Nemotron-3 runs, tail latency was the interesting part. That draws less attention than a big throughput jump, but in use it matters at least as much.&lt;/p&gt;
&lt;p&gt;Averages don&apos;t necessarily lie, but they reassure you at the wrong moments. A workflow feels slow because of the few requests that keep hanging.&lt;/p&gt;
&lt;p&gt;That&apos;s why tail latency is so practical. If an agent workflow has multiple steps, delays stack. One slow step is annoying. Three slow steps in a row feel like the system is reconsidering its life choices.&lt;/p&gt;
&lt;p&gt;FP8 looks useful in that corner: less extreme than NVFP4, but interesting when predictability matters more than running as much as possible at once.&lt;/p&gt;
&lt;p&gt;That&apos;s the nuance I didn&apos;t have yet in the first version. Precision isn&apos;t a ladder where lower is always faster and worse. It&apos;s a set of choices with different trade-offs.&lt;/p&gt;
&lt;h2&gt;Quality stays the open question&lt;/h2&gt;
&lt;p&gt;The benchmarks answer memory, throughput and latency. They say less about behaviour.&lt;/p&gt;
&lt;p&gt;That stays the hard side of quantization. You don&apos;t always see quality loss neatly in one metric. Sometimes an answer gets flatter. Sometimes code goes wrong a bit more often. Sometimes an agent picks the wrong tool. Sometimes you notice nothing, until your task is just different from your test set.&lt;/p&gt;
&lt;p&gt;For simple tasks that can be perfectly fine. Think classification, routing, first summaries, embeddings or a light pass over internal documents. The heaviest model doesn&apos;t always have to be on that.&lt;/p&gt;
&lt;p&gt;For code generation and agent workflows it&apos;s more sensitive. Small errors stack. One mediocre piece of reasoning is annoying. A wrong tool call is a different kind of problem.&lt;/p&gt;
&lt;p&gt;That&apos;s why I don&apos;t want to benchmark quantized models on speed alone. I want to know where I dare to deploy them.&lt;/p&gt;
&lt;p&gt;That&apos;s a different question. And honestly, the only one that counts.&lt;/p&gt;
&lt;h2&gt;The split gets clearer&lt;/h2&gt;
&lt;p&gt;My expectation is still that the best on-prem setup becomes a mix. &quot;Everything local&quot; sounds tough, but usually it&apos;s also needlessly strict.&lt;/p&gt;
&lt;p&gt;The logical split looks more like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;embeddings local&lt;/li&gt;
&lt;li&gt;sensitive documents local&lt;/li&gt;
&lt;li&gt;routing and classification local&lt;/li&gt;
&lt;li&gt;simple agent steps local&lt;/li&gt;
&lt;li&gt;heavy reasoning to Claude or GPT-5.5 when needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quantization decides how big that local part can get. The more tasks run reliably and fast enough locally, the less you have to send out.&lt;/p&gt;
&lt;p&gt;That matters for client work. Not because every token has to stay within four walls, but because some data does belong there. And because latency, &lt;a href=&quot;/en/dgx-spark-cost/&quot;&gt;cost&lt;/a&gt; and control simply count in production.&lt;/p&gt;
&lt;p&gt;An on-prem setup isn&apos;t a belief. It&apos;s a division of work.&lt;/p&gt;
&lt;h2&gt;What I&apos;d measure differently now&lt;/h2&gt;
&lt;p&gt;In the first version of this post I mostly had a list of questions. How long does downloading take? How long does loading take? How much VRAM is left? How many concurrent requests can I send before latency gets annoying?&lt;/p&gt;
&lt;p&gt;Those questions stay useful, but they&apos;re the start. How I set up those measurements on the Spark exactly is in the &lt;a href=&quot;/en/arena/methodology/&quot;&gt;arena methodology&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now I&apos;d put three things side by side per precision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;system behaviour: loading, memory, throughput, latency and tail latency&lt;/li&gt;
&lt;li&gt;model behaviour: Dutch output, code questions, longer context, tool use&lt;/li&gt;
&lt;li&gt;workflow fit: which tasks do I dare run locally with this&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one is easy to miss if you only look at benchmark tables. A model can technically run and still be awkward. Or score less prettily, but be exactly good enough for routing or summarizing.&lt;/p&gt;
&lt;p&gt;For production that makes the difference. Nobody buys &quot;tokens per second&quot; alone. You buy room in a workflow.&lt;/p&gt;
&lt;h2&gt;What I understand now&lt;/h2&gt;
&lt;p&gt;My working definition has shifted.&lt;/p&gt;
&lt;p&gt;Quantization makes a model smaller, but that&apos;s only the entrance. It changes how much work you get out of the same hardware, which latency you accept and which tasks you dare to keep local.&lt;/p&gt;
&lt;p&gt;On the DGX Spark, the highest precision rarely seems to be automatically the best choice. NVFP4 makes the machine much more usable for many workloads. FP8 is interesting when tail latency starts to matter. BF16 stays useful as a reference point, but on this hardware it less often feels like the practical default.&lt;/p&gt;
&lt;p&gt;That&apos;s exactly why I wanted to do these measurements. A universal ranking helps little; better architecture choices do.&lt;/p&gt;
&lt;p&gt;The question isn&apos;t: which quantization level wins?&lt;/p&gt;
&lt;p&gt;The question is: which task is allowed on which precision, on which machine, with how much risk?&lt;/p&gt;
&lt;p&gt;That&apos;s where on-prem AI starts getting interesting for me: at the division of work.&lt;/p&gt;
</content>
  </entry>
</feed>
