RFC+PoC: MoE offload to disk with on-demand paging #23324
Replies: 5 comments 10 replies
|
super cool. how do you decide which experts to evict? |
|
I think on Linux |
|
Independent Windows/CUDA implementation + measurements (incl. negative results) I independently built disk-backed expert streaming on Windows/CUDA before finding Code + full report: Hardware: i7-8700K (PCIe 3.0), RTX 4080 16GB, 32GB DDR4-2666, NVMe
Findings possibly relevant here:
The wall on this hardware is bytes/token: 4.13 GB/token pure streaming, 3.82 Happy to run Windows/CUDA tests for this RFC if useful. |
Benchmark data + calibrated pinning (complementary to this PoC)We've been working on the same problem from the CPU side and have some measurements and a technique that might be useful here. Your PoC is ahead of where we are — you have a working llama.cpp fork with Metal, LRU in shared memory, and What we measured (M1 MacBook Air, 16GB, CPU BLAS)We wrote a C benchmark that runs MoE forward passes with Apple Accelerate (sgemv) and a dedicated I/O thread doing async Results at controlled hit rates (f16 experts, Qwen3-30B-A3B):
Per-layer compute: 4.7 ms. SSD: 3.0+ GB/s sequential. Pipelining hides up to 4 misses/layer, so you're compute-bound at 50% cache hit. With Q4_K_M experts (~2.2 MB each), I/O per expert drops to ~0.27 ms vs 4.7 ms compute/layer. At that size, pipelining hides all misses — the cache becomes a bonus rather than a requirement. (Projected from measured SSD throughput, not directly benchmarked at Q4.) Where this is useful for your PoCThese numbers are CPU-only, so they won't match your Metal throughput. But the hit-rate sweep gives a clean way to tune Text-calibrated pinning (the part we think is most useful)This is the technique we'd most like to contribute. Your PoC uses LRU for cache management, which starts cold — every expert is a miss on the first token. We tested different pinning strategies:
Random profiles are useless — you have to calibrate from actual text. A short pass over ~10 real tokens, tracking which experts fire, then pinning the top-N per layer gives 42% hit rate. That matches oracle top-8 (45%) and stacks with LRU for 56% combined. For your PoC, this would mean pre-warming the Metal shared memory slots before inference starts, so the first few tokens don't pay full I/O cost. On 16GB hardware where A note on our contiguous re-layoutWe also built a tool that dequantizes GGUF expert tensors and repacks them so each expert sits contiguously on disk. This was important for our path (mmap access to dequantized caches), but your CodeEverything is at github.com/jerryjokesalot/tinygiant:
We also posted a broader writeup in Discussion #27149. If the calibrated pinning is useful, happy to help wire it into your branch. |
|
Please make it work for all the model files, don't make another model file format. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Running Qwen3-30B-A3B-Q6_K on M3 Pro 36GB, the expert weights alone wire ~27 GiB of memory. Wired memory on Apple Silicon can't be compressed or swapped, which leaves little headroom for other workloads.
This PoC implements on-demand expert loading: instead of keeping all 128 experts resident, we allocate a compact pool of N slots in Metal shared memory and page missing experts from the GGUF file via
pread. This is exact inference without fallback to zero/random weights.With lazy pool allocation this could enable running models larger than physical RAM.UPD: managed to run Qwen3-30B-A3B-Q6_K on M1 Pro 16GB with 13 tok/s, so the concept works already.
How it works
Each MoE layer gets a compact pool tensor of N slots. A small Metal kernel (
kernel_moe_interceptor) copies the selected expert IDs to shared CPU/GPU memory and publishes a request sequence number. A CPU sidecar thread sees the request, resolves expert-to-slot mappings via LRU, loads missing experts from disk withpread, writes remapped slot IDs back to shared memory, and signals anMTLSharedEvent. The GPU encoder waits on this event, thenMUL_MAT_IDruns unchanged against the compact pool.Measurements (Qwen3-30B-A3B-Q6_K, M3 Pro 36GB)
How to run
./build/bin/llama-cli \ --moe-n-slots 80 \ --moe-n-layers 48 \ --no-mmap \ --no-warmup \ -m /path/to/model.gguf \ -ub 10 \ --temp 0 -p "Explain mixture of experts"Key constraints:
-ubmust satisfyub * n_expert_used <= n_slots, otherwise we risk requesting more expert slots than available.--no-mmapto be able to actually skip allocating tensors for expert weights.--no-warmup, because warmup allocates a compute buffer for 2048 tokensand we get OOM.
Interested in feedback on whether this direction makes sense for llama.cpp, and whether there are cleaner integration points.
All reactions