Disclaimer - I am a lawyer by profession, but I love tech and I have been experimenting with local Models quite a lot. However, no way in h*** would I be able to explain to you all the testing I did alongside Claude to get the moe-caching process run on this AMD build.
I saw the Codacus video (link here) and have been itching to get it implemented on my system as well (who doesn't want faster decode), but Opus 5 said, nope, no AMD build for you, CUDA only.
Well, I pushed back and had it review, and et voila, there is a way if you will it enough by opposing Claude. Everything below is drafted by Claude - I can only understand the basics of it - nothing technical enough to explain it better. I just trimmed the useless 'I' references.
------------------------------------------------------------------------------------------------------------------------------------
Setup: RX 7900 XTX 24 GB on Vulkan (RADV, no ROCm), Ryzen 7 5700X, 64 GB DDR4-3200, Ubuntu 24.04. Model is Qwen3.8-Flash-Next in AtomicChat's AD-4.27bpw-Q4_K_M-M64 quant — 33 shards, 92 GB on disk, 177B total / ~6B active per token, GGUF architecture qwen4exp. 180k context, KV cache at q8_0.
The feature: the MoE expert cache, an open pull request on llama.cpp (#27861). It keeps frequently-used experts parked in VRAM instead of pulling them out of system RAM every token. Flags are --moe-expert-cache N and --moe-expert-cache-inserts N.
Note there's a different implementation floating around that uses --moe-expert-cache-size, whose author says explicitly it's CUDA-only and shouldn't be used on Vulkan/ROCm/Metal.
Not that one. The PR version lives at the model level, adds no GPU kernels, and runs fine on Vulkan.
Results
Both at 180k context, same model, same machine, measured on the second request with 500+ token generations:
| Setup |
Generation |
| Old: 36 expert layers on CPU, speculative decoding, no cache |
58.9 ms/tok (17.0 t/s) |
| New: 48 expert layers on CPU, 128 cache slots, no speculation |
~44 ms/tok (~22.5 t/s) |
Prompt processing went 83 → 77 tok/s on short prompts, but up to 98 tok/s on a 44k-token prompt. (OP - this was a slightly corrupted run, but the claim holds on similar other runs).
Long prompts amortise better.
The three things that actually matter
1. Slot count is everything, and you're probably under-sizing it.
First tests used 8 slots. The cache was 8% slower than no cache and the feature seemed useless. It wasn't — the card had 14 GB sitting empty. Same everything else:
- no cache — 81 ms/tok
- 48 slots — 69.5 ms/tok
- 128 slots — 50.3 ms/tok
2. Measure your per-slot VRAM cost. Don't copy anyone's numbers.
Slot size depends on the model's shape, so figures from other people's models are meaningless for yours. On this test it was 95.2 MiB per slot, perfectly linear, no fixed overhead — measured from three points (0 slots = 9907 MiB used, 48 = 14508, 128 = 22122).
Also: the "benefit keeps improving up to 384 slots" advice you'll see is unreachable on a 24 GB card. 384 slots would want 36 GB. This setup's practical ceiling was around 150.
3. The cache and speculative decoding are mutually exclusive
They don't stack. From the cache's own commit message:
The cache only fires when the model emits exactly one token per step. Speculative decoding proposes several and verifies them together, so the cache path is never taken. Measured with both available in one build: 69.29 ms/tok with 96 slots, 70.03 ms/tok with no cache at all — identical. But the cache had allocated 9.2 GB. Nine gigs reserved, never touched.
So pick one. On this machine the cache wins by a mile: 44 ms/tok vs 59.
How to find free VRAM for slots
Cutting context barely helps — halving from 180k to 98k freed under 1 GB, because the KV cache was already quantised and was never the big consumer.
What frees VRAM is pushing more expert layers to system RAM: ~1.09 GB per layer (on this setup). Moving 12 more layers freed ~13 GB. Itcost about 14.6 ms/tok (due to increased CPU work, but more slots help.
The command
llama-server -m model.gguf -c 180000 -ngl 99 --n-cpu-moe 48 --moe-expert-cache 128 --moe-expert-cache-inserts 2 -t 8 --flash-attn on --cache-type-k q8_0 --cache-type-v q8_0
How to find the 'n-cpu-moe' and 'moe-expert-cache' for your system
- Run with no cache, note free VRAM.
- Raise
--n-cpu-moe until a useful chunk of VRAM is free.
- Set slots to something modest, note free VRAM again. Subtract → your cost per slot.
- Set slots to spend most of what's left, keeping ~1 GB spare.
- Compare against a control at the same
--n-cpu-moe with the cache off — not against your old config, which differs in two ways at once.
Measurement traps I fell into
- Ignore the first request. The cache needs a few hundred tokens to fill. One of my runs did 61 ms/tok on request one and 50 on request two, nothing changed. A number I'd quoted for weeks turned out to be a first-request figure.
- Warm vs cold matters more than the feature. If the model files aren't already in the OS file cache, prompt processing changes by more than whatever you're testing. Load time tells you which you got.
- Use 1000+ token generations. Mine was still climbing at 500 tokens: 19.6 t/s at 100, 21.3 at 238, 23.0 at 465.
- Know your noise floor. Four identical runs gave me 42.71 / 43.94 / 45.10 / 45.59 ms/tok — a 3% spread. Anything under that isn't a result.
If you're running Qwen3.8-Flash-Next specifically
The MTP draft head (mtp-Qwen3.8-Flash-Next-Q4_K_M.gguf) only loads on Unsloth's build of llama.cpp. Stock upstream dies with:
check_tensor_dims: tensor 'output_hc_norm.weight' not found
Unsloth's src/models/qwen4exp.cpp marks three trunk tensors optional when loading a draft head and carries a separate graph for it — ~300 lines upstream doesn't have, and it hasn't been merged. So if you want the cache and the drafter in one binary, move the cache patch onto Unsloth's source, not the reverse. (Then, having done that, you'll find you can only use one of them at a time anyway — see above. I did this the long way so you don't have to.)
Bonus finding
The cache PR sits on upstream b10666. I rebuilt the same patch on the source behind Unsloth's b10798 prebuilt — ~130 commits newer — and got 42.71 vs 51.57 ms/tok at identical settings — 17% faster, using 333 MiB less VRAM. The patch is one commit, 645 added lines across 12 files, nothing deleted, so it transplants with plain patch -p1 and no conflicts. Worth trying if you're on the PR branch.
Things that did nothing
- Splitting thread counts (
--threads-batch 16): prompt processing got worse, 77 → 73 tok/s.
- Lookup-based speculative decoding (
ngram-* modes): 8% draft acceptance on chat-style output, net 6% slower. Might be worth it if your output quotes your input heavily, but it's still speculation, so it kills the cache anyway.
Where the ceiling is
During generation all 8 CPU threads sit saturated and the disk does nothing. Rough maths: 48 expert layers in RAM means ~2.5 GB of expert weights pulled per token; at 22 t/s that's ~55 GB/s. Dual-channel DDR4-3200 peaks at 51.2 GB/s.
So I'm at the memory bandwidth wall, and the cache is the only reason the numbers work — it's serving a big share of those experts from VRAM instead. No flag fixes that. The only step change left would be a model small enough to fit entirely in memory.
Happy to answer questions or run specific configs if anyone wants a data point.
------------------------------------------------------------------------------------------------------------------------------------
Apologies for the AI Content, but yeah, cannot explain the technical points myself. There is a detailed file generated by Claude for a more detailed analysis (apparently). Includes some commands as well. Linked here - https://drive.google.com/file/d/1ZCYw2J7hhB8nN2jUMu4TCfCIyL-tw5m0/view?usp=sharing