ggml : --prefetch-experts-slots — lookahead H2D prefetch of host-resident MoE experts - #28414
leshchukandrej wants to merge 1 commit into
Conversation
…-resident MoE experts During prefill the scheduler must upload each expert's weight tensor from host (or system RAM, via --n-cpu-moe / auto-offload fit margin) to the GPU right before its MUL_MAT_ID split launches, serializing H2D behind compute. With large batches every expert is exercised, so routing ids offer nothing worth waiting for; prefetch instead uploads full expert tensors through a second backend instance on the same device into rotating staging slots while the current split computes (1-deep lookahead), then the consuming split does a per-split cross-stream event wait that is already satisfied by launch time. - new flag --prefetch-experts-slots N (default 0 = off; >=2 = full-tensor prefetch with 1-deep lookahead; recommended 3; capped at 4) - GPU staging cost = slots * max expert tensor, lazy-allocated on first fire and gracefully disabled if the device lacks async/event caps or allocation fails - decode is unaffected: fires are gated on MUL_MAT_ID splits with batch >= 2*n_expert (prefill-scale) and are skipped entirely in callback_eval mode - lossless: prefetch only changes WHEN the bytes land on device - the staged copy carries the same host weights and the consuming kernels run unchanged after the ready-event wait; with slots = 0 no code path changes at all - measured TTFT/prefill speedups on host-expert configs (24B A3B ncmoe 20: -11% at ~42k-token prompt, -20% at 200 tokens; 21.8GB 35B A3B auto-offload: -22% at ~42k tokens) with flat decode and unchanged output
|
Hi @leshchukandrej, thanks for your contribution! Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:
Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below. |
|
/bot review |
Automated code reviewReview of PR #28414 ( Blocking(point 1) Staging state is per-scheduler but devices are per-split: multi-GPU layer-split is silently broken. (point 2) Disable/realloc while a lookahead fire is outstanding leaves dangling slot pointers. Once (point 3) Missing (point 4) Comment content and non-ASCII characters. The repeated "mindcontrol-port" / "mindcontrol prefetch-wait A/B verdict" / "the only mode that preserves tool_choice semantics" / "tool_calls" references (also in the public headers (point 5) Scope gate: this is a new scheduler subsystem with no prior discussion. This adds ~300 lines of cross-stream pipelining machinery to the core scheduler, plus new public surface ( Will slow the review(point 6) Dead debug machinery should be removed. (point 7) Unconditional per-step heap allocation. (point 8) Leak when the slot count is reduced. In the (point 9) Per-fire rescan. (point 10) Benchmark data is confusing. The "Lossless" table shows ON being slower (3501 ms off vs 3786 ms on) while the "Measured effect" table shows improvements, and all numbers come from a downstream fork build ("beellama v0.4.5"), not this tree. Maintainers will ask for vanilla-build numbers, so having them ready (and clarifying the first table) will save a round trip. Nits(point 11) (point 12) The overall design (second backend instance + rotating staging slots + ready/free event pairs) is a reasonable shape for the problem, and the off-by-default gating plus the per-split event wait are the right instincts. But the multi-GPU staging-device issue (point 1) and the disable-while-outstanding UAF (point 2) are correctness bugs that must be fixed, and points 4/6 alone will roughly halve the diff. This review was generated automatically by pi coding agent using |
|
Cool approach. The 1-deep lookahead with a second CUDA stream is a clean way to hide PCIe latency without changing the graph topology. Keeping it prefill-only means decode performance is guaranteed flat. A couple of things I'm curious about:
Byte-identical output with slots=0 is the right design. Curious if you've seen this help with models beyond qwen4exp. Mixtral-style routing where the expert set is smaller but more frequently repeated is one I'm wondering about. |
|
I have tested it on 35b moe only. 4 as a cap is found on different tests to
be optimal as after 3, rge gain is much smaller but the vram usage bigger.
30% is a real gain, after that it bis almost non visible. Probably
limitation of the card as it is almost 100% saturated on 3 comparing to
50-60% when feature off
…On Mon, Sep 7, 2026, 13:35 XBold ***@***.***> wrote:
*XBold* left a comment (ggml-org/llama.cpp#28414)
<#28414 (comment)>
Cool approach. The 1-deep lookahead with a second CUDA stream is a clean
way to hide PCIe latency without changing the graph topology. Keeping it
prefill-only means decode performance is guaranteed flat.
A couple of things I'm curious about:
1.
*Why cap at 4 slots?* For very long prefill batches (your ~42k tok
test showed 22% improvement), the expert routing set is broader and the gap
between compute and transfer is largest. Did you test with 8+ slots and
find a point of diminishing returns, or is it a conservative cap to keep
staging memory bounded?
2.
*Self-disable on allocation failure* is smart for correctness, but in
practice on a card like the 5070 Ti (12 GB), what's the failure mode? Is it
not enough VRAM for staging, or CUDA stream and event creation failing?
Worth documenting the typical failure conditions so users know when it
silently falls back.
3.
*The ids->ne[0]*ids->ne[1] >= 2*n_expert prefill-scale gate* is a
heuristic to avoid firing on short prompts where the prefetch overhead
outweighs the benefit, or is there a deeper correctness concern?
Byte-identical output with slots=0 is the right design. Curious if you've
seen this help with models beyond qwen4exp. Mixtral-style routing where the
expert set is smaller but more frequently repeated is one I'm wondering
about.
—
Reply to this email directly, view it on GitHub
<#28414?email_source=notifications&email_token=AIUMH6YXMBY2AP3UNN6THUT5N4EXNA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNJXGQ4TINBRGAYKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5574944100>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AIUMH62VFBKJVIYEVGNYVDD5N4EXNAVCNFSNUABFKJSXA33TNF2G64TZHM3DCMRTGU2DOOBUHNEXG43VMU5TKMZVGI4DONZVGYZ2C5QC>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Confirming blocking point 1 from the automated review with an end-to-end repro on CUDA. The failure is silent wrong output, not a crash, and it is easy to mistake for a model or quant problem. Setup: Windows, llama.cpp master Symptom: short answers are correct, then any generation past a few hundred tokens becomes a solid run of Two things made this hard to attribute:
It reproduces with two devices, so it is not specific to three, nor to the mixed sm_86 card. Configurations tested, all On this model the feature was also not a win. A 96k token prefill measured 135.0 tok/s with |
|
It suppose to give the gain only for the moe and only if the gpu is not
oversaturelsted already. Also, the values to use are 2, 3, 4. 1 suppose to
be treated as off or it may corrupt the output if not
…On Mon, Sep 7, 2026, 14:03 1jeff ***@***.***> wrote:
*1jeffchristensen* left a comment (ggml-org/llama.cpp#28414)
<#28414 (comment)>
Confirming blocking point 1 from the automated review with an end-to-end
repro on CUDA. The failure is silent wrong output, not a crash, and it is
easy to mistake for a model or quant problem.
Setup: Windows, llama.cpp master 465e49b plus this PR, CUDA 13.3, 2x
RTX 5060 Ti (sm_120) and 1x RTX 3060 (sm_86). Model
sh0wie/Qwen3.8-Flash-Next-REAP-288-GGUF Q4_K_M (qwen4exp, 288 experts).
Served with -sm layer and -ncmoe, so the experts are host resident.
Symptom: short answers are correct, then any generation past a few hundred
tokens becomes a solid run of / for the whole token budget. With --prefetch-experts-slots
0 the identical command is correct every time.
llama-server -m <model> -ngl 99 -c 8192 -sm layer -ts 3,1 -mg 0 -ncmoe 26 \
--cache-type-k q8_0 --cache-type-v q8_0 --prefetch-experts-slots 3
# "What is 17*23? Answer with just the number." -> "391", correct
# "Write at least 900 words about ..." -> 1400 tokens, every one of them "/"
# same command with --prefetch-experts-slots 0 -> 1363 tokens of correct prose
Two things made this hard to attribute:
1. It is masked by allocation failure. When the staging buffer cannot
be allocated the feature disables itself and output is correct, so the
configurations with the least free VRAM were the ones that looked healthy,
and freeing VRAM broke them. At ctx 163840 with -ncmoe 26 the 478 MiB
staging allocation failed and every task passed. The same two GPUs at the
same context with -ncmoe 30, which leaves about 3.5 GiB more free on
device 0, allocated successfully and produced only /. That is the
reverse of how an out of memory line in the log usually reads.
2. A short smoke test does not see it. The arithmetic answer is
correct, and even a 180 token paragraph is correct. Detection needs a few
hundred generated tokens.
It reproduces with two devices, so it is not specific to three, nor to the
mixed sm_86 card. Configurations tested, all -sm layer with host resident
experts: CUDA0+CUDA1, CUDA0+CUDA2, and all three together, at ctx 8192,
32768, 65536 and 163840. Every one that did not hit the staging allocation
failure produced /.
On this model the feature was also not a win. A 96k token prefill measured
135.0 tok/s with --prefetch-experts-slots 3 against 140.4 tok/s with it
off, same placement, same build.
—
Reply to this email directly, view it on GitHub
<#28414?email_source=notifications&email_token=AIUMH6ZELSDHUJYBCS2KPZD5N4IAFA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNJXGUYTMMJUGI22M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5575161425>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AIUMH653GODVQVWPAIVK3DT5N4IAFAVCNFSNUABFKJSXA33TNF2G64TZHM3DCMRTGU2DOOBUHNEXG43VMU5TKMZVGI4DONZVGYZ2C5QC>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
What was the b/ub param set?
On Mon, Sep 7, 2026, 15:14 Andrii Leshchuk ***@***.***>
wrote:
… It suppose to give the gain only for the moe and only if the gpu is not
oversaturelsted already. Also, the values to use are 2, 3, 4. 1 suppose to
be treated as off or it may corrupt the output if not
On Mon, Sep 7, 2026, 14:03 1jeff ***@***.***> wrote:
> *1jeffchristensen* left a comment (ggml-org/llama.cpp#28414)
> <#28414 (comment)>
>
> Confirming blocking point 1 from the automated review with an end-to-end
> repro on CUDA. The failure is silent wrong output, not a crash, and it is
> easy to mistake for a model or quant problem.
>
> Setup: Windows, llama.cpp master 465e49b plus this PR, CUDA 13.3, 2x
> RTX 5060 Ti (sm_120) and 1x RTX 3060 (sm_86). Model
> sh0wie/Qwen3.8-Flash-Next-REAP-288-GGUF Q4_K_M (qwen4exp, 288 experts).
> Served with -sm layer and -ncmoe, so the experts are host resident.
>
> Symptom: short answers are correct, then any generation past a few
> hundred tokens becomes a solid run of / for the whole token budget. With --prefetch-experts-slots
> 0 the identical command is correct every time.
>
> llama-server -m <model> -ngl 99 -c 8192 -sm layer -ts 3,1 -mg 0 -ncmoe 26 \
> --cache-type-k q8_0 --cache-type-v q8_0 --prefetch-experts-slots 3
> # "What is 17*23? Answer with just the number." -> "391", correct
> # "Write at least 900 words about ..." -> 1400 tokens, every one of them "/"
> # same command with --prefetch-experts-slots 0 -> 1363 tokens of correct prose
>
> Two things made this hard to attribute:
>
> 1. It is masked by allocation failure. When the staging buffer cannot
> be allocated the feature disables itself and output is correct, so the
> configurations with the least free VRAM were the ones that looked healthy,
> and freeing VRAM broke them. At ctx 163840 with -ncmoe 26 the 478 MiB
> staging allocation failed and every task passed. The same two GPUs at the
> same context with -ncmoe 30, which leaves about 3.5 GiB more free on
> device 0, allocated successfully and produced only /. That is the
> reverse of how an out of memory line in the log usually reads.
> 2. A short smoke test does not see it. The arithmetic answer is
> correct, and even a 180 token paragraph is correct. Detection needs a few
> hundred generated tokens.
>
> It reproduces with two devices, so it is not specific to three, nor to
> the mixed sm_86 card. Configurations tested, all -sm layer with host
> resident experts: CUDA0+CUDA1, CUDA0+CUDA2, and all three together, at ctx
> 8192, 32768, 65536 and 163840. Every one that did not hit the staging
> allocation failure produced /.
>
> On this model the feature was also not a win. A 96k token prefill
> measured 135.0 tok/s with --prefetch-experts-slots 3 against 140.4 tok/s
> with it off, same placement, same build.
>
> —
> Reply to this email directly, view it on GitHub
> <#28414?email_source=notifications&email_token=AIUMH6ZELSDHUJYBCS2KPZD5N4IAFA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNJXGUYTMMJUGI22M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5575161425>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AIUMH653GODVQVWPAIVK3DT5N4IAFAVCNFSNUABFKJSXA33TNF2G64TZHM3DCMRTGU2DOOBUHNEXG43VMU5TKMZVGI4DONZVGYZ2C5QC>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>
|
|
Defaults on every run in that report: Agreed on both of your points about where the feature applies, and I think they actually sharpen the bug rather than explain it away. It is a MoE, and it is served with So when the card is full enough that the staging allocation fails, the feature disables itself and everything is fine. Giving it about 3.5 GiB more headroom on device 0 is what breaks it. That is consistent with the static finding: the slots are allocated once from the first split's device, and a split on the other device then reads a foreign pointer. Single device never hits it, which fits your 35B results. One correction to my earlier comment, which I stated more flatly than my data supports. I said the throughput comparison was on the same build. It was not. The 135.0 tok/s with the flag was one binary and the 140.4 without it was another, the second adding For what it is worth, on this box the prefill lever that did pay was ubatch: |
|
Given my prior tests with prefetching experts, I'm not considering any PRs of this sort unless someone can clearly show me that this beats purely using |
|
@pwilkin again, this pr boosts the partial resident in RAM, not SSD. It allows the CPU to compute @1jeffchristensen regarding the multigpu setup, I dont have it so I cannot test the behavior unfortunately. Most likely there is an issue with that |
|
@1jeffchristensen yes, reread the implementation and it will not work in the current implementation for the multigpu units, unfortunately. You might need to do some work on top of it to make ot work |
|
Thanks for putting this together — a one-split lookahead on a second stream with rotating staging slots is exactly the shape we arrived at independently last week on a single AMD card, so flagging our numbers and one thing I noticed in the diff, in case they're useful. Nothing is urgent on our side. Setup. Radeon AI PRO R9700 32 GB (gfx1201), ROCm 10.0, PCIe 5.0 x16, Ryzen 9 9900X, 128 GB DDR5. Qwen3.8-Flash-Next UD-Q4_K_XL (512 experts, top-10, 48 MoE layers, ~72 GB of expert weights), Prefill t/s (warm, single request):
Decode unchanged (24.9–25.6 t/s); greedy output identical to the stock path over 5 prompts × 3 repetitions. 3 → 4 slots is +8% and 4 → 6 is nothing, which matches your "diminishing after 3". Two ROCm-specific data points: a hipMemcpy microbench here gives 53 GB/s H2D from pageable memory as well as pinned (the runtime pins on the fly), so the overlap works with plain One thing in the diff to double-check. The slot is allocated with Smaller data point: at prefill scale the ids read-back that decides which experts to copy is itself a device sync per MoE layer; copying the whole tensor asynchronously without it was worth ~+11% on its own before any overlap, consistent with your Happy to re-run anything on this ROCm single-GPU setup if you revise the branch — we can't help with the multi-GPU case, unfortunately. (Disclosure: the measurements and our implementation were done with Claude Code assisting, and this comment was drafted with it and reviewed by me.) |
|
Follow-up with data from running your branch itself on the ROCm box (ported onto our tree at Correctness: greedy outputs identical to the stock copy path on every run (5 prompts × repetitions), at 2, 3 and 4 slots. No issues. Speed on ROCm, mmap'd weights (prefill t/s, 27k / 64k): stock 1,014 / 1,079 → 2 slots 1,059 / 1,051 → 3 slots 1,319 / 1,292 → 4 slots 1,318 / 1,291. Decode unchanged. With The timeline (GPU timestamps around every large H2D copy and every split's kernels; the same per-layer pattern repeats in every layer, times in ms):
Two cheap ways around it, if you want ROCm+mmap to get the full effect: issue the consumer's Correction on the padding point: zeroing the slot's tail once at allocation is not enough, because slots rotate between tensors of different sizes (a 629 MB One more VRAM note: since every slot is sized to the largest tensor (891 MB here), 3 slots cost ~2.7 GB and 4 didn't fit next to the 96-slot cache; it doesn't affect the design, just the sizing advice. (As before: measurements and the port were done with Claude Code assisting; drafted with it and reviewed by me.) |
Snapshot of the pre-rebase branch: machine setup, the unmerged PRs it carries, why ggml-org#28414 is reverted, and the pp/tg numbers measured with this build (the baseline the later speedup-2026-09-20 numbers are compared against). Links to the follow-up branch README.
Snapshot of the pre-rebase branch: machine setup, the unmerged PRs it carries, why ggml-org#28414 is reverted, and the pp/tg numbers measured with this build (the baseline the later speedup-2026-09-20 numbers are compared against). Links to the follow-up branch README.
Snapshot of the pre-rebase branch: machine setup, the unmerged PRs it carries, why ggml-org#28414 is reverted, and the pp/tg numbers measured with this build (the baseline the later speedup-2026-09-20 numbers are compared against). Links to the follow-up branch README.
Snapshot of the pre-rebase branch: machine setup, the unmerged PRs it carries, why ggml-org#28414 is reverted, and the pp/tg numbers measured with this build (the baseline the later speedup-2026-09-20 numbers are compared against). Links to the follow-up branch README.
--prefetch-experts-slots: lookahead H2D prefetch of host-resident MoE expert weightsBranch:
port/prefetch-experts-slots(head:leshchukandrej/beellama.cppfork) — commit on top of currentmasterScope: 8 files, +327 / −0, single feature
Flags:
--prefetch-experts-slots N(CLI) /llama_context_params.prefetch_experts_slots/ggml_backend_sched_set_prefetch_experts_slots()Problem
When MoE expert weights are not GPU-resident (
--n-cpu-moe N/--cpu-moe, orhost pages when the model does not fully fit VRAM), the scheduler uploads each
used expert tensor host→device right before the
MUL_MAT_IDsplit that consumesit. In the common single-context case (
n_copies == 1) that H2D transfer is notoverlapped: each split boundary synchronizes, so large prefill batches wait on
PCIe/NVLink instead of computing. Long-prompt prefill TTFT is dominated by this
serial upload cost.
What this does
During prefill, expert weight uploads are issued one split ahead of need
(1-deep lookahead) through a second backend instance on the same device
(a separate CUDA stream), into rotating staging buffers. The consuming split
performs a per-split cross-stream event wait that is already satisfied by launch
time — so the H2D bytes arrive during compute, not after it.
Design
slots == 0(default): feature fully off — no state, no allocations, thescheduler behaves exactly as before.
slots >= 2: pipeline on. Staging memory =slots * max_expert_tensor,lazily allocated on first use.
3is recommended; capped at 4.GGML_OP_MUL_MAT_IDwithhost-resident weight inputs (
GGML_BACKEND_BUFFER_USAGE_WEIGHTS,is_host),and only at prefill scale (
ids->ne[0]*ids->ne[1] >= 2*n_expert). Atdecode-scale batches the stock per-split copy of used experts is kept —
routing ids carry information there, and decode is unaffected by construction
(
callback_evalmode never fires).async+eventscaps (CUDA); on any allocation/cap failureor with an eval callback installed the feature disables itself and the regular
copy path is used. Correctness never depends on prefetch.
duration of its split and restored right after graph launch, and the
ready-event wait orders the copy before the kernels. Gated per-split (not
per-graph) so
tool_choicesemantics are preserved.Lossless
Prefetch changes when bytes land on device, never what is computed: the same
host weights are copied, the consuming graph/kernels are unchanged, and the
event wait guarantees completion before launch.
slots = 0leaves every codepath untouched.
Empirically (temp 0, same prompt/seed,
--n-cpu-moe 20on a 24B A3B, q4_0 KV):Measured effect
Large-prefill TTFT is the target; decode is flat (untouched by design).
Benchmarks below measured on a downstream (beellama v0.4.5) build carrying this
byte-identical code, on an RTX 5070 Ti (12 GB) with host-expert configs:
-ncmoe 20, q4_0 KV-ncmoe 20, q4_0 KVNotes: with
-ncmoe 20~20 layers' experts are host-resident; the 35B row usedno
-ncmoe— auto-offload alone leaves half the experts host-resident, therealistic deployment target. The feature is off by default, so no-one pays the
(fixed, ~prefill-only) staging cost unless they opt in.
Files
common/arg.cppcommon/common.{h,cpp}common_paramsplumbinginclude/llama.hllama_context_paramsfieldsrc/llama-cparams.hsrc/llama-context.cppsched_reserve()ggml/include/ggml-backend.h,ggml/src/ggml-backend.cpp