Local Host Lab
Running this year's open-weight models on hardware a university or a person already has: 2017 Tesla V100s on shared university GPU nodes, a DGX Spark, consumer GPUs, and a Mac. Each entry records what was run, on what, how it was served, and what the numbers were.
The question behind every entry is the same: if a model was released this year with open weights, how well does it run on hardware that is already in the building? That means GPUs from 2017 that are still in academic clusters, a small desktop box with a lot of memory, a gaming card, and a laptop-class chip. Every number below was measured on the machine described. Where a longer write-up exists, the entry links to it.
Three findings came up on every machine:
- Running locally is not a way to save money. A DGX Spark lists at $4,699. At a hosted price of $0.28 per million output tokens, that buys about 16.8 billion tokens; the machine, running all year without stopping, produces about 570 million. What local hardware buys is privacy, offline use, the freedom to modify weights, and a backend that does not go quiet when someone else's quota runs out.
- On old hardware the limit is usually software, not memory or arithmetic. Whether anyone compiled a numerical path for the card, and whether several cards can be joined into one device, decided more than FLOPs did.
- Benchmarks and real use diverge. The most useful finding in almost every round came from something a benchmark does not measure: an answer that would not stop, a prefix cache that was invalidated every agent turn, decode speed falling to under 40% at full context.
Beyond these machines
The entries below cover only the hardware measured here. Four companion pages widen the view to hardware that was not, combining these measurements with public benchmarks, specifications and prices, each cited with the date it was read.
The machines
| Hardware | Representative model | Served with | Single-stream decode |
|---|---|---|---|
| 8 × V100 32GB, four nodes joined | a 744B MoE at 1 bit (202 GB) | llama.cpp RPC | 12 to 14 tok/s |
| 5 to 6 × V100, three nodes joined | DeepSeek-V4-Flash 284B | llama.cpp RPC, patched | about 21 tok/s |
| 2 × V100 PCIe, no NVLink | Qwen3.8-27B, 256K context | llama.cpp, tensor parallel | 53 (Q8_0) / 66 (Q4_1) |
| 2 × V100 PCIe, no NVLink | Qwen3.8-Flash-Next Q8_0 (188 GB) | Strata, second card as expert cache | 55 to 58 tok/s |
| 4 × V100 SXM2, rented, NVLink | Qwen3.6 / 3.8-27B | same machine, NVLink on and off | n/a |
| DGX Spark, 128 GB unified memory | DeepSeek-V4-Flash, Qwen3.8-27B, Flash-Next 180B | llama.cpp, resident | 19.2 / 26 to 30 / 43 |
| RTX 3080, modified to 20 GB | Qwen3.6-27B IQ4_XS + MTP | llama.cpp, one card | 72 tok/s at 128K |
| RTX 5080 16GB | Qwen3.6-27B IQ4_XS | llama.cpp, one card | about 49 tok/s |
| 2 × V100 PCIe | MiniMax-H3 33B video and audio | diffusers, hand-written tensor parallel | 18.33 s / step |
| Mac Studio M4 Max 64GB | MiniMax-H3 33B, bf16 | h3.c (C and Metal) | 19 min per 5 s clip |
Old datacenter GPUs: Tesla V100
Shared university GPU nodes, two Tesla V100-PCIE-32GB cards per node, PCIe inside a node, 25 GbE between nodes, no NVLink. The cards are from 2017: no bf16 tensor cores, no FlashAttention, and dropped by PyTorch 2.11 and CUDA 13. The five long write-ups and the nine lessons that carry over from them are collected in Serving modern models on 2017 silicon.
V4-Flash is a 284B mixture of experts with 13B active. On one node with the experts in CPU memory it decoded at 4.75 tok/s, three to five times below the estimate made beforehand. The CPU side used about 17 GB/s of the machine's memory bandwidth, and the experts were stored as MXFP4, whose CPU path in ggml is less mature than Q4_K's.
Joining four to six cards over RPC crashed llama.cpp: rpc_server::graph_compute rejected view nodes that did not belong to a local buffer. Skipping those nodes, while keeping the existing security check intact, fixed it. After the fix, IQ3_XXS on five cards ran prefill at 259.7 and decode at 21.2 tok/s, 12.5 times and 4.5 times the single node. Q4_K_XL (144.4 GiB) fits on six cards with about 46 GiB left for the KV cache. Five cards were slightly faster than six; the sixth card buys KV room, not speed.
Entry 03, continued: layer split against tensor parallel
Under layer split, decode on two cards was the same as on one card: 26.309 against 26.301 ms per token. It decides whether the model fits and contributes nothing to speed. Switching llama.cpp to -sm tensor, so both cards read weights at once, took Q8_0 at full context from 8.9 to 18.4 tok/s. With MTP on top, the cumulative gain was 3.34×.
The recommended setup became Q4_1, tensor parallel, MTP with a draft depth of 2: 66 tok/s at an empty context and about 35 at 224K. Q8_0 in the same setup runs at about 53. Going from 8-bit to 4-bit weights was worth about 20% in speed; changing how the model is split was worth a factor of two. Per token, about 52% of the time is reading weights, 38% other compute and scheduling, and 11% the all-reduce between cards. Doubling the all-reduce payload did not change the speed, so it is latency-bound.
Two traps: with speculation on, quantising the KV cache sends the verify batch through a kernel that dequantises the whole cache to f16, which is 35% slower at 224K. And IQ4_XS with one published MTP head had its acceptance rate drop deterministically to zero under tensor parallel at full context, with no error; a different published head worked.
Entry 03, continued: long context and power
| KV depth | Baseline tok/s | MTP tok/s | MTP, two cards | MTP kWh per 1M tokens |
|---|---|---|---|---|
| 0 | 23.60 | 33.14 | 262 W | 2.20 |
| 64K | 18.35 | 26.69 | 278 W | 2.90 |
| 224K | 8.87 | 17.72 | 281 W | 4.40 |
At full context decode falls to 38% of the short-context rate. The MTP speed-up grows with depth, from 1.40× to 2.00×, while the acceptance rate does not change: each verify step spreads the cost of reading a deep KV cache over several tokens. Generating one million tokens at full context occupies both cards for 15.7 hours. At an assumed $1 per card-hour that is $31.40; the electricity is $0.53. The cost of self-hosting is almost entirely the time the cards are held, not the power.
Two PCIe cards cannot say what NVLink would add, so a 4 × V100-SXM2 machine was rented by the hour. Instead of comparing two machines, the interconnect was switched on and off on the same one (NCCL default against NCCL_P2P_DISABLE=1), with layer split as a zero control and a card pair that has no NVLink as a negative control.
The gain is all in prefill: +20 to 30% on two cards, +55% on three fully linked cards, +76% on four. Decode gains 1.4 to 4.1% without speculation and nothing, or slightly less than nothing, with MTP. llama.cpp's own all-reduce does not use NVLink at all; without NCCL at build time the link is wasted. NCCL_P2P_DISABLE is a lower bound for PCIe, not a real PCIe machine, so the two comparisons are reported separately.
A capacity machine: DGX Spark
GB10 (sm_121), 128 GB of unified memory of which about 119 GiB is usable, and about 273 GB/s of memory bandwidth. Single-stream decode is close to bandwidth divided by the bytes read per token. It holds models that do not fit anywhere else on hand; it is not fast.
UD-IQ3_XXS, 104.2 GB (97.1 GiB); about 18% of tokens take a different argmax from the original. The larger quant often recommended for 128 GB machines does not fit in 119 GiB. Loading has to bypass mmap, because on unified memory the page cache and the CUDA buffer each hold a copy and the load crashed at 2 minutes 50 seconds; without mmap it loaded in 93 seconds.
Decode is 19.2 tok/s and prefill about 400. Six A/B changes (KV precision, ubatch, newer builds) all stayed within ±5%: the bottleneck is the overhead of many small operators across 43 layers, not bandwidth, of which about 30% is used. The KV cache is 3.68 KiB per token, so a million tokens of context is 3.7 GiB.
Speculative decoding sped up code by 79% and prose by 0%, and it conflicts with context checkpoints. An agent rolls back a little every turn, so with speculation on the second turn recomputed 15,600 tokens: 51.7 seconds for the turn, against 7.1 seconds without it. Keeping whole sessions' KV in host memory brought switching back to a 125K-token session from 532 seconds to 0.41. A second Spark joined to the first would decode about 30% slower single-stream.
A clean bandwidth-bound case (82% bandwidth efficiency), so speed tracks weight size closely.
| Format | Weights | Code tok/s, MTP | Top-1 vs BF16, English | Chinese + code |
|---|---|---|---|---|
| Q8_0 | 28.60 GB | 18.38 | 98.64% | 98.48% |
| Q4_K_M | 17.11 GB | 25.41 | 94.67% | 93.52% |
| NVFP4 | 15.71 GB | 29.52 | 89.12% | 86.10% |
MTP took the baseline from 7.87 to 18.27 tok/s (2.32×); at a draft depth of 4 acceptance fell to 24%. NVFP4 is 15% faster than Q4_K_M at a similar size but has 4 to 5 times its KL divergence. Measuring on English alone understates the loss: KL on Chinese and code was consistently about 30% higher. With speculation on, smaller weights are not always faster, because quantisation error lowers the acceptance rate.
180B total, 6B active per token, including a 51B lookup table that llama.cpp reads from NVMe on demand, so it fits in 128 GB. UD-Q4_K_XL with MTP decodes at 43.2 tok/s with 80.6% acceptance; prefill, at 123 tok/s, is the weak side.
It served as the backend for a development instance of an Anthropic Messages endpoint for NCSA Lumen, the university's open-weight model service, so that Claude Code and the Anthropic SDK can call Lumen-hosted models. The end-to-end smoke test passed 9 of 9. Rewriting mid-conversation system messages in place, instead of moving them to the front, kept the backend's prefix cache valid and took per-turn prefill from about 40,000 tokens and 73 seconds to about 100 tokens and 0.5 seconds. One trap: llama.cpp divides -c evenly across parallel slots, so 65,536 with two slots gave each request 32K, while Claude Code's system prompt alone is 26K; --kv-unified fixes it. The client side is claude-code-lumen.
Qwen3.5-122B-A10B with INT4 weights and MTP-2 under vLLM: 37 to 40 tok/s on long thinking outputs, MMLU 90.0% and GSM8K 96.0% (n = 100). NVFP4 on the same machine ran at 16.6 tok/s, half the INT4 speed, because the FP4 kernels were written for SM120 and GB10 is SM121.
MiniMax-H3 at 960×544, 124 frames and 20 steps took about 410 seconds with community int8 weights, against 3.1 hours for a 10-second 544p clip on the V100s. An eight-segment, 43-second short was rendered on it.
Consumer GPUs
An Ampere card (sm_86, no native FP8 or FP4) modified to 20 GB, about 19.6 GiB usable. Qwen3.6-27B, fully on the card:
| Setup | Peak memory | Decode |
|---|---|---|
| IQ4_XS, 64K | 15.66 GB | 39 tok/s |
| Q5_K_S, 96K (the 5-bit limit) | 19.50 GB | 33 tok/s |
| Q5_K_S, 128K | out of memory | |
| IQ4_XS + MTP (depth 2), 128K | 18.3 GB | 72.3 tok/s |
MTP costs about 0.6 GB. Adding n-gram drafting on top speeds code tasks by another 8 to 13% at no memory cost. The trade is 4-bit, 72 tok/s and 128K against 5-bit, 33 tok/s and 96K. Decode barely changes with context because it is bound by reading weights.
The same card also trains small models. For a self-study course on training agents with SFT and RL, Qwen3-1.7B with LoRA went from 0.00 to 0.83 test success after 24 SFT steps (207 trajectories, about 100 seconds). GRPO from a 32-trajectory SFT start went from 0.31 to 0.64 in 40 steps and 29 minutes; from the 207-trajectory start it barely moved (0.83 to 0.84).
Qwen3.6-27B IQ4_XS fits with a 48K context and a 4-bit KV cache, leaving 335 MiB free, and decodes at about 49 tok/s. Q4_K_M does not fit; with 59 of 65 layers on the card it falls to about 20 tok/s. With K at q8_0 and V at q4_0 there is no fast attention kernel for this model's head size of 256, and prefill at 16K fell from 1032 to 42 tok/s; keeping K and V at the same 4-bit type avoids it.
Apple silicon
The same 33B video model as entry 05, run with antirez's C and Metal engine h3.c. The M4 has native bf16, so none of the V100 numerical problems apply; the limit becomes arithmetic. A 5.17-second clip at 960×544, 124 frames and 20 steps takes 19 minutes.
bf16 GEMM tops out at about 14.7 TFLOPS whoever writes the kernel: MPSGraph reached 14.70, and a hand-written kernel swept over about 50 configurations reached 14.66. The M4 GPU has no neural accelerators; forcing the Metal 4 tensor API compiles and runs about 10% slower. Upstream's hand-written kernels are enabled only on M5 devices, so published M5 numbers describe code the M4 never runs.
At production length the output is not reproducible: two runs with the same binary, settings and seed had a mean SSIM of 0.831 across 124 frames. Timing is: the two denoise times differed by 0.01%. Correctness checks for video diffusion cannot ask for a matching full clip.
Across machines
| 2 × V100 PCIe | DGX Spark | RTX 3080 20GB | |
|---|---|---|---|
| Memory | 64 GB over two cards | about 119 GiB unified | 20 GB |
| Bandwidth | 900 GB/s per card | about 273 GB/s | about 80% of an RTX 5080 |
| 27B, Q8_0 baseline decode | 23.6 tok/s | 7.87 tok/s | Q8 does not fit |
| 27B, fastest setup | 66 tok/s (Q4_1, tensor parallel, MTP) | 26 to 30 tok/s (NVFP4, MTP) | 72 tok/s (IQ4_XS, MTP, one card) |
| Best at | joining cards for very large models | 100B to 300B on one box, resident backend | 27B-class daily use, small training runs |
| Hardest part | software support for sm_70; queueing on shared nodes | the bandwidth ceiling | memory for one setup only |
Five things that held on every machine
- Single-stream decode is roughly bandwidth divided by bytes read per token. A 27B reached about 75% of bandwidth on the V100s and 82% on the Spark. The exception is a model made of many small operators (V4-Flash on the Spark, about 30%), where no quantisation format helps.
- Speculative decoding (MTP) was the largest single gain, 1.5 to 2.3 times on every machine, and larger at longer context. Acceptance can swing 30 points between single runs at temperature 1.0, so report a median of several runs.
- Measure speed and quality separately. Acceptance rate is not a quality measure. Quality needs KL divergence and top-1 agreement against the original weights, on more than English text.
- Benchmarks hide what quantisation damages most: answers that do not stop, slowdown at long context, cache misses in agent loops. Include one uncapped task from real use.
- Run it before reasoning about it. Several conclusions reached from source code or spec sheets were reversed by a single run: restoring a saved session on the Spark, V4-Flash speed on one node, and the value of NVLink.