BigMoeOnEdge/docs/bench-data/2026-07-24-desktop-qwen36/findings.md
Helldez fbdf755561 docs(bench): record the 2026-07-24 desktop campaign — the bottleneck flips
Qwen3.6-35B (22.3 GB, ~1.5x RAM) on a Windows x86 laptop, engine
unmodified: baseline 4.78 tok/s, best recipe --overlap --cache-mb auto
--drop-cold-experts 0.75 = 7.33 (+55%), above the phone's 5.0-5.8 on
the same model.

Streamed decode is DRAM-bandwidth-bound there, the opposite of the
phone: compute sits at ~0.11 s/token in every cell (a ~9 tok/s ceiling
at zero I/O), doubling compute threads buys 2 ms, and io8 = io4 to the
millisecond once the cache is fixed — round 1's apparent +17% from
lanes was entirely a --cache-mb auto budget confound (5.3-7.4 GiB
across cells), recorded with its resolution. Aggregate flash read
stays ~900 MiB/s on a ~3 GB/s NVMe regardless of lanes, and with
overlap on it drops to ~330 MiB/s: async reads compete with FFN
compute for the same DRAM bandwidth.

README gets a dedicated Desktop section (table + the flip) replacing
the old one-line quick check; raw CSVs and findings.md land under
docs/bench-data/2026-07-24-desktop-qwen36/.
2026-07-24 10:34:14 +02:00

4 KiB
Raw Blame History

Desktop (x86) streaming — Qwen3.6-35B on a 16 GB laptop (2026-07-24)

Verdict: the engine runs unmodified on a Windows x86 laptop, and the bottleneck flips. On the phone streamed decode is I/O-bound; here it is DRAM-bandwidth-bound in compute (~0.11 s/token floor in every cell → a ~9 tok/s ceiling at zero I/O). io lanes and compute threads are both dead levers — the live ones are cache budget, --drop-cold-experts, and --overlap. Best recipe: --overlap --cache-mb auto --drop-cold-experts 0.75 = 7.33 tok/s, +55% over the 4.78 baseline and 80% of the DRAM ceiling.

Setup

Host build (bmoe-cli.exe, Release, engine at 0.15.1), Windows 11, an 8-core x86-64 laptop (2021 class): 14.8 GiB usable RAM, dual-channel DDR4, NVMe SSD (~3 GB/s class). Qwen3.6-35B-A3B-Q4_K_M (22.3 GB ≈ 1.5× RAM, qwen35moe, 40 layers, 256 experts, top-k 8). Same prompt everywhere, greedy, 256-token generations, 60 s between cells, nothing else running. Defaults unless stated: --moe-stream --dense-weights anon --io-threads 4 -t 4, overlap and prefetch off. Single runs, not best-of — treat absolute numbers as indicative.

Round 1 — exploratory (cache auto): a confound, caught

Cell tok/s compute s/tok I/O s/tok hit cache budget
A baseline io4·t4 4.78 0.116 0.083 84.5% 5281 MiB
B io8 5.61 0.116 0.057 89.0% 7128 MiB
C drop-cold 0.75 6.82 0.112 0.032 92.4% 7420 MiB
D t8 6.14 0.107 0.050 89.3% 7305 MiB

--cache-mb auto sized itself differently per run (5.3–7.4 GiB, tracking free RAM), so B and D are confounded: their gains could be the varied knob or the extra cache. Round 2 pins it.

Round 2 — fixed cache (--cache-mb 5000): lanes and threads are dead

Cell tok/s compute s/tok I/O s/tok hit
E io4·t4 4.72 0.115 0.088 83.5%
F io8·t4 4.63 0.117 0.089 83.5%
G io4·t8 4.68 0.113 0.089 83.3%
H io8·t8·drop 0.75 6.33 0.107 0.046 89.1%
  • Lanes: dead. io8 = io4 to the millisecond; aggregate read bandwidth is pinned at ~900 MiB/s on a ~3 GB/s disk with either lane count. B's +17% was entirely the cache budget. The phone-side "flash saturates at 2 lanes" finding holds on NVMe too, for this read pattern.
  • Threads: dead. Doubling compute threads moves compute 0.115 → 0.113. The GEMV work is limited by DRAM bandwidth, not cores — which is also why the ~0.11 s/token compute floor is identical in every cell of both rounds.

Round 3 — overlap

Cell tok/s compute s/tok I/O stall s/tok hit
I overlap, cache 5000 5.14 0.115 0.068 83.5%
J overlap + cache auto + drop 0.75 7.33 0.107 0.026 92.3%

Overlap hides part of the I/O behind FFN compute (+9% on the plain baseline, not all of it — at 83% hit there is more I/O than compute to hide it behind). On the full recipe the residual stall is 0.026 s/token: the token costs 0.136 s of which 0.107 is the compute floor. With overlap on, the reported flash bandwidth drops to ~330 MiB/s — the async reads compete with compute for the same DRAM bandwidth, which is the wall itself showing up in a second gauge.

What this changes

  • Desktop >RAM streaming works out of the box and beats the phone on the same model (7.3 vs 5.0–5.8 tok/s), but for the opposite reason the phone numbers happen: I/O is nearly free to fix, compute is not.
  • To break the ~9 tok/s ceiling on this class of machine the levers are outside today's scope: dense weights on a GPU, or a lower expert quantization.
  • --drop-cold-experts 0.75 transfers cleanly from the phone (+43% alone, 6% of routings dropped at auto cache). Quality was not re-measured here; the phone-side GSM8K result is the only quality evidence.

Files

pc-A…D are round 1 (cache auto), pc-E…H round 2 (cache 5000), pc-I/J round 3 (overlap). The # moe_stream=… preamble in each CSV records the exact configuration.