Local language models are becoming a practical alternative, and how they are served makes a real difference in performance.
In his latest article, Hıdır Bozkurt explores how to speed up local LLMs with vLLM, covering quantization, speculative decoding and their impact on accuracy.
The DGX Spark is a small desktop machine built around NVIDIA’s GB10 chip. Its selling point is 128 GB of unified memory (about 121 GB usable), enough to hold models that would never fit on a gaming GPU. Its weak point is that this memory is LPDDR5X with about 273 GB/s of bandwidth, several times less than a data-center GPU.
That matters because an LLM does two very different kinds of work:

Figure 1. Reading the prompt is limited by compute. Writing the answer is limited by memory bandwidth.
When the model reads your prompt (prefill), it processes thousands of tokens in parallel, and the GPU cores are busy. When it writes the answer (decode), it produces one token at a time, and for every single token it has to stream the active model weights from memory. The GPU spends most of that time waiting for data.
A useful rule of thumb: decode speed ≈ memory bandwidth ÷ bytes read per token. A 9B-parameter model in BF16 is about 18 GB, so the ceiling is 273 ÷ 18 ≈ 15 tokens/s. We measured 12.4. Every trick in this post therefore does one of two things: read fewer bytes per token, or get more than one token out of each read.
The same holds on any single GPU or workstation, so the methods below carry over to other hardware. Only the absolute numbers and the kernel choice (section 4.4) are specific to DGX Spark.
Item | Details |
Hardware | 1x DGX Spark (GB10, compute capability 12.1), 121 GB unified memory, 273 GB/s. |
Serving | vLLM nightly Docker image (13 Oct 2026), OpenAI-compatible API |
Models | Qwen3.5-9B (dense), Qwen3.5-35B-A3B (MoE), Qwen3.6-35B-A3B (MoE, NVIDIA NVFP4 checkpoint). All official releases. |
Speed test | Our own streaming client: 16 coding prompts, 512-token answers (256 in the concurrency test), 1 to 16 concurrent requests, plus 32K/60K-token long-input and 6-turn agent scenarios |
Quality test | MMLU-Pro (1,008-question subset), HumanEval+ and MBPP+ (full sets) and a 32K/64K needle-in-a-haystack test. Thinking off, temperature 0, slowest vs fastest setting of each model |
All speeds are generated tokens per second as seen by the client. For the quality runs we also record what matters to a user: the wall-clock time to finish the whole test set.
We tuned three models separately. Each one is compared with its own untuned starting point: the bars below are not one model getting faster step by step.

Figure 2. Single-request decode speed for each model, from its untuned baseline to its best configuration. Same machine, same prompts.
To check that the speed did not cost accuracy, every model answered the same 1,550 benchmark questions in its slowest setting (BF16 weights, BF16 KV cache, no speculative decoding) and its fastest one, with thinking off and temperature 0. Qwen3.6 also ran in Qwen’s official FP8 release as a reference for the cost of 4-bit weights.

Figure 3. Benchmark scores at each model’s slowest (light) and fastest (dark) setting. The fastest settings are the last bars in Figure 2.
Model and setting | tok/s, 1 request | tok/s, 16 requests | MMLU-Pro | HumanEval+ | MBPP+ | Time for all tests |
Qwen3.5-9B · slowest (BF16) | 12.4 | 193 | 74.4 | 86.0 | 62.2 | 82 min |
Qwen3.5-9B · fastest (FP8 + MTP 3) | 36.5 | 451 | 74.0 | 86.0 | 62.7 | 38 min |
Qwen3.5-35B-A3B · slowest (BF16) | 29.9 | 122 | 75.6 | 87.2 | 71.7 | 121 min |
Qwen3.5-35B-A3B · fastest (FP8 + marlin + MTP 2) | 68.1 | 310 | 76.5 | 87.8 | 72.2 | ~55 min |
Qwen3.6-35B-A3B · slowest (NVFP4 as shipped) | 75.9 | 327 | 78.1 | 91.5 | 76.7 | 47 min |
Qwen3.6-35B-A3B · fastest (NVFP4 + DSpark 4) | 129.2 | 455 | 77.3 | 92.1 | 78.8 | 36 min |
Qwen3.6-35B-A3B · official FP8 (reference) | — | 221 | 78.8 | 91.5 | 79.4 | 65 min |
Single-request speeds come from the speed test (section 2), where the untuned baselines already used FP8 KV cache. The 16-request speeds and scores come from the benchmark runs. The fastest settings also use FP8 KV cache. A needle-in-a-haystack test at 32K and 64K tokens found every needle in all seven settings.
Within each model, the same two levers do most of the work: lower-precision weights (fewer bytes per token) and speculative decoding (more tokens per read). The Qwen3.6 checkpoint is already 4-bit when you download it, so its untuned baseline is fast and the remaining gain comes from speculative decoding alone.
Benchmarks: MMLU-Pro via lm-evaluation-harness · HumanEval+ and MBPP+ via EvalPlus · needle test modelled on RULER niah_single_1.
Across models the picture is different, and it is a separate question: which model to run. Fully tuned, the 35B MoE is 1.9x faster than the dense 9B (68.1 vs 36.5 tokens/s), even though it is four times larger, and it also scores higher on every benchmark. Qwen3.6 in NVIDIA’s NVFP4 release is a newer version of the same MoE architecture. At 4 bits it reaches 129.2 tokens/s. On this kind of hardware, the number that matters is not total parameters but bytes that have to be read for each token.
A dense 9B model touches all 9B parameters for every token. Qwen3.5/3.6-35B-A3B has 35B parameters split into 256 small “experts”, but a router picks only 8 of them per token, so roughly 3B parameters are active. All 35B must fit in memory (fine with 121 GB), but only a tenth is read per token. On a bandwidth-starved machine this is the single most effective choice.

Figure 4. A dense model reads all its weights for every token. An MoE reads only the experts the router picked.
Weights are stored as numbers. Using fewer bits per number means fewer bytes to read.
Section 3 shows that, in our runs, the 4-bit model lost about one benchmark point against FP8.
Since decode is waiting on memory anyway, checking several tokens costs almost the same as producing one. Speculative decoding exploits that: a cheap drafter guesses the next few tokens, and the main model verifies all guesses in a single pass. Correct guesses are kept, and at the first wrong one the main model’s own token is used instead. The output follows exactly the same distribution as the main model alone. It is only faster.

Figure 5. The drafter proposes four tokens. The main model accepts three and corrects the fourth, giving four tokens for the price of one read.
Method | What the drafter is | Best result (Qwen3.6 NVFP4) |
MTP (multi-token prediction) | An extra prediction layer that ships inside Qwen3.5/3.6. No extra download. | 117.8 tok/s (2 tokens), 120.7 with NVIDIA's recommended settings |
DSpark | A separate ~1B drafter from Red Hat that drafts a whole block and predicts its own confidence | 129.2 tok/s (4 tokens) |
DFlash | Block-diffusion drafter (z-lab) | 128.1 tok/s, 140.2 at temperature 0 |
n-gram | No model: copies matching text from the prompt | +35% on 9B, helps only when the answer repeats the input |

Figure 6. Asking MTP for more tokens helps up to 2–3, then wasted guesses cost more than they save.
The first guess gives the biggest jump. On Qwen3.6, one MTP token took the speed from 75.9 to 103.5 tokens/s, and two tokens to 117.8. A third added only 2 tokens/s more. The 9B model peaks at 2–3 tokens (36.6 tokens/s) and slows down after that: with 5 tokens it drops to 33.9.
The reason is that each extra guess is less likely to be right. The MTP layer was trained to look one token ahead, so its second and third guesses build on its own earlier guesses and errors add up. On 9B, each step produced 1.9 tokens with 1 guess, 3.0 with 3 guesses and only 3.7 with 5. Every rejected guess still costs compute in the verification pass, so past 2–3 tokens the extra work outweighs the extra accepted tokens. DSpark drafts the whole block at once instead of one token at a time, which is why it stays useful at 4 tokens.
GB10 is new (compute capability 12.1), and many optimized kernels simply refuse to run on it. For FP8 MoE only the triton and marlin backends worked. For NVFP4 only marlin did. Setting –moe-backend marlin explicitly matters: a pre-quantized FP8 checkpoint otherwise selected DeepGEMM automatically and was slower.
A single request leaves most of the GPU idle, so serving several at once is nearly free at first: the same weight read serves every request in the batch.

Figure 7. Top: total throughput of the box. Bottom: average time until one 256-token answer is complete, as seen by each user.
Concurrent requests | 1 | 2 | 4 | 8 | 16 |
Total tokens/s (Qwen3.6 NVFP4) | 122.7 | 172.4 | 253.9 | 331.7 | 483.7 |
Per request tokens/s | 127 | 93 | 69 | 48 | 33 |
Time to finish one 256-token answer (s) | 2.1 | 2.9 | 3.9 | 6.0 | 8.2 |
Total throughput and the wait each user feels move in opposite directions. Going from 1 to 16 requests, the box produces 4x more tokens in total, but each answer takes 4x longer to finish (2.1 s → 8.2 s for Qwen3.6), because every request now shares the same weight reads with fifteen others. For a chat user 33 tokens/s is still faster than reading. For an agent that waits on every answer before its next step, the longer finish time adds up across hundreds of steps.
Two more observations. The 9B dense model barely slows down per answer (6.9 s → 9.2 s): a dense model reads the same weights no matter how many requests share them. And the dense 9B catches up with the Qwen3.5 MoE and overtakes it from 8 requests on. With one request the MoE reads only 8 experts per layer. With 16 different requests the union of chosen experts grows toward all 256, so its per-token advantage shrinks.
“Concurrent” means answers being generated at the same moment. A chat service with 20 active users typically has only a few requests in flight. An agent, by contrast, keeps roughly one request running all the time.
The fastest configuration we found for one DGX Spark:
vllm serve nvidia/Qwen3.6-35B-A3B-NVFP4 \
--moe-backend marlin \
--speculative-config '{"method":"dspark",
"model":"RedHatAI/Qwen3.6-35B-A3B-speculator.dspark",
"num_speculative_tokens":4}' \
--kv-cache-dtype fp8 --load-format fastsafetensors \
--enable-prefix-caching --enable-chunked-prefill --async-scheduling \
--max-num-batched-tokens 8192 \
--reasoning-parser qwen3 --enable-auto-tool-choice \
--tool-call-parser qwen3_coder --trust-remote-code
If you do not want an extra drafter download, replace the speculative config with the built-in MTP ({“method”:”mtp”,”num_speculative_tokens”:2}) for about 118 tokens/s.
Limits: one machine, one model family, coding prompts for the speed tests, thinking off in the quality tests, a vLLM nightly build, and single quality runs with real noise. Treat small differences as indicative, not final.

Data Scientist
Hıdır Bozkurt conducts R&D across various areas of data science, primarily focusing on NLP and computer vision.
Your mail has been sent successfully. You will be contacted as soon as possible.
Your message could not be delivered! Please try again later.