Replies: 1 comment
|
This repo ignores all GPUs except the first one. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
This follows up on #24528 (leloch's CUDA-side adaptive cache, whose PR #24524 was closed for scope) with a different design point: zero kernel changes via id remapping, plus a prefill fallback rule
RFC: Persistent expert slot pool for MoE CPU offload (
--moe-expert-cache)Summary
Add
--moe-expert-cache N(-mec N): for every MoE expert weight tensor that offloadingplaced in host memory, keep a persistent pool of
Nexpert slots in accelerator memory.Cache hits serve a decode step with zero host-to-device traffic; misses copy the
expert in and evict the least recently used slot. Expert ids are remapped to slot ids
through a per-tensor map table applied with
ggml_get_rows— no new ggml op and nobackend-specific code — so CUDA, Vulkan, Metal and CPU all work unchanged.
Measured on a single RTX 4090 24 GB, Qwen3.8-Flash-Next (qwen4exp, 48 MoE layers, all
experts on CPU, UD-Q3_K_XL, 8 K context, greedy):
On a real code-generation workload served through
llama-server(173-token prompt,512 generated tokens, warm rounds): 12.82 → 14.10 (+10%) at N=32 and → 17.14 (+34%)
at N=64; a long multi-task session (2561 tokens with pool-churning 1 K-token
generations interleaved) held anchor outputs byte-identical across four runs with
no throughput decay. Greedy decoding with the pool active is perplexity-equivalent
to the host-copy path (PPL 3.3096 ± 0.053 vs 3.3262 ± 0.053, deterministic reruns;
difference well inside the error bars).
Prefill is unchanged within noise by construction (below). A backend-level matrix test
passes bit-exact on CUDA (RTX 4090) and Metal (AMD dGPU): 5 quant types × dual pools
sharing one routing × ubatch sizes 1/3 × cold / full-eviction / hit+reload /
graph-rebuild. Server-level greedy A/B initially diverged deterministically from the
host-copy path; root cause was found and fixed (details in Validation — including one
invalid verification round we are disclosing rather than hiding).
Problem
With
--cpu-moe/--n-cpu-moe(or the auto-fitter), decode re-streams every selectedexpert over PCIe on every token: the selective expert copy added in #15346 removed the
unused experts but still copies the same hot experts token after token. Routing is
skewed, so for code-generation-style workloads the same small expert set is re-read
thousands of times. #20757 requests a cache for exactly this.
Why not the other approaches on the table
--prefetch-weights(#21067)overlaps next-layer transfers with compute but cannot know the next layer's routing;
for MoE it was measured moving 2.06× the bytes with +47.8% TTFT. Complementary, not
competing: prefetch targets dense/prefill transfer overlap, this targets decode
residency. The two can share the copy-stream infrastructure.
evict the decode hot set; a frequency-gated admission filter was needed to recover.
This design sidesteps the problem structurally: a ubatch that can touch more
distinct experts than the pool has slots never goes through the pool at all — it
takes the existing selective-copy path. Prefill therefore behaves exactly like
master (verified: pp512/pp2048 unchanged), and prefill never evicts decode state.
this branch is deliberately small in backend impact: zero kernel changes, one
scheduler hook, ~700 lines total of which ~350 are the scheduler core.
Design
ggml_backend_sched_register_expert_pool(sched, w, backend_id, n_slots, &table)allocates
n_slots * expert_size(+ a small NaN-safe tail) on the compute backendand a host-side I32
map_tableshaped[1, n_expert](flagged as a graph input).llm_graph_context::build_lora_mm_id— the single funnel for all MoE expertmatmuls — routes through the pool when one is registered for the tensor and the
ubatch cannot select more distinct experts than slots (
n_expert_used * n_tokens ≤ n_slots). The remap isget_rows(table, cont(ids)): existing ops only.so the remap
GET_ROWSalways anchors its own split. In that split's prologue thescheduler reads the (original) expert ids back to the host, updates LRU state,
issues async H2D copies for misses and rewrites the map table — before the same
split's input copies upload the fresh table. (This ordering is the fix described
below; anchoring the update anywhere later lets the remap consume a stale table.)
time (tensors beyond the budget silently keep the selective-copy path); pools are
disabled under pipeline parallelism; slot-overflow is a hard assert instead of
silent corruption.
Slot sizing:
Nmust cover the decode working set, not just top-k —N = top-kis afull-miss worst case and measurably slower than master (the per-layer id readback
synchronization has nothing to buy). Start at 2–4× top-k and scan; gains are
routing-skew dependent (code workloads benefit most, flat-routing models least).
Validation — including a bug we found, mis-verified once, then actually fixed
The full evidence chain (raw captures, the invalid round, the fix, the bypass control)
is archived in
memoriaru/llama-cpp-expert-pool-stale-table-fix.
tests/test-expert-pool.cppruns the pooledMUL_MAT_IDand the regular host-copy path in the same process — Q2_K/Q3_K/Q4_K/Q6_K/Q8_0 × two pools sharing one routing (fused gate_up + down shape) × ubatch 1/3
× cold/evict/hit/rebuild. 40/40 on CUDA and Metal.
CPU:
-mec 0is run-to-run byte-identical (three runs across days and codeversions);
-mec 32diverged at byte 17 on a coherent near-tie ("wants" vs "needs")and stayed deterministic across environments and code revisions.
showed mec0/mec32 agreeing was produced against the wrong server instance — the
-mec 32launch had failed (No such file or directoryinf32.err) and bothcurls hit the still-running
-mec 0server (visible retroactively in the responsetimings:cache_n = 830on what should have been a cold prompt). Lesson adoptedin the test guide: verify the instance (startup log, prompt cache counters) before
trusting an A/B pair.
GET_ROWSreads the map table through aRESHAPEview;views carry no buffer, so the pooled-split boundary check (which keys on src
buffers) could not see it, and the remap could land in an earlier split than the
pool-update prologue — consuming the previous ubatch's table. Every cache miss
then read whatever expert last occupied the remapped slot (~20–30% wrong weight
reads per step on a 512-expert model with 32-slot pools). Small enough to stay
coherent, deterministic enough to reproduce byte-for-byte.
[1, n_expert]and consumed directly (no view),its buffer is registered with the boundary check so the remap anchors its own
split, and the pool update runs at that remap, before the same split uploads the
fresh table. The bypass control — pools allocated but the graph on the host path —
is byte-identical to
-mec 0; with the pool active the A/B now agrees for 353bytes and then flips once on a near-tie (byte 354, deterministic), consistent with
llama.cpp's known sensitivity of kernel selection to buffer placement (the same
class of near-tie flips observed when changing
-ngl/tensor placement). Theperplexity check quantifies the residual: PPL 3.3096 ± 0.053 with the pool vs
3.3262 ± 0.053 without (8 × 4096-token chunks, deterministic reruns) — the
difference is well inside the error bars, i.e. quality-equivalent.
the LRU state between four replays of the same anchor prompt: anchor outputs
byte-identical (same SHA-1 four times), throughput stable (13.9–15.3 t/s).
Maintenance footprint
~700 lines:
ggml-backend.cpp(+~350: pool struct, register/update, split hooks),llama-graph.cpp(+~20 remap),llama-context.cpp(+~70 registration & budget),argument plumbing,
llama-benchflag, and the self-contained matrix test. No changesto any backend, no new ops, no allocator changes.
Limitations / future work
Validation §5); we propose treating it like the variance already accepted for
-nglchangescomposable later)
Reproduce
AI usage disclosure: this feature was developed with AI assistance (analysis, code and
benchmarks); the author ran, verified and debugged all results on their own hardware
and has reviewed every line — including writing the fix for the bug the validation
uncovered.
All reactions