DGX Spark with 8 users: 2.9x tokens, 36% speed each

dgx-spark Qwen3.8-Flash-Next

Every throughput figure published on this site — every number in the comparison table, every chat_short result in every post — is single-stream. One request in flight, batch of one. It is the right shape for “what does this feel like to use”, and it is what almost every local-LLM benchmark reports.

It is also a floor, not a ceiling, and until now nothing here said so.

A machine serving several requests at once produces more total tokens than one serving a single request, because the weights are read once per forward pass and that read serves every request in the batch. On a memory-bandwidth-bound machine, which the resident-size post showed this one is, that sharing is where the spare capacity lives.

So how much is there? Nobody publishes the curve for these machines. Here it is.

The measurement

One model, one quantisation, one server. A fixed 128-token prompt and a fixed 256-token output. The only thing that changes is how many requests are in flight simultaneously.

Simultaneous requests Aggregate tok/s Per-request tok/s Total vs 1 request Efficiency
1 31.0 31.0 1.00× 100%
2 49.2 24.6 1.59× 79%
4 71.2 17.8 2.30× 57%
6 82.0 13.7 2.65× 44%
8 89.8 11.2 2.90× 36%

Qwen3.8-Flash-Next at UD-IQ1_S, llama.cpp with eight slots, five repetitions per level, zero failed requests, coherence gate 3/3. Standard deviations between 0.44 and 1.60 tok/s.

The two numbers point in opposite directions

Aggregate throughput nearly triples. Eight concurrent users get 89.8 tokens per second out of the machine against 31.0 for one. If you are running a batch job — grading essays, tagging documents, anything where nobody is waiting — that is 2.9× the work per hour for free.

Each individual user gets 36% of the speed. A single user sees 31 tok/s. That same user, sharing with seven others, sees 11.2. An answer that took ten seconds now takes twenty-eight.

A benchmark that reports only the first number flatters the machine. One that reports only the second — which is what this site has been doing — understates it. Both are true and they describe different jobs.

When this matters, and when it does not

The test is simple: does anything wait on an individual answer?

If nothing does, concurrency is close to free money. Two hundred questions to grade, a corpus to tag, a benchmark to run — the work is a queue of independent requests and only the total time matters. Here is the same 200-question batch at each level, using the measured 256-token answers:

Simultaneous requests Aggregate tok/s 200 questions takes Speedup
1 31.0 27.5 min 1.00×
2 49.2 17.4 min 1.59×
4 71.2 12.0 min 2.30×
6 82.0 10.4 min 2.65×
8 89.8 9.5 min 2.90×

Twenty-eight minutes becomes ten. Nobody is worse off, because nobody was waiting on question 137 in particular.

This is not hypothetical for us. Every MMLU-Pro sweep on this site is exactly that shape — 200 independent questions per subject. The quantisation comparison ran one arm at two concurrent requests and the other at six, and the six-slot arm finished 400 questions in 4.8 hours against 8.4. Same machine, same afternoon; the difference was how many questions it was allowed to work on at once.

If something does wait, concurrency does nothing for you. A single person chatting with a model is one request. There is no batch to share a weight read with, so the number that matters is 31 tok/s and the other rows are irrelevant. Worse, if that person shares the machine with seven batch requests, they get 11.2 tok/s — concurrency has actively made their experience worse.

So the practical form:

Where the efficiency goes

The efficiency column is the interesting one. Two concurrent requests get 79% of perfect scaling. Eight get 36%.

That shape is what a bandwidth-bound machine looks like. Adding a second request is nearly free, because the expensive part — reading the weights — was already happening and the second request rides along on the same read. By eight requests the batch is large enough that the other costs, per-request KV cache traffic and attention over eight separate contexts, have grown to dominate, and each addition buys less.

The curve is still rising at eight. It has not flattened, so the ceiling is somewhere past where this machine could be tested — eight slots at 12,288 tokens each was what 121 GiB would hold alongside a 67.6 GiB model.

What this changes about the rest of the site

Every published throughput number is a single-user figure. For interactive use, that is the number you want. For anything serving more than one person or running a batch, the machine has roughly three times more capacity than those figures suggest.

That reframes some earlier conclusions. Nemotron 3.5 at 78 tok/s single-stream is not a machine doing 78 tokens per second — it is a machine doing 78 for one user and considerably more in total for several. The question “is a DGX Spark enough for my team” has a different answer than “is it fast enough for me”.

What this does not tell you

One model, one quantisation. A 67.6 GiB 1-bit MoE is not every model. A dense model, or one closer to the memory ceiling, will have a different curve — plausibly a worse one, since less spare bandwidth means less to share.

Short requests. 128 tokens in, 256 out. Long-context requests spend far more of their time in prefill, which parallelises differently, and eight simultaneous 8k prompts would compete for memory in a way eight 128-token prompts do not.

Slots were fixed at eight. llama.cpp allocates its slot count at launch, so this measures real concurrency at every level rather than queueing — but it does not tell you what sixteen would do, and this machine could not hold sixteen at a usable context length.

Nothing about latency distribution. The per-request figure is a median across five repetitions. A production serving setup cares about the tail, and this does not measure it.

Production services were stopped for the duration, to leave memory for eight slots. A machine also serving embeddings and a fast tier has less room and would show a shorter curve.

Total run time

Not recorded, and that is a gap in the harness rather than an omission here. bench/concurrency.py wrote its record without a duration_s field — the throughput harness has always stored one and this script did not — so the run behind this post has no measured wall clock to report.

It could be derived from the token counts and the throughput figures above, and that is exactly why it is not: a number computed from other numbers is an estimate, and putting one in a row labelled “machine time” beside figures that were actually measured is how a benchmark site stops being worth reading. The script now records the field, so the next concurrency run will have one.

The tool

bench/concurrency.py is in the repo. It reuses the throughput harness’s machine probe, backend detection and coherence gate, so a concurrency record carries the same provenance as any other run and refuses to measure a model that cannot answer three trivial questions correctly.

Two details in it are load-bearing. Each repetition builds fresh prompts with a new nonce, because otherwise the second round is served from the first round’s prefix cache and the curve measures caching rather than concurrency. And the slot count is a required argument that is recorded but cannot be verified — asking eight concurrent requests of a two-slot server measures queueing, and a record that did not state the slot count could not be read safely afterwards.