There's a result that circulates in local-LLM threads and sounds like nonsense: someone moves more of their model off the GPU and onto the CPU, and it gets nearly five times faster. Not five percent. Five times. Every instinct says that's backwards — CPU RAM is slower than VRAM, so shifting work to it should hurt.
The measurement is real. I traced it to a merged llama.cpp pull request with the full llama-bench output attached. But the explanation people give for it is usually wrong, and if you act on the folklore version you'll make your setup slower rather than faster.
What the flag actually does
--n-cpu-moe N does one specific, narrow thing. For layers 0 through N-1, it overrides the buffer type for tensors matching this pattern:
\.ffn_(up|down|gate|gate_up)_(ch|)exps
Those are the routed expert weights — the big stacks of per-expert feed-forward matrices that make an MoE model large. Nothing else moves. Attention weights stay where -ngl put them. So does the KV cache, the router that picks which experts to use (ffn_gate_inp), any shared experts (_shexp), the layer norms and the embeddings.
You can see the shape of it in our model catalogue: on Qwen3-30B-A3B, the routed experts account for roughly 95% of the parameters, while only about 3.3B are active for any given token. Nearly all the memory, almost none of the per-token reading.
--cpu-moe vs --n-cpu-moe vs -ot
All three flags do the same kind of thing: they tell llama.cpp to keep certain weight tensors in system RAM. They differ only in how much you control. --cpu-moe moves every MoE expert weight. --n-cpu-moe N moves them for the first N layers only. -ot lets you write the pattern yourself.
The three MoE offload flags in llama.cpp (help text read 4 Oct 2026)
--cpu-moe (short: -cmoe)
Official help text
keep all Mixture of Experts (MoE) weights in the CPU
What moves to RAM
Every layer's routed experts
Environment variable
LLAMA_ARG_CPU_MOE
--n-cpu-moe N (short: -ncmoe)
Official help text
keep the Mixture of Experts (MoE) weights of the first N layers in the CPU
What moves to RAM
Layers 0 to N-1 only
Environment variable
LLAMA_ARG_N_CPU_MOE
-ot / --override-tensor
Official help text
override tensor buffer type
What moves to RAM
Any tensor whose name matches your regex
Environment variable
LLAMA_ARG_OVERRIDE_TENSOR
Official help text
What moves to RAM
Environment variable
--cpu-moe (short: -cmoe)
keep all Mixture of Experts (MoE) weights in the CPU
Every layer's routed experts
LLAMA_ARG_CPU_MOE
--n-cpu-moe N (short: -ncmoe)
keep the Mixture of Experts (MoE) weights of the first N layers in the CPU
Layers 0 to N-1 only
LLAMA_ARG_N_CPU_MOE
-ot / --override-tensor
override tensor buffer type
Any tensor whose name matches your regex
LLAMA_ARG_OVERRIDE_TENSOR
Under the hood, the first two are shortcuts for the third. --cpu-moe adds one override with the pattern shown above. --n-cpu-moe 3 adds three overrides, one per layer, of the form blk\.0\.ffn_(up|down|gate|gate_up)_(ch|)exps, then blk\.1… and blk\.2…. So these two commands do the same thing:
Which one should you use? Use --n-cpu-moe almost always. It has one knob, and that knob maps straight onto the V-shaped curve below. Use --cpu-moe when the model is far too big for your card and you want the simplest possible start. That is the same as setting --n-cpu-moe to the layer count. Reach for -ot only when you need a pattern the shortcuts can't express, like pinning specific layers to a second GPU. The text after the = must be a buffer type your build knows. If it isn't, llama.cpp prints the list of available buffer types and stops.
If you searched for the phrase "number of layers for which to force MoE weights onto CPU", that is this same N. It counts layers from the start of the model, not experts. N = 0 moves nothing. N equal to the model's layer count is the same as --cpu-moe.
LM Studio added an all-or-nothing version in 0.3.23 (12 August 2025) and replaced it with a per-layer slider for n_cpu_moe in 0.4.0 (28 January 2026). Its release notes describe an advanced model load setting that places all MoE expert weights onto the CPU or the GPU, with GPU as the default. That switch is the --cpu-moe equivalent.
Normally, more offload is slower
This is the part the folklore skips. When the model already fits on your GPU, increasing --n-cpu-moe degrades throughput steadily and predictably. A llama.cpp contributor published a clean sweep of it while working on CUDA graph support for this path:
Token generation as offload increases (tg128) — model fits comfortably
GLM-4.5-Air 106B IQ4_XS
ncmoe 8
60.84 t/s
ncmoe 16
48.25 t/s
ncmoe 32
32.84 t/s
ncmoe 64
25.08 t/s
gpt-oss 120B MXFP4
ncmoe 8
95.96 t/s
ncmoe 16
70.42 t/s
ncmoe 32
44.80 t/s
ncmoe 64
40.87 t/s
ncmoe 8
ncmoe 16
ncmoe 32
ncmoe 64
GLM-4.5-Air 106B IQ4_XS
60.84 t/s
48.25 t/s
32.84 t/s
25.08 t/s
gpt-oss 120B MXFP4
95.96 t/s
70.42 t/s
44.80 t/s
40.87 t/s
Monotonic decline, both models. Every layer you push to the CPU is a layer whose expert weights now have to come across PCIe from system RAM instead of out of VRAM. That's the behaviour physics predicts, and it's what you should expect by default.
If more offload is making you faster, something is already wrong. The speedup is a symptom, not a feature.
So where does the 5× come from?
From a different situation entirely — one where the baseline was broken. The measurement comes from the pull request that added `--n-cpu-moe` to llama-bench, where contributor jacekpoplawski ran a 13.70 GiB model on a 12 GB card:
Qwen3-30B-A3B-Instruct-2507 Q3_K_M (13.70 GiB) on a 12 GB RTX 5070, Windows
-ngl 99 alone
Generation (tg128)
11.09 t/s
Prefill (pp512)
378.46 t/s
-ngl 99 --n-cpu-moe 12
Generation (tg128)
54.04 t/s
Prefill (pp512)
1,253.62 t/s
Change
Generation (tg128)
4.87×
Prefill (pp512)
3.31×
Generation (tg128)
Prefill (pp512)
-ngl 99 alone
11.09 t/s
378.46 t/s
-ngl 99 --n-cpu-moe 12
54.04 t/s
1,253.62 t/s
Change
4.87×
3.31×
Look at the model size against the card. 13.70 GiB does not fit in 12 GB. That's the entire story.
On Windows, NVIDIA's driver has a feature called System Memory Fallback. When a CUDA allocation won't fit in VRAM, rather than failing it silently backs the allocation with host RAM and pages it over PCIe on demand. Your model "loads". It runs. It runs terribly, because the driver is now shuttling weights across the bus with no idea which ones matter, thrashing on whatever it guesses wrong about.
The 11 t/s baseline isn't GPU inference. It's a driver frantically paging. When you then set --n-cpu-moe 12, you're not adding a penalty — you're taking control of a decision the driver was already making badly. You tell it exactly which tensors live in host memory, everything else fits properly in VRAM, and the thrashing stops. Hence 54 t/s.
The actual shape: a V, not a slope
Put both findings together and the real curve has three regions.
**Too little offload** — the model overcommits VRAM. On Windows you get driver paging and catastrophic throughput. This is the left arm of the V.
**The knee** — the smallest N where everything genuinely fits in VRAM. This is the fastest configuration, and it's what you're looking for.
**Too much offload** — every extra layer sends more expert weight across PCIe for no benefit. Slow, steady decline. The right arm.
Which reframes the tuning problem completely. You're not looking for "how much can I offload" — you're looking for the least you can get away with. Start at zero, increase until it fits without overcommitting, stop. Every step past that costs you.
Three reasons this may not apply to you
I'd rather tell you when advice is irrelevant than let you apply it blindly, so:
You're probably on a build that fixes this automatically. llama.cpp's --fit is on by default in current source. It auto-sizes offload to what will actually fit. That largely prevents anyone from landing in the overcommit regime in the first place. This advice increasingly matters for older builds, manually forced configurations, and non-CUDA backends. The next section shows exactly how --fit and --n-cpu-moe interact.
On Linux you'd probably have seen an error instead. Stock CUDA on Linux doesn't do the silent host-memory fallback unless you opt in with GGML_CUDA_ENABLE_UNIFIED_MEMORY=1. Without it, an overcommitted model usually just fails to allocate and tells you so. Which is, frankly, the better behaviour — a loud failure beats a quiet 5× slowdown.
Your system RAM speed matters more than the flag does. Once expert weights live in host memory, their read speed is your memory bandwidth. Dual-channel DDR5 is in a different league from quad-channel or server memory, and the same --n-cpu-moe value will behave very differently across those machines. Any specific number someone quotes you is a number from their motherboard, not yours.
How to pick N on a current llama.cpp build
On a current build, try no MoE flag at all first. The default --fit on already does a version of --n-cpu-moe for you. If you then add --n-cpu-moe yourself, --fit stops placing weights and your N is final. So a manual value only helps when it beats the automatic one.
Here is what the fit code does, read in common/fit.cpp on 4 October 2026. It only adjusts settings you did not set yourself. It aims to leave a safety margin of 1,024 MiB free on each GPU (change it with --fit-target). It may shrink the context first, but never below 4,096 tokens (change it with --fit-ctx). For a MoE model it then fills the GPU with the small, hot tensors first, with all expert weights in RAM. After that it moves expert weights back onto the GPU while they still fit. That is the "knee" of the V, found for you.
The catch is in the same file. If you set -ngl, --n-cpu-moe, --cpu-moe or -ot yourself, the fit step logs "failed to fit params to free device memory" and leaves weight placement to you. It can still have trimmed the context before that point. So you have two sane workflows:
**Let --fit do it.** Run with no offload flags, read the log, and check speed with llama-bench. This is the right default for most people.
**Tune by hand.** Start at `--n-cpu-moe 0` and raise it one step at a time until the model loads with headroom, then stop. llama-bench accepts `--n-cpu-moe` too, so you can sweep several values in one run (PR #15952).
**Never** keep raising N once it fits. That is the slow right arm of the V.
A dense model has no expert tensors, so --n-cpu-moe does nothing for it. Since 27 August 2026 (PR #26622) llama.cpp has a sibling for that case: --n-cpu-ffn N (short -ncffn). It keeps the dense feed-forward weights of the first N layers in RAM. The gpt-oss-20b on an 8GB or 12GB card walkthrough shows the MoE version on a real model.
The smallest one where the model fits in VRAM without overcommitting. Start at 0 and increase until it loads cleanly — then stop, because every further step costs throughput. There is no universally good value; it depends on your model, quantisation, context length and card.
What is the difference between --cpu-moe and --n-cpu-moe?
--cpu-moe (short -cmoe) keeps every layer's MoE expert weights in system RAM. --n-cpu-moe N (short -ncmoe) does the same for the first N layers only. Setting N to the model's layer count gives the same result as --cpu-moe. Both are shortcuts for an -ot (--override-tensor) rule.
What does ncmoe mean in llama.cpp?
-ncmoe is the short form of --n-cpu-moe, the number of layers whose MoE expert weights stay in CPU memory. Its environment variable is LLAMA_ARG_N_CPU_MOE. llama-bench accepts the same flag, with a comma-separated list, so you can compare several values in one run.
Does --n-cpu-moe help dense (non-MoE) models?
No. It targets routed expert tensors specifically, and a dense model doesn't have any. For dense models use -ngl, or the newer --n-cpu-ffn N (short -ncffn, added 27 August 2026), which keeps the dense feed-forward weights of the first N layers in RAM.
Do I still need --n-cpu-moe if --fit is on?
Usually not. --fit is on by default in current llama.cpp and already keeps expert weights in RAM only where they don't fit. If you set --n-cpu-moe yourself, --fit stops placing weights and your value wins, so only set it when it measures faster.
Why did my throughput get worse after following this advice?
Almost certainly because your model already fitted. In that case you've moved expert weights from VRAM to system RAM for no reason, and you're now reading them over PCIe. Reduce the value, or remove the flag entirely and let a recent llama.cpp size it for you.
Does the KV cache move to the CPU too?
No. The flag only overrides tensors matching the routed-expert pattern. The KV cache stays wherever it was — which matters at long context, because a large KV cache can push you back into overcommit even when the weights fit fine.
Is this the same as CPU offload in vLLM or ktransformers?
The idea is related — keep the hot path on the GPU, push bulky rarely-read weights elsewhere — but the implementations and the flags differ, and numbers are not comparable across them. Measure on your own stack rather than porting a value someone quoted for a different runtime.
If you want to know whether your model fits before you start guessing at flags, our cost-to-run pages compute the weight and KV-cache requirement per quantisation from each model's own configuration, so you can see which term is pushing you over the edge — and it's more often the KV cache at long context than people expect.