Lab notebook

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.

June to October 2026 · Tesla V100, DGX Spark, RTX 3080 20GB, RTX 5080 16GB, M4 Max 64GB · llama.cpp, Strata, vLLM, PyTorch, ComfyUI, h3.c

What ran on each machine, in 40 seconds. Every clip was generated on the hardware it names: MiniMax-H3 and LTX-2.5 on two Tesla V100s (entry 05, entry 06), and MiniMax H3 on the DGX Spark for the Quad Day short (entry 11) and the Format Tax film. The numbers come from the entries below.

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:

  1. 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.
  2. 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.
  3. 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.
744B
largest model served
6
kinds of hardware
72 tok/s
27B on one 20 GB card
$2.47
to measure NVLink

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
single-stream decode, tok/supper end of a reported range
RTX 3080, modified to 20 GBQwen3.6-27B IQ4_XS + MTP, 128K
72
2 × V100 PCIeQwen3.8-27B Q4_1, tensor parallel
66
2 × V100 PCIeQwen3.8-Flash-Next Q8_0, Strata
55 to 58
RTX 5080 16GBQwen3.6-27B IQ4_XS
about 49
DGX SparkQwen3.8-Flash-Next 180B
43
DGX SparkQwen3.8-27B
26 to 30
5 to 6 × V100, three nodesDeepSeek-V4-Flash 284B
about 21
DGX SparkDeepSeek-V4-Flash
19.2
8 × V100, four nodes744B MoE at 1 bit
12 to 14
Single-stream decode from the language-model rows of the table above, fastest first, on one shared scale. The video rows are measured per step and per clip and are left out. The fastest setups are 27B models on one or two cards; pools of joined nodes buy model size, not speed.

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.

node 1 V100 32 GB V100 32 GB PCIe node 2 V100 32 GB V100 32 GB PCIe node 3 V100 32 GB V100 32 GB PCIe node 4 V100 32 GB V100 32 GB PCIe 25 GbE
The V100 nodes: two PCIe cards each, no NVLink, 25 GbE between nodes. Entry 01 pools all eight cards through llama.cpp RPC, entry 02 joins three nodes, and entries 03, 05, 06 and 07 run on one node's two cards.
744B compressed to 1 bit, 202 GB 8 × V100 across four nodes
Entry 01 · V100 note 01
744B at one bit, on eight V100s
No node has eight cards, so llama.cpp's RPC backend joins four nodes into one device pool. Across 300 paired items the 1-bit model finishes three questions behind full precision. What it lost was the ability to stop: one open question ran 14,711 tokens and about 20 minutes, and one added sentence brought it to 1,712 tokens at the same quality.
12 to 14 tok/s · 3 of 300 net · cold start 560 to 600 s
21.2 tok/s V4-Flash 284B, after the RPC fix one node one node: 4.75 4.75 five cards five cards: 21.2 21.2 4.5 times the single node
Entry 02 · notebook only
DeepSeek-V4-Flash on five or six V100s, and a fix to llama.cpp RPC

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.

4.75 → 21.2 tok/s decode · 5 cards faster than 6
66 tok/s 27B, two cards, tensor parallel 262,144-token context
Entry 03 · V100 note 02, updated
Qwen3.8-27B resident on two cards
The note covers the first version: Q8_0 with a separate MTP draft head, layer split, full 262,144-token context. Later rounds, below, changed the recommended setup.
MTP 1.49× to 1.96× · 48.9 of 64 GiB

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.

+76% prefill with NVLink on, four cards 2 cards 2 cards: +20 to 30% 2 cards: +20 to 30% +20 to 30% 3 cards 3 cards: +55% +55% 4 cards 4 cards: +76% +76%
Entry 04 · notebook only
What NVLink is worth, measured on a rented machine for $2.47

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.

prefill +76% on four cards · decode about 0 with MTP
A frame from a generated clip: a rally car throwing gravel past a crowd on a forest stage
Entry 05 · V100 note 03
A 33B BF16 video model on fp16-only cards
MiniMax-H3 unquantised on two V100s. The residual stream peaks at 114 times the fp16 limit, so it is accumulated in fp32 while GEMMs stay fp16. Precomputing one timestep-only layer as a table (39.3% of the parameters) cut memory from 61.7 to 37.5 GiB, and tensor parallel took a step from 31.18 to 18.33 s.
61.7 → 37.5 GiB · 1.70× from tensor parallel · no quantisation
A frame from a generated clip: a lighthouse keeper climbing to a lit lantern room at night
Entry 06 · V100 note 04
720p video in five minutes
A distilled 22B video model at 720p with memory to spare, and a two-stage upscale to 1920×1088. On the same two cards it is about a hundred times cheaper per clip than the 33B model in entry 05.
285 s at 720p · 615 s at 1920×1088
58 tok/s 188 GB of weights, 64 GiB of VRAM 10,523 of 24,576 experts on the cards
Entry 07 · V100 note 05
A 188 GB model on two 32 GB cards
Qwen3.8-Flash-Next at Q8_0, three ways on the same file. Using the second card as an expert cache decodes 2.6 times faster than tuned llama.cpp and adds 0.0024 KL. Experts at Q5 cannot be told apart from Q8, and two emulated 16 GB cards still decode at 35 to 40 tok/s.
55 to 58 tok/s · +0.0024 KL · 262,144 context

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.

0.41 s to switch back to a 125K session recompute recompute: 532 s 532 s from host RAM from host RAM: 0.41 s 0.41 s 19.2 tok/s decode on the Spark
Entry 08 · notebook only
DeepSeek-V4-Flash as a resident agent backend

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.

19.2 tok/s · 1M context in 3.7 GiB · 532 s → 0.41 s session restore
29.5 tok/s code with MTP, by weight format Q8_0 Q8_0: 18.4 18.4 Q4_K_M Q4_K_M: 25.4 25.4 NVFP4 NVFP4: 29.5 29.5
Entry 09 · notebook only
Qwen3.8-27B: a quantisation ladder against BF16

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.

MTP 2.32× · NVFP4 fast but lossy · test beyond English
0.5 s per-turn prefill, cache kept valid cache broken cache broken: 73 s 73 s cache kept cache kept: 0.5 s 0.5 s 43.2 tok/s decode · smoke test 9 / 9
Entry 10 · notebook only
Qwen3.8-Flash-Next 180B behind NCSA Lumen

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.

43.2 tok/s · 80.6% MTP acceptance · smoke test 9 / 9
37 to 40 tok/s 122B with MTP-2 under vLLM INT4 INT4: 37 to 40 INT4: 37 to 40 37 to 40 NVFP4 NVFP4: 16.6 16.6 MMLU 90.0% · GSM8K 96.0%
Entry 11 · notebook only
Earlier rounds on the same box: a 122B at INT4, and video

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.

122B at 37 to 40 tok/s · H3 clip in about 410 s

Consumer GPUs

72.3 tok/s 27B at 128K on one 20 GB card memory memory: 18.3 / 20 GB 18.3 / 20 GB IQ4_XS + MTP, depth 2
Entry 12 · notebook only
A 27B on one modified 20 GB RTX 3080

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).

72.3 tok/s at 128K · LoRA SFT and GRPO on 20 GB
49 tok/s 27B IQ4_XS, 48K context memory memory: 335 MiB free 335 MiB free 4-bit KV cache on a 16 GB card
Entry 13 · notebook only
The same 27B on a 16 GB RTX 5080

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.

about 49 tok/s · 335 MiB to spare

Apple silicon

19 min per 5.17 s clip at 960×544 MPSGraph MPSGraph: 14.70 14.70 hand kernel hand kernel: 14.66 14.66 bf16 GEMM ceiling in TFLOPS
Entry 14 · notebook only
MiniMax-H3 on a borrowed M4 Max with 64 GB

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.

19 min per 5 s clip · 14.7 TFLOPS bf16 ceiling · SSIM 0.831 run to run

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.