Conversation
…ok-ahead design
…kahead Wire the consumer half of docs/moe-lookahead-design.md: a new --moe-lookahead N gate (default 0, env LLAMA_ARG_MOE_LOOKAHEAD) that asks the CUDA MoE cache to prefetch the next layer's expert weights during the current layer's compute. The prefetch path mirrors acquire with is_prefetch=true, wait_for_compute=false and pin=false: no stream synchronize, no compute-stream wait, pinned slots are skipped, expert ids outside [0, n_experts) are ignored, and prefetch telemetry is accounted separately from demand. Host-to-device copies are batched through cudaMemcpyBatchAsync when CUDART >= 12.80, with a per-copy fallback otherwise. Targets whose buffer is not MoE-cached are skipped with a one-shot warning. --moe-lookahead 0 leaves the decode step unchanged. A non-zero width without --moe-expert-cache-size > 0 fails loudly at model load. Adds test-moe-cache --lookahead-prefetch-only and the lookahead prefetch legacy layer case. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
The speculative prefetch path picked its victim with plain LRU, so a prediction could displace an expert that demand had already proven hot. The eviction guard the design requires (docs/moe-lookahead-design.md, "Invariants to preserve" #2 and the colibri safety invariant at c:1462-1478) was missing: only the pin guard - never evict a slot a running GEMM is reading - was implemented, on both the demand and speculative paths. Port colibri's PILOT_EVICT_GUARD. A speculative fill may displace a resident only when that resident is not genuinely warm; a resident is protected when it has at least 2 demand accesses AND is clearly hotter than the prediction, by the 25% + 4-frequency hysteresis in LFRU score units (score ported from colibri c/tier.h:40-43: frequency in the high bits, recency saturating in the low byte). A blocked prediction is dropped rather than forced in, so it can never thrash a demand-loaded expert. Heat is recorded on demand accesses only. Counting predictions would let a speculation inflate the very score that decides whether it may displace a resident, which is the failure colibri hit in ggml-org#490. Refactor the eviction core so the demand and speculative paths cannot drift: ggml_cuda_moe_cache_select_victim_locked picks the LRU unpinned slot and applies the guard for speculative fills, and ggml_cuda_moe_cache_install_fill_locked records the eviction telemetry and publishes the new entry. The copy-batching split in the prefetch path is unchanged, and the free-slot, pin and unknown-eid behaviour is identical. Add phase_prefetch_dropped so a guard that drops everything (colibri ggml-org#490) is visible rather than silent, and extend the test prefetch accessor with it. Add the lookahead prefetch eviction guard case: a 3-slot pool, a warm resident survives a prediction for a never-seen expert (dropped, no copy, no eviction, still a demand hit), while a once-demanded resident is evicted for it. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
…kahead Predict layer il+1's top-k experts from layer il's post-attention state and page them while layer il still computes, so the H2D copy overlaps compute instead of stalling the next layer. Adds GGML_OP_MOE_PREFETCH (an explicit no-op outside CUDA), the graph-level producer, the per-context gate and a reserve-time pool preinstall, plus a debug-only recall instrument. Measured on the rig (Qwen3.8-Flash-Next-APEX-I-Mini, -b 1 -ub 1 so every ubatch is a decode row, --moe-expert-cache-size 84, look-ahead width 8): - decode 37.6 t/s against 43.2 t/s with the feature off, about -13%, because the ids readback synchronizes the stream and forces use_cuda_graph = false. - the MoE grouped-decode certificate rejects the graph unless GGML_OP_MOE_PREFETCH is excluded from the backend use counts, otherwise the run dies with "graph=unproven(14)" and a failed graph compute. - logits are not preserved: PPL 2.9434 against 3.0613, and byte-different dumped logits. Isolation shows the extra MUL_MAT that reads ffn_gate_inp is the trigger; a neutral extra node does not reproduce it, disabling CUDA graphs does not reproduce it, and the prefetch op itself contributes nothing. - every prediction is dropped in this configuration. A pool can only be installed while its group is under legacy cache authority with admission open, and the groups here are not, so preinstall_legacy_pools() fails for all 144 targets and the look-ahead has nothing to page into. Not landable as is. The review PR carries the full isolation matrix. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
…roducer Adds a measured-outcome section to the design note: the three blockers with their numbers, the isolation matrix that pins the trigger to the extra MUL_MAT reading ffn_gate_inp, Q1 answered negatively (the authority regime refuses the cross-layer pool install), the throughput cost of the ids readback, the already-recorded byte-identical claim refuted, and the future direction (early-router copy worker plus device-to-host mailbox) if the track is ever re-approached. Also corrects ffn_gate_up_exps being null, the preinstall point, Q3 and Q4. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
Recall was the one unmeasured number in the park decision. Measured it properly: the horizon is one MoE layer (never a token), and the demand path cannot score it because during decode only blk.47 reaches the host-visible ids read - the rest run in the certified grouped path. Ground truth was therefore built in-graph from each layer's own router matmul on its real FFN input. Width 2/4/6/8/10 gives 97/95/91/86/79% recall of the predicted set against 19/38/55/69/79% coverage of the 10 experts the model actually uses; width 8 is about 22x chance. RTX 3060 reproduces width 8 within noise (85.85% / 68.68%). Also records the two traps that had kept this unmeasured: the committed instrument's GGML_LOG_INFO never reaches the server log at default verbosity, and cached-path scoring only ever sees the one layer holding a legacy lease. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
…rence The design cites colibri's 71.6% PILOT recall on GLM-5.2 as the expectation for this mechanism. Records that the producer measures 91.50% at width 6 and 86.21% at width 8, with the explicit caveat that the models and expert counts differ so the comparison is not like-for-like. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
…design invariant preinstall_legacy_pools returned -1 for all 144 targets and every prediction was dropped. Recording the ruling so it is not re-litigated: the acquire_legacy_cache new-record latch (authority == LEGACY && !admission_closed) binds pool creation to a certified execution, and an unauthenticated install would let a caller claim VRAM pools certification never proved - the overcommit class README.md:25 warns about. L+1 is never the authoritative group while L runs, so cross-layer install is unreachable by design, not broken. If the track reopens the seam is authority publication at graph reserve/certification time, which is a certification-contract change needing its own review. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
…evice-side) Reviewer ruling on how blocker 3 would be fixed if the track reopens. Mailbox approved as the transport (it reuses the shipped early-router copy worker plus the async paging half); the FreeToken-style device-side gather is rejected because it needs device-resident slot management and would be a second cache implementation, reintroducing the demand/speculative drift that select_victim_locked and install_fill_locked eliminated. Deleting the use_cuda_graph=false rule alone does not suffice: the op is capture-incompatible as written (pageable D2H memcpy, stream sync, host-side slot booking), and a captured cuStreamWaitValue32 bakes an expected value, so it must be refreshed per step or replaced by a polling kernel. Minimal change set, required guards, and the unchanged sequencing (census exclusion #128 -> authority publication -> transport) are recorded. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
Two env-gated diagnostics, off by default, kept for troubleshooting the look-ahead track: - GGML_CUDA_MOE_PHASE_PROBE=1 attributes each step's GPU time to op classes (attention ops, the MoE expert path, the per-layer embedding read, dense compute, plumbing), separates decode from prefill by inter-step wall time, reports the dispatch mode that produced each step, and prints a per-layer table. One CUDA event pair per maximal run of same-(phase, layer) nodes, read back once per step after a single stream sync (~2.6% of decode throughput on the RTX 3060). - GGML_CUDA_MOE_LOOKAHEAD_DEBUG=1 scores the recorded prediction against the ids the router actually selected, for every MUL_MAT_ID node (the ids are in src[2]), i.e. off the legacy blk.47 lease, and reports recall and precision plus the used-not-predicted population. The other two populations are cache counters that were counted but never printed; they now appear on the moe-cache-phase line as prefetch_dropped and evicted_prefetched_unused. Both probes turn themselves off with a single stderr note if an event or a memcpy cannot be issued, which is what happens under CUDA graph capture, so a diagnostic cannot break a capture run; during replay the host never visits the dispatch loop either, so an attribution needs GGML_CUDA_DISABLE_GRAPHS=1. With the env vars unset the only added work is a cached getenv lookup behind a static bool. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
…sults Instrument A on the RTX 3060 at ctx 81920 / cache 42: a decode step is 69% MoE execution, 25% dense compute, 1.5% attention ops and 0% per-layer embedding, with the phase rows summing to the step time; prefill is 88% MoE. In the same window SM sits at 95-99% and the DRAM controller at 19-26%, the PCIe link is gen4 x4 carrying 337.6 MiB of expert traffic per token at ~47% of its ceiling, and the grouped telemetry reports every staged copy already complete, so the limiter is MoE kernel execution rather than PCIe or VRAM bandwidth. Instrument B, scoring predictions in the dispatch loop instead of the legacy blk.47 lease: 64.5% of the used experts were predicted and 80.6% of the predictions were used, the same order as the earlier in-graph measurement. The other two populations are structurally zero in this configuration because the consumer is inert (decode phase line ops=0, legacy cache authority printed once), so every prediction is simply unused. Also records the independent finding that the rig's -ot per_layer_token_embd=CPU is redundant and inert. Assisted-by: Oh My Pi (deepseek-v4.1-flash)
Owner
Author
|
Measured addendum from the same 3060 run, because it changes what the consumer is worth: During prefill the legacy lease path installs 122,327 speculative slabs, uses 4,720 of Two consequences:
Blocker status is unchanged: the producer is still not landable. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR-P1: look-ahead prefetch producer (NOT LANDABLE)
Base is
feat/763-reconcile-qwen4exp-mtp(PR #120 head), the same deviation PR #126took for the llama.cpp-side consumer, as accepted by the reviewer.
This branch contains the complete producer implementation plus the measured blockers.
It must not merge in this state. The three blockers below are all reproducible,
deterministic and measured on the rig; the first one is a correctness blocker.
What is implemented
GGML_OP_MOE_PREFETCHend to end: enum, constructor, op name, symbol, CPU dispatchas a documented no-op,
ggml_get_n_tasks, RPC op list, backend-ops skip list.build_moe_lookahead()picks layeril+1's experts fromlayer
il's post-attention state (build_lora_mmonffn_gate_inp, thenggml_argsort_top_k) and ends inggml_moe_prefetch(), gated on--moe-lookahead > 0,ctx_type == DEFAULTand decode rows only.src/models/qwen4exp.cppandsrc/models/qwen35moe.cpp.preinstall_legacy_pools()), the-1"snapshot notpublished" contract, and a bounded self-healing retry in the consumer.
GGML_CUDA_MOE_LOOKAHEAD_DEBUG.ffn_up_exps, notffn_gate_up_exps:ffn_gate_up_expsisnull for every layer of this model. The consumer resolves the layer by name, so any
of the layer's cached expert tensors works.
Blocker 1: the producer changes the model output
Logits are not preserved. Same prompt, same flags, only
--moe-lookaheaddiffers:fc1da5b9e41d249e7da51791a934d5618e0a9d10e9095ce7806f645e7edbba0efc1da5b9e41d249e7da51791a934d5618e0a9d10e9095ce7806f645e7edbba0e4ba9f8e7a60ca9830b0dcbed073ffc3e9bbf44eb3b98b5503ed2d1f9b028dda3fc1da5b9e41d249e7da51791a934d561Readings, all deterministic across reruns:
MUL_MATthat readsffn_gate_inpis the trigger. Removing the argsort andthe prefetch op does not remove the divergence.
logits byte-identical, so the scheduler is not merely shape-sensitive; the platform is
shape-neutral for a node that does not touch the router.
capture is not the cause of the numeric change.
use_countsproof that rejectsthe graph for the prefetch node (
graph=unproven(14)) also covers the router tensors,and an extra reader of
ffn_gate_inpchanges those counts. This is unconfirmed, and itis the one open question left.
An earlier handoff recorded these logits as byte-identical. That reading came from a
build in which the producer never ran. It is refuted here.
Blocker 2: every prediction is dropped, so the feature is inert
acquire_legacy_cache()installs a new record only while the target group's authority isGGML_CUDA_MOE_GROUP_AUTHORITY_LEGACYwith admission open. On this rig the groups arenot under legacy authority, so the pool install fails for every target:
The producer runs and the consumer resolves the right layer, but no copy is ever
enqueued. This answers the design's open question Q1 negatively: the cross-layer pool
install is not reachable in this configuration.
Blocker 3: -13% decode throughput
The ids readback synchronizes the stream, which forces
use_cuda_graph = falsefor everygraph containing the op. The precedent is
ggml-cuda.cuggml_cuda_mul_mat_id_needs_syncat the same decision point, and the demand path's own ids readback has the same cost class.
The feature stays default-off.
Correctness work that is independent of the blockers
ggml.c: the per-node src loop must not countGGML_OP_MOE_PREFETCHsources. The opreads no tensor values, and counting them made the MoE grouped-decode certificate reject
the graph:
grouped decode certificate failed: ... graph=unproven(14)followed by a failed graphcompute. With the exclusion, zero certificate failures.
preinstall_legacy_pools()returned 0 when the candidate snapshot was not published,which is indistinguishable from success, so the caller latched "pools ready" forever.
It now returns -1 and the caller only latches on a real result.
Verification performed
test-moe-cacheon the final code: OK, exit 0, includingtest_lookahead_prefetch_eviction_guard OK.ninja -C build llama-server test-moe-cache llama-perplexity, CUDA arch86;120.
Recommended decision
perfect producer has nothing to page into while the target layers are not under legacy
cache authority (blocker 2), and the readback cost is structural (blocker 3).
ffn_gate_inpmust not be able to change the model's output. That is a property of thefork's router identification or of its count-based proofs, not of the prediction math,
and it needs to be fixed where it lives.
Diagnostics kept on the branch (env-gated, inert when unset)
Added after the reopen-transport ruling commit, at the owner's request that the
instruments stay in the tree for troubleshooting rather than be reverted:
9d24345de- phase attribution probe (GGML_CUDA_MOE_PHASE_PROBE=1) and off-leaserecall scoring (
GGML_CUDA_MOE_LOOKAHEAD_DEBUG=1), plus the two cache populations thatwere counted but never printed (
prefetch_dropped,evicted_prefetched_unused).570370183- measured results indocs/moe-lookahead-design.md.Both probes are inert with the env vars unset (one cached
getenvbehind a static bool)and both turn themselves off with a single stderr note if an event or a memcpy cannot be
issued, which is what happens under CUDA graph capture, so they cannot break a capture
run. An attribution needs
GGML_CUDA_DISABLE_GRAPHS=1, because replay steps never visitthe host dispatch loop where the events are recorded. This does not change the three
blockers below: the producer is still not landable.
Measured on the RTX 3060, ctx 81920, cache 42, single 13,946-token prompt, 256 decode
steps at 96.90 ms/step (10.43 t/s). Phase rows sum to 95.7 ms, so the split covers the
step:
MUL_MAT_ID,MOE_PREFETCH,ARGSORT,TOP_KGET_ROWSonper_layer_token_embdDispatch state
mode_legacy=3 mode_direct=253; prefill (28 ubatches, 4555 ms each) is87.9% MoE. Recall scored in the dispatch loop instead of the legacy blk.47 lease: 64.5%
of the used experts predicted, 80.6% of the predictions used; the other two populations
are structural zeros because nothing is offered to the installer (decode phase line
ops=0,legacy cache authority).The bytes/token this implies (GGUF tensor inventory, 10 of 512 experts per layer):
1031 MiB of expert weights, 3.34 GiB of dense/attention/head/shared, 4.35 GiB total, and
the MoE phase streams its 1.01 GiB at 16 GB/s, i.e. 5.4% of the card's bandwidth, while
the dense phase reaches 126 GB/s. The decode step is MoE-kernel-bound, not transport- or
capacity-bound: prefetching ahead layers cannot make the kernels faster, and the expert
H2D (337.6 MiB/token, 3.71 GB/s of the gen4 x4 link's 7.88 GB/s) is already fully hidden
(
calls=ready=12240,ready_min=255).