Repository navigation
hybrid MoE strategy 20-40x slower than offload on an offload-heavy box (24 GB card, PCIe gen 3) #436
Description
Summary
On a single 24 GB card with the routed experts offloaded, --moe-strategy hybrid decodes at 0.17-1.02 tok/s. The same tree, same checkpoint, same box, with only --moe-strategy offload instead, decodes at 23.07 tok/s — about 20-40x faster. GPU utilisation during generation is 0% under hybrid and 85% under offload.
hybrid is not a niche choice here: --moe-strategy auto resolves a MoE model to hybrid when a ft bench bw profile recommends it, which it does on this box, and hybrid is what made this configuration fast in the first place — the same box on v0.1.2 serves this checkpoint at 33.5 tok/s under hybrid.
Numbers
Single RTX PRO 4000 Blackwell 24 GB (sm_120), PCIe gen 3 x16, EPYC 7302, CUDA 13.0, torch 2.11.0+cu130, TP=1, lovedheart/Qwen3.8-Flash-Next-NVFP4-FP8 (123 GiB, NVFP4 routed experts + block-FP8 dense), --moe-cache-auto, --kv-reserve-tokens 65536, --moe-prefill-hit-d2d.
| tree | --moe-strategy |
decode (end-to-end, every streamed delta) | engine-reported decode | GPU util |
|---|---|---|---|---|
v0.1.2 + local patches |
hybrid |
33.5 tok/s | 33.4 - 38.9 | 74-95% |
main + #427 + #428 (b16877b) |
hybrid |
never completed a 3-prompt run | 0.17 / 0.43 / 0.55 / 0.56 / 0.74 / 1.02 | 0% |
main + #427 + #428 (b16877b) |
offload |
23.07 tok/s | 21.3 - 25.9 | 85% |
The hybrid figures come from three independent launches of the new tree, so they are not a cold-start artifact. Both new-tree rows are the same install; the only difference is the strategy flag.
What it looks like while it is slow
- GPU 0% during generation, weights resident at 21,962 MiB.
- One CPU thread pinned at ~100% (
state=R,wchan=0, ~1,034 s of CPU accumulated) while the other 76 threads sit idle — including the wholeCPU MoE executor ready: threads=15 (pinned to cores 0..14)pool. So neither the GPU nor the CPU expert pool is doing the work; something is spinning on the engine's main thread. - Under
offloadon the identical install that same thread reads 3.0% CPU,state=Ssl.
That pattern points at the hybrid split itself. Startup logs --moe-hybrid-max-fetch auto: fetching 27.8% of each decode step's expert misses over PCIe (benched PCIe/CPU bandwidth ratio), the rest on the CPU — and it is exactly the "rest on the CPU" half that appears not to reach the pool.
Ruled out
- Not JIT compilation.
~/.triton/cachestopped growing (4,015 files / 145 MB, unchanged over a 30 s sample) while that thread burned CPU. - Not expert-cache starvation.
--moe-cache-autoresolvesmoe_cache_size=3741 num_pages=1025, versus 4041 on the old build — 7% down, nowhere near enough. - Not the deprecated flags. Re-ran with
--nvfp4-backend tritonremoved entirely (quant_backend=None); it still resolvesMoE experts: nvfp4 via tritonand is still 0.55 tok/s. Theoffloadrun above also has that flag removed. - Not the loader. feat(qwen4_exp): load block-fp8 dense projections natively #428's reader loads this checkpoint correctly — all buffers match, output is correct, and the
offloadrun proves the same install serves it fine.
Bisect status
I have not bisected. My baseline is v0.1.2 plus local patches rather than upstream main, because main cannot load this checkpoint at all without #428, so the window is v0.1.2..b16877b — roughly 34 commits including the #418 quantization refactor. Each cycle here is a ~5 minute install plus load, so it is a few hours; happy to do it if that is the fastest way to close this out, or to run any specific commit, flag set, or instrumented build you want to point at.
I could not get a Python stack: the box is ptrace_scope=1 and running the engine under py-spy record --subprocesses attached and sampled but never flushed before the window closed.
Why this may not show elsewhere
hybrid only carries real load when the experts genuinely do not fit. On an 80 GB H100 or 2 x 48 GB Ada this model is close to resident and the CPU-assist half of the split barely engages, so the same code path is nearly free. Here it carries 123 GiB of weights against 24 GB of VRAM on a gen-3 board, which is the regime the offload family exists for.
Context: first reported on #428 (#428 (comment)), narrowed to the strategy flag afterwards.
Activity
jason-fxz commentedon Sep 11, 2026
Thanks for the write-up. Two things would help us narrow this down:
ft bench bwoutput for this box (or the profile file it wrote,~/.cache/freetoken/benchbw.json). The 27.8% fetch split comes from it, and we would like to see the CPU / PCIe numbers behind that.hybridlaunch onb16877b, up toAPI server is ready to serve. The lines we are after are the ones fromCPU MoE executor ready: ...(isa / threads), whethercpu-moe flag handshake unavailable: ...appears, andPLE disk backend: ..., wait-sync|launch-gating.For reference, the same tree and checkpoint on an H100, with
--moe-cache-rate 0.152to match your 3741-slot residency, decodes at 56 tok/s onhybridvs 50 tok/s onoffload, so this is specific to something on your platform rather than the hybrid path itself.gberasmus87 commentedon Sep 11, 2026
All three, plus one comparison that I think narrows it a long way.
1.
~/.cache/freetoken/benchbw.jsonWritten
2026-08-25T00:23:21+00:00,version 4. Ceilings and thenvfp4entry (the one this checkpoint's experts use):ratio 2.29againstthreshold 2.0is where thehybridrecommendation and the 27.8% split come from.isa_sweepisnullon every entry. Happy to attach the whole file if useful —bf16isratio 2.352,ds_fp42.329,mxfp4_triton1.38 -> offload,fp8_blockhas"CPU MoE has no fp8_block weight path; hybrid unavailable".Note the date: this profile predates the problem and both builds below read the same one, so it is a constant across the comparison rather than a variable.
2. Startup,
hybridonb16877b, to "ready to serve"PLE disk backendisio_uring, O_DIRECT, **wait-sync**.3.
cpu-moe flag handshake unavailableNever appears.
journalctl | grep -ci "cpu-moe flag handshake"over every run since 2026-09-09 returns0.The comparison I think matters
My 33.5 tok/s baseline is
v0.1.2+ local patches. I pulled its startup log and put it beside theb16877bone. Every logged parameter is identical:hybrid,qsa_sparse,hybrid_radix, page 64moe_cache_auto3741slots,num_pages=102516 -> 1threads=15, cores 0..14,isa=avx2,fmt=nvfp4io_uring, O_DIRECT, wait-syncSame box, same checkpoint, same flags, same cached bench profile, same resolved everything — 60x apart. So whatever this is, it is not the split, the ISA, the thread layout, the cache sizing or the PLE backend being resolved differently. It is in the code between the two.
Two other things that rule out the obvious:
PLE disk backendiswait-syncon theoffloadrun too, and that run is fine at 23 tok/s. So the PLE path is not the differentiator between the two strategies on this box.torch intra-op threads: 16 -> 1with 15 cores pinned to the MoE pool leaves torch exactly one thread. I suspected that, but the working build does exactly the same thing, so it is not the cause on its own — though it may be what makes this platform sensitive to a change that is free on a box with more cores.Given your H100 numbers (56
hybridvs 50offloadat matched residency), agreed that it is platform-specific rather than the hybrid path as such. The window isv0.1.2..b16877b. I am happy to bisect it here — roughly 34 commits at a ~5 minute install-plus-load cycle, so a few hours — or to run any specific commit or instrumented build you would rather point at first. Say which and I will get you numbers.