Local Host Lab · V100 · 05

A 188 GB model on two 32 GB cards

Qwen3.8-Flash-Next at Q8_0 is a 188 GB file, three times the memory of the two cards it ran on. Three ways of running it were compared on the same file. The winner keeps the model's error within 0.0024 KL of llama.cpp and decodes at 55 to 58 tokens per second, 2.6 times the tuned llama.cpp setup.

October 2026 · Qwen3.8-Flash-Next Q8_0 (188.2 GB, Unsloth GGUF) · one node, 2 × Tesla V100-PCIE-32GB, no NVLink, 377 GB RAM · llama.cpp, Strata v0.1.42

188 GB
weights, Q8_0
55 to 58
decode tok/s
+0.0024
KL vs llama.cpp Q8_0
262,144
context, native

Note 02 put a 27B model on these cards and found that the second card bought capacity, not speed. This model is different in kind: a mixture-of-experts whose weights are three times the memory of both cards together. Most of it has to live in system RAM, and the question becomes which parts sit on the GPUs and how the rest gets there.

The rule was fixed before any run. If Strata on two cards came out about as precise as Q8_0 on llama.cpp and much faster, it would be the serving backend; otherwise llama.cpp at Q8_0 would be.

The setup

One borrowed HPC node: two Tesla V100-PCIE-32GB with no NVLink, two Xeon Gold 6248 (20 cores each, AVX-512 without VBMI), 377 GB of RAM and a locked-memory limit of about 47 GiB. Compute nodes have no outbound network, and the weights sit on a shared filesystem.

The weights are Unsloth's Q8_0 GGUF of Qwen3.8-Flash-Next (GGUF builds), 188.2 GB in six shards. From the file's own metadata: 48 layers, of which 12 are attention layers and the rest linear attention; two KV heads of 256 dimensions; 24,576 routed experts; a native context of 262,144 tokens. The file also carries a 54 GB per-layer lookup table, from which each token reads 16 rows.

Q8_0 was chosen because it is the highest precision that fits in one node's RAM, and the highest that Strata v0.1.42 will load, so all three arms ran the same file.

engine how the two cards are used
A llama.cpp master, CUDA 12.9, sm_70 experts in RAM, then as many as fit moved onto both cards; tensor split also tried
B Strata, one card hot experts in VRAM, the rest in RAM
C Strata, two cards either a layer split, or the second card as an extra expert cache

Speed was measured with a long math TeX prompt, temperature 0.6, thinking on, 512 output tokens, single stream. Single runs unless a row says otherwise.

Tuning llama.cpp from 9.5 to 22

configuration 1K prefill 1K decode 16K prefill 16K decode
all experts in RAM, mmap 23 9.5 25 9.0
t1: experts fill both cards, no mmap, ubatch 4096 113 15.4 293 13.0
t3: t1 + MTP, draft depth 3 67 24.1 112 16.0
t5r: t1 + NUMA interleave 133 14.5 295 13.5
t6b: tensor split across both cards 161 10.2 211 9.3
t9: t5r + lookup table loaded into RAM (4K, mean of 2) 4K: 232 4K: 18.6
t10: t9 + MTP, depth 3 (4K and 16K, mean of 2) 4K: 267 4K: 22.0 208 20.3

The cause of decode jumping between 6 and 24 tok/s was the lookup table being read lazily. By default llama.cpp maps tensors larger than 4 GiB on demand. Once the table's pages fall out of the file cache, every token reads its rows from the shared filesystem. With --lazy-mode off the table is loaded into RAM: decode on the same configuration went from 8.0 to 18.6 tok/s, the run-to-run spread disappeared, and MTP stopped slowing down prompts. Rows t1 to t8 were all affected.

  • Without NUMA interleave the whole model lands on one socket, and prefill swung between 120 and 293 tok/s on identical runs. numactl --interleave=all made it stable.
  • MTP in llama.cpp makes prompt processing about 2.5 times slower (a 4,096-token chunk went from 13 s to 36 s). The cause was not found.
  • The tensor split, with experts in RAM, is slower than the default: about 10 tok/s decode.

Three ways to use the second card

llama.cpp, all experts in RAM: 9.0 tok/s at 16K llama.cpp, experts in RAM 9.0 llama.cpp tuned (table in RAM, NUMA interleave, MTP): 20.3 tok/s at 16K llama.cpp, tuned 20.3 Strata, one card: 18.0 tok/s at 16K Strata, one card 18.0 Strata, two cards, layer split: 26.7 tok/s at 16K Strata, two cards, layer split 26.7 Strata, second card as expert cache: 57.9 tok/s at 16K Strata, second card as expert cache 57.9 decode tok/s, 16K-token prompt, 512 tokens out Single stream, temperature 0.6, thinking on.
Same 188 GB file in every row. Splitting the layers across the two cards helps a little; using the second card to hold more experts is what doubles it.
Data
configuration 16K decode tok/s 16K prefill tok/s
llama.cpp, all experts in RAM, mmap 9.0 25
llama.cpp t10: experts fill both cards, table in RAM, NUMA interleave, MTP n=3 (mean of 2) 20.3 208
Strata, one card, defaults 18.0 550
Strata, two cards, layer split, resident budget 200 GiB 26.7 983
Strata, second card as expert cache, re-measured on a healthy node (mean of 2) 57.9 773

Strata has no tensor parallelism. On two cards it offers a layer split, where each card holds half the layers, or a mode where the second card holds nothing but more experts (--expert-cache-device1 auto --remote-expert-opt). Both were measured. The second mode was re-measured on a healthy node, as the next sections explain:

configuration 4K prefill 4K decode 16K prefill 16K decode
llama.cpp t10 267 22.0 208 20.3
Strata, second card as expert cache (mean of 2) 394 55.0 773 57.9
same, four concurrent streams 8.5 to 16.5 each, ~29 total

Batching four streams lowers total throughput in this mode, because the prompt reads serialise, so the server runs one stream per node. With one card, 4,864 of the 24,576 experts (19.8%) fit in VRAM, and on math text they served about 80% of the routed calls.

Precision

23 math TeX requests, teacher-forced, 16,031 positions. The reference is the same Q8_0 file on llama.cpp's CPU backend with fp32 accumulation. The metric is KL from the reference to each arm over the reference's top 256 tokens plus one bucket for the rest.

llama.cpp Q8_0 on the GPUs: mean KL 0.0141, 95% CI 0.0104 to 0.0180 llama.cpp Q8_0, CUDA 0.0141 Strata, expert cache on the second card: mean KL 0.0170, 95% CI 0.0124 to 0.0210 Strata, second card as expert cache 0.0170 Strata, layer split with a resident RAM budget: mean KL 0.0351 Strata, layer split, resident budget 0.0351 Strata, one card with a resident RAM budget: mean KL 0.0365, 95% CI 0.0272 to 0.0476 Strata, one card, resident budget 0.0365 mean KL against llama.cpp Q8_0 on the CPU (lower is closer) 23 math requests, 16,031 positions, teacher-forced.
Divergence from the same Q8_0 file run on the CPU in fp32. Whiskers are bootstrap 95% intervals over the 23 requests. The two configurations with a resident RAM budget add about 0.021; the expert-cache configuration adds 0.0024.
Data
configuration mean KL [95% CI] top-1 agreement top-10 overlap
llama.cpp Q8_0, CUDA 0.0141 [0.0104, 0.0180] 98.1% 92.4%
Strata, second card as expert cache, --remote-expert-opt 0.0170 [0.0124, 0.0210] 97.9% 92.2%
Strata, two cards, layer split, resident 40 GiB, --adapt-swaps 0 0.0351 96.5% 89.7%
Strata, one card, defaults, resident 200 GiB 0.0365 [0.0272, 0.0476] 96.5% 88.6%

Paired over the 23 requests, against llama.cpp on the GPUs:

arm KL difference [95% CI] top-1 difference [95% CI] requests worse
second card as expert cache +0.0024 [+0.0003, +0.0049] −0.17 pt [−0.46, +0.07] 14 of 23
layer split, resident budget +0.0209 −1.55 pt 21 of 23
one card, resident budget +0.0220 −1.51 pt 22 of 23

Both Strata configurations with a resident RAM budget add about 0.021 KL, roughly the distance between two adjacent quantisation levels. The expert-cache mode cannot be combined with that budget, and it adds 0.0024. Its KL interval excludes zero; its top-1 interval does not. The largest 1% of positions carry 47% of its total KL, against 46% for llama.cpp on the GPUs, and its single worst position is 8.7 nats against 4.7.

Three limits on these numbers. Every request was under 1,024 tokens, so this measures the batched prompt path, not token-by-token generation and not long context. The reference is itself Q8_0, so this is the error each engine adds on top of Q8_0, not what Q8_0 costs against BF16; a BF16 reference does not fit on one node. And the precision runs used a 65,536-token maximum context against 262,144 in production, with the KV cache at int8 in both.

The node that printed 3333

The first precision round made Strata's two-card modes look worse than one card: the expert-cache arm at 0.0410 KL, and a layer split whose per-position output was NaN at 11,749 of 16,031 positions.

All of those runs were on one node. On that node, llama.cpp's serving configuration on both cards answered a short greedy sanity question with 3333... every time, while the identical binary, model and flags answered correctly on two other nodes with the same driver, VBIOS and kernel. Each card on its own was correct. A peer-to-peer test (peer copies with and without peer access, plus direct peer reads, 256 MB three times) found zero mismatches. The cause was not found, and the node was released.

Re-measured on a healthy node, the expert-cache arm dropped from 0.0410 to 0.0170 and the layer split had no NaN rows. From then on every new node got the two-question sanity check before any measurement.

Where the 64 GiB goes

A production instance fills both cards: 31.4 GiB each, 62.8 of 64 GiB in all. Strata places what it needs first and fills the rest with experts.

card use memory
first non-expert weights (attention, linear attention, routers) 1.4 + 2.9 GiB
first output head (Q5_K) 0.6 GiB
first MTP draft layer 1.1 GiB
first KV cache, 262K tokens, int8 (from free memory at start) ~3.9 GiB
first expert cache, 4,226 experts 20.6 GiB
first CUDA context, speculative decoding buffers ~1 GiB
second expert cache, 6,297 experts 30.6 GiB

About 10 GiB is required; the other 51 GiB or so is cache. Together the two cards hold 10,523 of the 24,576 experts, 43%. Of the cache on the first card, 5.1 GiB is lent to prefill while a long prompt is read. Every expert in VRAM is one that does not cross PCIe from RAM for each token that routes to it.

Long context

At a 131,072-token maximum, KV int8, reading an unseen prompt from scratch and generating 512 tokens, two runs per length:

prompt prefill tok/s decode tok/s
27K to 29K 361 / 404 24 / 35
59K to 61K 489 / 503 31 / 34
95K to 96K 485 / 578 26 / 31
116K to 119K 453 / 769 28 / 31

In a real math-agent run (487 requests, prompts up to 97.7K tokens, 1K to 17K tokens generated each), the engine logs show decode barely moving with length: 48, 51, 47 and 46 tok/s for prompts under 16K, 16K to 32K, 32K to 64K and over 64K.

The full 262,144 tokens

The KV cache is small. At int8 (int8 codes plus one fp16 scale per 64 values) it measured about 15.6 KB per token from free memory, because only 12 of 48 layers have attention and each has two KV heads. Going from 131,072 to 262,144 cost 1.9 GiB on the first card and moved its expert slots from 4,619 to 4,226.

A 223,076-token prompt read at 585 tok/s (382 s) and then decoded at 41.7 tok/s.

The first two attempts at 223K to 245K tokens stalled between 66K and 82K and were killed by the engine's 300-second watchdog. Strata's default reads the 54 GB lookup table row by row with direct I/O, bypassing the page cache; cold, that is about 2,500 rows per second against 16 rows per token. Loading the table into locked RAM needs a 54 GB lock, over the 47 GiB limit. The fix was --ple-io mmap plus one sequential read of the table's shard at startup, 19 seconds from the file cache.

Prefix cache and routing

During that agent run, three nodes sat behind a small local proxy. Prefill took 25% of engine time (8,406 of 33,428 s), and 6,527 s of it, 78%, was 107 long prompts read from scratch. The proxy sent each request to the least busy node, so one conversation hopped between nodes and lost its prefix cache every time it moved. Follow-up requests that stayed on the same node read only 36 to 661 new tokens.

The proxy now routes by conversation: the key is a hash of the first two messages, and a new conversation goes to the node with the fewest conversations plus requests in flight.

Smaller experts

Later, the production instance slowed to about 4 tok/s decode, and long prompts stalled at the first layer. That led to two findings: what caused it, and that the experts can be stored at lower precision without a measurable cost.

The lookup table was being evicted by the expert load itself. The engine reads the experts through the file cache, and on this node the file cache never grew past about 100 to 150 GB. Streaming 92 to 120 GiB of experts through it pushed out the table pages that had been warmed before startup (Q8_0: 100% cached before, 0% after). Reading the table's shard again after the engine finished loading (14 to 21 s) brought it back to 100%, and Q8_0 returned to its original speed: 400 / 59.8 tok/s prefill / decode at 4K, against 394 / 55.0 first measured.

Unsloth publishes the same model with lower-precision experts, all loadable by Strata. Same 23 requests, same engine arguments as the Q8_0 expert-cache arm:

experts download mean KL [95% CI] vs Q8_0 expert cache, paired 4K / 32K decode
Q8_0 188.2 GB 0.0170 [0.0124, 0.0210] 59.8 / 49.4
UD-Q6_K_XL 169.2 GB 0.0186 [0.0124, 0.0249] +0.0017 [−0.0015, +0.0057] 64.4 / 57.1
UD-Q5_K_XL 158.3 GB 0.0190 [0.0131, 0.0243] +0.0020 [−0.0025, +0.0073] 69.3 / 62.0
UD-Q4_K_XL 111.3 GB 0.0257 [0.0160, 0.0362] +0.0089 [+0.0013, +0.0183] 83.8 / 68.3

Speeds are single runs at 262K context with the table re-read after loading. Q6 and Q5 cannot be told apart from Q8_0. Q5 uses 28 GiB less RAM, fits about 31% more experts on the cards, and decodes 15 to 25% faster. Q4 is a significant loss: about 0.021 KL was taken above as one quantisation level, which puts Q4 at roughly 0.4 of a level (that scale comes from community numbers, not from these runs).

Two 16 GB cards, emulated

The 16 GB V100 is common too. There were no 16 GB cards to test on, so the same node emulated them: before each start, a placeholder process allocated 16,608 MiB on each 32 GB card, leaving about 16,160 MiB, the usable memory of a 16 GB card. The table was re-read after loading, and the sanity check passed before and after every configuration.

2 x 32 GB, UD-Q5_K_XL, expert cache, 262K: 62.0 tok/s at 32K 2 × 32 GB, Q5, expert cache 62.0 emulated 2 x 16 GB, Q8_0, expert cache, 262K: 28.1 tok/s at 32K 2 × 16 GB, Q8_0, expert cache 28.1 emulated 2 x 16 GB, UD-Q5_K_XL, expert cache, 262K: 37.7 tok/s at 32K 2 × 16 GB, Q5, expert cache 37.7 emulated 2 x 16 GB, UD-Q5_K_XL, expert cache, 128K: 39.0 tok/s at 32K 2 × 16 GB, Q5, 128K context 39.0 emulated 2 x 16 GB, UD-Q5_K_XL, expert cache, --kv-resident 32768: 38.5 tok/s at 32K 2 × 16 GB, Q5, KV partly in RAM 38.5 emulated 2 x 16 GB, UD-Q5_K_XL, layer split: 31.6 tok/s at 32K 2 × 16 GB, Q5, layer split 31.6 emulated 2 x 16 GB, UD-Q4_K_XL, expert cache, 262K: 47.0 tok/s at 32K 2 × 16 GB, Q4, expert cache 47.0 decode tok/s, 32K-token prompt, 512 tokens out 262K context unless noted. Single run per bar.
Each 32 GB card was filled to 16,608 MiB by a placeholder process before the engine started, so these are emulated 16 GB cards. Q5 with the second card as expert cache keeps about 60% of the 32 GB speed; giving the experts more room by shortening the context or moving KV to RAM adds 1 to 2 tok/s.
Data
configuration 4K prefill / decode 32K prefill / decode 116K prefill / decode
2 × 32 GB, UD-Q5_K_XL, expert cache 542 / 69.3 1048 / 62.0
2 × 16 GB, Q8_0, expert cache 343 / 31.3 636 / 28.1 639 / 27.8
2 × 16 GB, UD-Q5_K_XL, expert cache 445 / 40.4 796 / 37.7 784 / 35.0
2 × 16 GB, UD-Q5_K_XL, expert cache, 128K context 448 / 39.8 874 / 39.0 887 / 37.4
2 × 16 GB, UD-Q5_K_XL, expert cache, --kv-resident 32768 336 / 40.7 725 / 38.5 770 / 35.1
2 × 16 GB, UD-Q5_K_XL, layer split, --adapt-swaps 0 446 / 29.3 1335 / 31.6 1571 / 30.2
2 × 16 GB, UD-Q4_K_XL, expert cache 561 / 53.0 891 / 47.0 929 / 44.4
  • Best setting: UD-Q5_K_XL, second card as expert cache, full 262K context. Precision indistinguishable from Q8_0, decode 35 to 40 tok/s, about 60% of the same quant on the 32 GB cards.
  • Cutting the context to 128K, or keeping part of the KV cache in RAM, frees room for experts and raises the hit rate, but adds only 1 to 2 tok/s.
  • A layer split doubles long-prompt prefill (1,571 against 784 tok/s at 116K) and loses about 20% of decode. Its precision was not measured on a healthy node.
  • Q4 is about 30% faster again, at the precision cost above.

What this settles, and what it does not

  • When a mixture-of-experts does not fit in VRAM, the second card is worth more as an expert cache than as half a layer split. At 16K: one card 18.0 tok/s, layer split 26.7, expert cache 57.9. This does not contradict note 02. There a dense model was layer-split; here the second card reduces the expert bytes that cross PCIe for every token.
  • Engine configuration on one file can cost as much precision as a quantisation level. With a resident RAM budget Strata added about 0.021 KL; without one, 0.0024. Precision has to be measured per configuration, not per engine.
  • llama.cpp's speed problems here were I/O, not arithmetic. Lazy reads of a large table and NUMA placement mattered more than any kernel setting.
  • Under agent traffic, route by conversation. Least-busy routing spent three quarters of prefill re-reading prefixes that were already cached elsewhere.
  • Warm a large memory-mapped table after the weights have loaded, not before. Loading the experts evicted it and cost an order of magnitude in decode.

Not measured: what Q8_0 costs against BF16, precision on token-by-token generation and at long context, accumulated error from the int8 KV cache, anything beyond 262,144 tokens, and real 16 GB cards rather than emulated ones.

Previous: 720p video in five minutes · All V100 notes · Local Host Lab

Ask about these notes

A small agent with three tools — search, read, outline — over the 44 sections of these notes. It looks things up before answering and links the section it read. It will tell you when the notes do not cover something rather than guess.

Grounded only in these pages, and it cites the section it read. A question costs your browser about a third of a second of arithmetic — that is the spam gate, and it stores nothing.