Benchmarks.

Wall-clock, client-side measurements against fixed viability thresholds — a record of what two specific pools did, not a spec to size a deployment against.

Relay is alpha and unaccredited
Releases are pre-release versions on the 0.1.0-alpha.N → -beta.N → -rc.N → 0.1.0 ladder. The wire protocol between hub and node, the on-disk state layout and the CLI surface can all change without a deprecation period; /v1 tracks the OpenAI shape and is the most stable thing here. Nothing is accredited or covered by a compatibility promise. The numbers below are runs on the maintainers’ own hardware, reported so a claim can be checked against data — not a benchmark suite to reproduce a guarantee from.

The question this page answers is not “how many tokens per second.” It is: which real workloads are viable on a pool, and which are not. A pool that can load a 32B model but takes nine seconds to start answering is a demo, not a tool.

How this is measured

Every number is wall-clock, measured at the client, over the real network path — not llama.cpp’s own internal timings, which cannot see the hop from editor to hub to node and back. That hop is the thing under suspicion, so it has to be inside the measurement.

MetricWhat it answers
TTFT (time to first token)Is it alive? A fast generator with slow TTFT feels broken.
Sustained tok/sCan I read as fast as it writes? Below reading speed, the user waits on every reply.
Prompt processingWhat does my context cost? Real developer prompts are files and diffs, and this dominates agentic work.
Concurrency behaviourWhat happens when the agent and I both ask? Editors, agents, and teammates do not take turns.

Cold start is tracked separately: it is paid once, and is annoying rather than disqualifying provided it is not paid often.

Two traps that make a GPU benchmark say nothing
Generation windows too short to mean anything — a two-token reply computes tok/s over one inter-token gap, which is noise. Every prompt here ends with an instruction to write at length, and scripts/bench_matrix.py excludes any run that finished well short of its token budget from throughput statistics, reporting the count in a short column instead of silently averaging it in.
Letting the context axis move two things at once — prompt size and generation task must vary independently, or “bigger context is slower” cannot be told apart from “the model wrote a different amount.” The generation instruction is identical at every context size; only the filler in front of it changes.

Viability thresholds

Fixed before any measurement, from how people actually work, so results are judged rather than rationalised.

Interactive chat

A person reads prose at roughly 250 words per minute, about 5.5 tok/s. Generation slower than that means watching the model type.

TargetReasoning
TTFT< 1.0 sUnder ~1 s reads as a response. Past ~3 s attention leaves.
Sustained> 15 tok/sComfortably ahead of reading speed, so the reply is ready as it is read.
Floor7 tok/sBarely ahead of reading. Usable, not pleasant.

Inline code completion

TargetReasoning
TTFT< 300 msA developer types faster than that. Slower suggestions arrive after the moment has passed and are ignored.

This is the hardest bar in the document, and the one most sensitive to the network hop.

Agentic automation (a coding agent driving the pool)

This is where a pool either earns its place or does not, and it is arithmetic rather than feel: a task of ~20 tool turns pays TTFT twenty times, not once; context grows through the task, often to 10k–50k tokens; and each turn re-processes that context unless it is cached. At 500 tok/s of prompt processing, a 30k-token context costs 60 s per turn before a single token is generated — 20 minutes of pure overhead across a task. At 5,000 tok/s the same turn costs 6 s.

TargetReasoning
Prompt processing> 2,000 tok/sKeeps a 20k-token turn under ~10 s.
TTFT at 8k context< 3 sPaid on every turn; multiplies by turn count.
Sustained> 15 tok/sTool-call arguments are short; generation is rarely the bottleneck.

Prompt caching changes this picture substantially and is measured separately — llama.cpp’s per-slot cache already gives a large speedup on repeated prefixes, which is exactly the shape of an agent loop.

Concurrency

TargetReasoning
2 clients< 2x TTFT degradationAn editor plus an agent is the normal case, not the exceptional one.
4 clientsNo timeouts, no errorsA household pool with several users must degrade, not fail.

Method

scripts/bench_matrix.py
scripts/bench_matrix.py --url <hub> --key <api key> --cacert <hub CA> \
    --models solo,coder-14b --contexts 32,2000,8000 \
    --runs 5 --concurrency 1,2,4 --max-tokens 128 \
    --out bench-<config>.json --note "<hardware description>"

Each configuration writes a JSON record per request, so results across different hardware are compared from data rather than from memory.

Config A — hub GTX 1060 (6 GB) + node RTX 4070 SUPER lending 8 GB

Hub and node on the same LAN, 1 ms RTT, node reached over Tailscale, client on the hub machine. solo is a 4.4 GB model placed as a whole replica on the node; qwen3-4b was sharded across host and node because solo’s replica already held the node’s VRAM.

Whole-model replica on a remote node (solo)

ctxprefixTTFT p50TTFT p95prompt tok/sgen tok/s
32cold41 ms48 ms2,12595.7
32warm25 ms31 ms— (cache hit)96.0
2,000cold379 ms384 ms4,76593.5
2,000warm27 ms52 ms— (cache hit)93.5

Concurrency (solo, ctx 2,000, 128 tokens each)

clientsTTFT p50tok/s eachtok/s total
122 ms94.293.4
2203 ms43.681.8
4463 ms70.4225.6

Throughput rises with load rather than collapsing, and no request failed.

Sharded across two machines (qwen3-4b, 9 layers host / 27 node, before adaptive slot sizing existed)

ctxprefixTTFT p50prompt tok/s
2,000cold13.2 s135
2,000warm190 ms9,347

The warm row is the prefix cache, not the fabric: a repeated prompt skips prompt evaluation almost entirely even on a shard. It is reported because an agent loop genuinely sees it, but it must not be read as the cost of a cold context, which is the 13.2 s row.

Verdict against the thresholds

WorkloadRequirementReplicaSharded
Chat TTFT< 1 s379 ms cold, 27 ms warm ✅13.2 s cold ❌
Chat generation> 15 tok/s93 tok/s ✅n/a
Completion TTFT< 300 ms379 ms cold, 27 ms warm ⚠️❌
Agentic prompt processing> 2,000 tok/s4,765 ✅135 ❌
2 clients< 2x TTFT203 ms (9x, from a 22 ms floor) ⚠️—
4 clientsno failuresnone ✅—

A whole-model replica on a pooled machine is comfortably viable for interactive and agentic work. The network hop costs about 25 ms on a warm request — nowhere near the budget. Generation at 93 tok/s is roughly 17x reading speed.

Sharding a model across machines is a different product. Prompt processing drops to ~135 tok/s, roughly 33x slower, because activations cross the network at every stage boundary. A 4,000-token prompt then costs about 30 s before the first token. Sharding is what makes a too-big model run at all; it is not what makes it pleasant.

In this single-stream test, the concurrency caveat is honest but mild: TTFT rises 9x from a very low floor (22 ms to 203 ms) and remains well inside the 1 s chat budget. That table sends each client the same prompt, though, so every request past the first lands on a cache hit — it is not a measurement of genuinely concurrent load. Config C, below, redoes this with a unique prompt per client and finds a different picture at 4 concurrent requests.

The controlled replica-vs-shard comparison

Adaptive slot sizing makes the same model placeable both ways on this hardware, so the cost of sharding can be measured directly rather than inferred across two different models. qwen3-4b, ctx 2,000, identical hardware, identical prompts — only the placement differs.

PlacementTTFT (cold)prompt tok/s
Sharded across host + node (4 fixed slots)13,220 ms135
Whole replica on the node (adaptive, 2 slots)366 ms4,888

36x, on both numbers. Two machines’ VRAM added together is not the same product as one machine holding the model: crossing the network at every stage boundary costs more than an order of magnitude. Sharding earns its place by making a too-big model run at all, and should be described that way rather than as the pooling story’s happy path. The corollary is that anything which pushes a model off the replica path and onto the shard path is expensive out of all proportion to the VRAM it saves — which is why the slot count is sized to the node rather than fixed.

What this configuration could not test

LimitDetail
Models larger than either machinecoder-14b needs 16.3 GB at the default 8,192 x 4 slots and the fabric had 12.3 GB, so it was correctly refused. Testing the actual product thesis — a model neither machine could host alone — needs the node lending its full 12 GB.
Thinking modelsQwen3-4B spent its entire 512-token budget on reasoning_content and emitted zero visible tokens. Time-to-first-visible-token is the number that matters for such models, and it is not yet characterised.

Config B — speculative decoding on a sharded placement

The premise: a shard pays a network round trip at every pipeline stage boundary, so drafting k tokens and verifying them in one pass should amortise that hop. Published work measures 2.5x in that regime, and a llama.cpp discussion reports 1.84–2.21x for a target split across two RTX 4070s. On this pool it produced no gain. See Speculative Decoding for how the mechanism works.

Target Qwen2.5-Coder-14B-Instruct-Q3_K_M, sharded 20 layers on the host (GTX 1060, Vulkan) and 28 on the node (RTX 4070 SUPER), ctx 4096 x 2 slots. Draft Qwen2.5-0.5B-Instruct-Q4_K_M, same tokenizer family, --spec-draft-ngl 99. Identical benchmark either side.

no draftwith draft
gen tok/s, ctx 3210.910.8
gen tok/s, ctx 2,00010.310.4
prompt tok/s, ctx 2,000 cold8183
cold TTFT, ctx 2,00022.46 s21.87 s

Every difference is inside run-to-run noise.

The mechanism did engage — this is not silently-dropped flags
The hub logged the full argv including --spec-draft-model and --spec-draft-ngl 99, and llama.cpp logged common_speculative_init_result: loading draft model. Host VRAM went to 3.5 GB of 6.1 GB with the draft resident.

The likely reason it did not help is that the hop is not this pool’s bottleneck. Speculation pays when link latency dominates compute; the published regime is roughly 3x compute < latency < 10x compute. Here the host holds 20 of 48 layers on a GTX 1060 over Vulkan, and at 10.4 tok/s a token costs ~96 ms — far more than a small activation tensor crossing a 1 GbE LAN. Amortising a hop that was never the expensive part buys nothing.

What this does not establish is that speculation never helps a shard. The reported 1.84x pairs two RTX 4070s, where host compute is fast enough for the hop to matter — a pool of two fast, evenly matched cards is the case still untested here. What it does establish is that the win is conditional on the hardware, and this pool is not in the regime.

The change is kept because it costs nothing when RELAY_DRAFT_MODEL is unset (the default) and it fixed a real VRAM-accounting gap on the way. But the speedup should not be claimed until a pool is measured that actually shows it.

Config C — what concurrency does to each placement

The earlier configurations measured one request at a time. That flattered the shard: it compared throughput, and throughput was merely bad. Measured under concurrent load, the two placements do not differ in degree. They differ in kind.

Same pool as Config A. solo (4.4 GB, 28 layers) is a whole-model replica on the node; a 14B Q3_K_M model, sharded 20 layers on the host and 28 on the node, stands in for the shard path. Context ~2,000 tokens, with a unique nonce per request so no request can hit another’s prefix cache.

Aggregate generation throughput, tokens/sec across all concurrent requests

concurrent requestsreplica (node)shard (host + node)
193.711.0
2148.69.4
4263.211.9

Time to first token at the same load

concurrent requestsreplicashard
10.5 s20.7 s
40.45–2.24 s33–91 s

A replica scales: four concurrent requests produce 2.8x the tokens of one, and the slowest client still sees its first token in under 2.3 s.

A shard does not scale at all. Four concurrent requests produce the same total tokens as one — the per-request rate collapses from 11.0 to as low as 1.2 tok/s — while the slowest client waits a minute and a half for its first token. Nothing errors and no request is dropped; the pool simply stops making progress proportional to what is asked of it.

Cold start compounds this. A sharded model loading from an evicted pool took 96.5 s to first token, of which roughly 95 s is weight loading and RPC setup rather than compute; a second measurement on a 14B shard reached 3m44s. A user who has not been warned assumes the process has hung.

What this means for what relay is

Sharding is not a slower version of pooling. It is a different product, and the concurrency numbers are what make that unambiguous.

  • A whole-model replica on a lent GPU is a serving tier. It scales with load, it answers in under a second, and a household or small team can share it. This is the case relay should be sold on.
  • A shard is a capability tier. It runs a model no single machine in the pool could hold at all, at roughly a thirtieth of the prompt throughput and a ninth of the generation throughput, for one user at a time. It is the answer to “can I run this model” and never the answer to “can we all use this model.”

The planner already encodes this preference — replica first, shard as fallback — and relay status already warns when a placement fell back. These numbers are the justification for both, and the reason the fallback warning should stay loud.

The harness's own concurrency phase measures cache hits
scripts/bench_matrix.py’s concurrency phase sends the same prompt to every concurrent client, so every request after the first hits the prefix cache and the run reports throughput that never happened — one such run produced a median of 65,134 tok/s, the same trap documented at the top of this page. The numbers above come from a corrected probe that gives each concurrent request its own nonce. Until the harness itself is fixed to match, its concurrency output is not trustworthy — including the Config A concurrency table earlier on this page.