Skip to content

ggml-cpu: improve --n-cpu-moe TG performance - #20596

Open
am17an wants to merge 4 commits into
ggml-org:masterfrom
am17an:ncmoe-cpu
Open

ggml-cpu: improve --n-cpu-moe TG performance#20596
am17an wants to merge 4 commits into
ggml-org:masterfrom
am17an:ncmoe-cpu

Conversation

@am17an

@am17an am17an commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

Add a special path for gated activation for n_tokens=1, useful when doing -n-cpu-moe

Using a 5090 with -ncmoe 999

Model Threads Test t/s 5a32a9b t/s ncmoe-cpu Speedup
qwen35moe 35B.A3B Q4_K_S 2 tg32 26.20 28.94 1.10
qwen35moe 35B.A3B Q4_K_S 4 tg32 38.64 40.64 1.05
qwen35moe 35B.A3B Q4_K_S 8 tg32 40.33 43.89 1.09
qwen35moe 35B.A3B Q4_K_S 16 tg32 40.32 43.25 1.07

@am17an
am17an requested a review from ggerganov as a code owner March 15, 2026 15:51
if (ggml_can_fuse_subgraph(cgraph, node_n, 3, fuse_ops, outputs, 1)) {
struct ggml_tensor * node1 = cgraph->nodes[node_n + 1];
struct ggml_tensor * glu = cgraph->nodes[node_n + 2];
// Fused path for `--n-cpu-moe` when n_tokens = 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we do it only for n_tokens == 1?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single token is a special case because you don't need to create the row mapping. Also in this I let each thread quantize the activation to remove a barrier. The barrier cost was around 30% of the total run time in the cpu part, with this change it's about 7%

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that's unexpected for the barrier to be so expensive. I'll need to double-check - do you have a patch that I can apply to test the barrier path?

Btw, you should space the wdata with CACHE_LINE_SIZE_F32 to avoid false sharing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have test-barrier that measures the overhead with tiny graphs.
@am17an can you share what numbers you get from that.
Ideally with and without OMP (GGML_OPENMP=OFF)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ggerganov on master if you do

CUDA_VISIBLE_DEVICES=4 perf record ./build/bin/llama-bench -m /opt/models/Qwen3.5-35B-A3B-Q4_K_S.gguf -fa 1 -n 32 -r 20 -p 0 -ncmoe 99 -t 8

Image

@max-krasnyansky

with OpenMP (the default build)

graph-compute with
n_threads: 4
  n_nodes: 2000
 n_rounds: 100
graph-compute took 516226 usec
5162.26 usec per-iter
2581.13 nsec per-node
graph-compute with
n_threads: 4
  n_nodes: 4
 n_rounds: 10000
graph-compute with
gf0 n_nodes: 4
gf1 n_nodes: 8
  n_threads: 4
   n_rounds: 1000

With OpenMP=OFF

graph-compute with
 n_threads: 4
   n_nodes: 2000
  n_rounds: 100
graph-compute took 479589 usec
 4795.89 usec per-iter
 2397.95 nsec per-node
graph-compute with
 n_threads: 4
   n_nodes: 4
  n_rounds: 10000
graph-compute with
 gf0 n_nodes: 4
 gf1 n_nodes: 8
   n_threads: 4
    n_rounds: 1000

@am17an am17an Mar 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also since this barrier cost is non-trivial, it has a meaningful effect for this case, when there are too many threads contending. Maybe we should limit the n_tasks based on some heuristic. Can someone else also confirm these results (I'm not sure if NUMA is playing a role here). For reference all results on a AMD EPYC 7742 64-Core Processor

CUDA_VISIBLE_DEVICES=4 ./build/bin/llama-bench -m /opt/models/qwen3_next_80b_a3b_instruct-iq4_xs.gguf -fa 1 -n 32 -r 10 -p 0  -ncmoe 999 -t 8,16,32,48,64
ggml_cuda_init: found 1 CUDA devices (Total VRAM: 32111 MiB):
  Device 0: NVIDIA GeForce RTX 5090, compute capability 12.0, VMM: yes, VRAM: 32111 MiB (31604 MiB free)
| model                          |       size |     params | backend    | ngl | threads | fa |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | --: | ------: | -: | --------------: | -------------------: |
| qwen3next 80B.A3B IQ4_XS - 4.25 bpw |  39.67 GiB |    79.67 B | CUDA       |  99 |       8 |  1 |            tg32 |         30.85 ± 0.48 |
| qwen3next 80B.A3B IQ4_XS - 4.25 bpw |  39.67 GiB |    79.67 B | CUDA       |  99 |      16 |  1 |            tg32 |         29.81 ± 0.45 |
| qwen3next 80B.A3B IQ4_XS - 4.25 bpw |  39.67 GiB |    79.67 B | CUDA       |  99 |      32 |  1 |            tg32 |         28.76 ± 0.58 |
| qwen3next 80B.A3B IQ4_XS - 4.25 bpw |  39.67 GiB |    79.67 B | CUDA       |  99 |      48 |  1 |            tg32 |         27.48 ± 0.30 |
| qwen3next 80B.A3B IQ4_XS - 4.25 bpw |  39.67 GiB |    79.67 B | CUDA       |  99 |      64 |  1 |            tg32 |         26.19 ± 0.35 |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look later today. Btw, I was rather thinking to compare the fused version:

  • no barrier + all threads reading src1 (current PR)
  • barrier + 1 thread reading src1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the patch here 0c0cf6f

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is what I'm getting with OpenMP=OFF

AMD EPYC 7543
  5177.86 usec per-iter
  2588.93 nsec per-node

AMD Ryzen 395+
   1559.2 usec per-iter
   779.601 nsec per-node

Mac M4 Pro
  1338.91 usec per-iter
  669.456 nsec per-node 
 
Galaxy S24+
  1873.33 usec per-iter
  936.669 nsec per-node 

Your numbers are a bit high but inline with the AMD EPYC.

With OpenMP=OFF

 4795.89 usec per-iter
 2397.95 nsec per-node

Also from your profile results it looks like it's not really the barrier itself that is expensive.
Most of the cycles are spent in the barrier_wait.
That just means that some of the cores are completing their chunks faster than the others cores.
Maybe our chunking is suboptimal in this case?
We recently updated MUL_MAT and FA to improve dynamic chunking.
Perhaps, it's a similar issue here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@max-krasnyansky that profile is from the master branch. MUL MAT ID also has the same chunked approach if I'm not wrong

Comment thread ggml/src/ggml-cpu/ggml-cpu.c Outdated
@github-actions github-actions Bot added the ggml changes relating to the ggml tensor library for machine learning label Mar 15, 2026
@ggerganov

Copy link
Copy Markdown
Member

I don't think I am seeing any benefit on M2 Ultra:

./scripts/compare-commits.sh 983df142a99b764f5cbe9acb6b9ec862f861d353 pr/20596 llama-bench -m ~/models/qwen3.5-35B-a3b-q4_0-pure.gguf -m ~/models/qwen3-30b-a3b/ggml-model-q8_0.gguf -fa 1 -p 0 -n 32 -r 5 -mmp 1 -t 1,2,4,8,16
Model Threads Test t/s 983df14 t/s pr/20596 Speedup
qwen35moe 35B.A3B Q4_0 1 tg32 23.93 21.28 0.89
qwen35moe 35B.A3B Q4_0 2 tg32 29.75 25.91 0.87
qwen35moe 35B.A3B Q4_0 4 tg32 32.10 31.04 0.97
qwen35moe 35B.A3B Q4_0 8 tg32 27.78 27.99 1.01
qwen35moe 35B.A3B Q4_0 16 tg32 27.97 24.96 0.89
qwen3moe 30B.A3B Q8_0 1 tg32 13.35 13.78 1.03
qwen3moe 30B.A3B Q8_0 2 tg32 20.83 15.39 0.74
qwen3moe 30B.A3B Q8_0 4 tg32 27.93 27.96 1.00
qwen3moe 30B.A3B Q8_0 8 tg32 27.96 28.18 1.01
qwen3moe 30B.A3B Q8_0 16 tg32 22.38 22.08 0.99

0c0cf6f does not make much difference compared to the PR branch.

This is without OpenMP.

@am17an

am17an commented Mar 18, 2026

Copy link
Copy Markdown
Contributor Author

The results seem to have a lot of variability. Especially at t=2 threads. In any case it should not have a slowdown, because it's doing less flops, barriers and memory accesses than baseline.

@am17an

am17an commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

I repeated this machine on another recent AMD machine (AMD EPYC 9B14 96-Core Processor), using

GGML_CUDA=ON ./scripts/compare-commits.sh master ncmoe-cpu llama-bench -hf unsloth/Qwen3.5-35B-A3B-GGUF -n 32 -p 0 -r 10 -t 2,4,8,16,32 -fa 1 -ncmoe 999

Model Threads Test t/s master t/s ncmoe-cpu Speedup
qwen35moe 35B.A3B Q4_K_M 2 tg32 29.85 32.32 1.08
qwen35moe 35B.A3B Q4_K_M 4 tg32 49.73 51.60 1.04
qwen35moe 35B.A3B Q4_K_M 8 tg32 71.19 73.94 1.04
qwen35moe 35B.A3B Q4_K_M 16 tg32 93.33 97.26 1.04
qwen35moe 35B.A3B Q4_K_M 32 tg32 103.58 108.68 1.05

@am17an

am17an commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

@ggerganov in your command don't you have to specify -ncmoe 99?

@ggerganov

Copy link
Copy Markdown
Member

Hm, I must have copied the wrong command - without -ncmoe 99 the speeds are much faster.

In any case, here is another run with -ncmoe:

./scripts/compare-commits.sh 983df142a99b764f5cbe9acb6b9ec862f861d353 pr/20596 llama-bench -m ~/models/qwen3.5-35B-a3b-q4_0-pure.gguf -m ~/models/qwen3-30b-a3b/ggml-model-q8_0.gguf -fa 1 -p 0 -n 32 -r 10 -mmp 1 -t 1,2,4,8,16 -ncmoe 99
Model Threads Test t/s 983df14 t/s pr/20596 Speedup
qwen35moe 35B.A3B Q4_0 1 tg32 26.32 23.04 0.88
qwen35moe 35B.A3B Q4_0 2 tg32 30.36 28.09 0.93
qwen35moe 35B.A3B Q4_0 4 tg32 31.85 31.51 0.99
qwen35moe 35B.A3B Q4_0 8 tg32 35.75 36.09 1.01
qwen35moe 35B.A3B Q4_0 16 tg32 35.37 35.88 1.01
qwen3moe 30B.A3B Q8_0 1 tg32 16.38 15.18 0.93
qwen3moe 30B.A3B Q8_0 2 tg32 23.92 17.92 0.75
qwen3moe 30B.A3B Q8_0 4 tg32 32.43 25.73 0.79
qwen3moe 30B.A3B Q8_0 8 tg32 28.84 27.58 0.96
qwen3moe 30B.A3B Q8_0 16 tg32 31.47 31.56 1.00

Here is your command on the M2 Studio, DGX Spark and a 16-core AMD Ryzen 9:

  • M2 Studio
./scripts/compare-commits.sh master pr/20596 llama-bench -hf unsloth/Qwen3.5-35B-A3B-GGUF -n 32 -p 0  -r 10 -t 2,4,8,16 -fa 1 -ncmoe 999
Model Threads Test t/s master t/s pr/20596 Speedup
qwen35moe 35B.A3B Q4_K_M 2 tg32 27.05 26.41 0.98
qwen35moe 35B.A3B Q4_K_M 4 tg32 29.81 29.60 0.99
qwen35moe 35B.A3B Q4_K_M 8 tg32 34.21 34.07 1.00
qwen35moe 35B.A3B Q4_K_M 16 tg32 33.94 34.05 1.00
  • DGX Spark
GGML_CUDA=ON ./scripts/compare-commits.sh master pr/20596 llama-bench -hf unsloth/Qwen3.5-35B-A3B-GGUF -n 32 -p 0  -r 10 -t 1,2,4,8 -fa 1 -ncmoe 999
Model Threads Test t/s master t/s pr/20596 Speedup
qwen35moe 35B.A3B Q4_K_M 1 tg32 17.93 17.07 0.95
qwen35moe 35B.A3B Q4_K_M 2 tg32 13.98 12.22 0.87
qwen35moe 35B.A3B Q4_K_M 4 tg32 13.39 13.35 1.00
qwen35moe 35B.A3B Q4_K_M 8 tg32 15.94 15.76 0.99
  • AMD Ryzen 9 + RTX 5090
CUDA_VISIBLE_DEVICES=0 GGML_CUDA=ON ./scripts/compare-commits.sh master pr/20596 llama-bench -hf unsloth/Qwen3.5-35B-A3B-GGUF -n 32 -p 0  -r 10 -t 2,4,8,16,32 -fa 1 -ncmoe 999
Model Threads Test t/s master t/s pr/20596 Speedup
qwen35moe 35B.A3B Q4_K_M 2 tg32 41.53 41.58 1.00
qwen35moe 35B.A3B Q4_K_M 4 tg32 51.19 49.85 0.97
qwen35moe 35B.A3B Q4_K_M 8 tg32 52.69 51.45 0.98
qwen35moe 35B.A3B Q4_K_M 16 tg32 54.36 52.92 0.97
qwen35moe 35B.A3B Q4_K_M 32 tg32 47.28 46.24 0.98

There is indeed a lot of variability from run to run, so it's difficult to make a conclusion. Maybe what we need to bench is running with -n 32,32,32,32,32 and taking the max of the separate runs instead of the average. Not sure.

Do the results look more stable on your machines?

@am17an

am17an commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

Yes I get pretty stable results. I tried -r 100 on a 5090 + AMD EPYC 7742

  Device 0: NVIDIA GeForce RTX 5090, compute capability 12.0, VMM: yes, VRAM: 32111 MiB
| model                          |       size |     params | backend    | ngl | threads | fa |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | --: | ------: | -: | --------------: | -------------------: |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |       2 |  1 |            tg32 |         25.83 ± 0.09 |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |       4 |  1 |            tg32 |         39.57 ± 1.07 |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |       8 |  1 |            tg32 |         41.64 ± 0.78 |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |      16 |  1 |            tg32 |         41.52 ± 0.49 |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |      32 |  1 |            tg32 |         39.52 ± 0.69 |

PR

| model                          |       size |     params | backend    | ngl | threads | fa |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | --: | ------: | -: | --------------: | -------------------: |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |       2 |  1 |            tg32 |         29.49 ± 0.64 |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |       4 |  1 |            tg32 |         43.41 ± 1.14 |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |       8 |  1 |            tg32 |         44.58 ± 1.22 |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |      16 |  1 |            tg32 |         44.46 ± 0.95 |
| qwen35moe 35B.A3B Q4_K - Small |  18.55 GiB |    34.66 B | CUDA       |  99 |      32 |  1 |            tg32 |         42.41 ± 1.09 |

@am17an

am17an commented Mar 22, 2026

Copy link
Copy Markdown
Contributor Author

@max-krasnyansky maybe you can also test on your machines? I'm not sure how to get rid of the noise in the measurement.

@am17an

am17an commented Mar 28, 2026

Copy link
Copy Markdown
Contributor Author

@ggerganov can you try again, for -t 1 the results should be strictly better as it uses less flops and no syncs anyway. If not then the results might be too noisy. I implemented the work stealing as used in master so everything should be the same, just less syncs and flops

| Model                    |   Threads | Test   |   t/s 51a84efc5 |   t/s ncmoe-cpu |   Speedup |
|:-------------------------|----------:|:-------|----------------:|----------------:|----------:|
| qwen35moe 35B.A3B Q4_K_S |         1 | tg32   |           16.51 |           18.50 |      1.12 |
| qwen35moe 35B.A3B Q4_K_S |         2 | tg32   |           25.24 |           28.29 |      1.12 |
| qwen35moe 35B.A3B Q4_K_S |         4 | tg32   |           39.23 |           40.98 |      1.04 |
| qwen35moe 35B.A3B Q4_K_S |         8 | tg32   |           40.62 |           42.68 |      1.05 |
| qwen35moe 35B.A3B Q4_K_S |        16 | tg32   |           40.81 |           42.63 |      1.04 |

fukuro-kun pushed a commit to fukuro-kun/fukuro-llama-cpp-turboquant that referenced this pull request Jul 20, 2026
…hed ⏭️ + TheTom#76 CPU Fusion ⏭️

3 parallele Tiefen-Evals für Tier-3 Items:

AtomicBot-ai#74 Vulkan Descriptor Indexing (Bindless) ❌ VERWORFEN:
- Redundant mit TheTom#85 Push Descriptors (✅ implementiert 2026-07-14)
- Push Descriptors eliminieren dieselben CPU-Aufrufe
- TheTom#85-Benchmark auf Mars RADV: ±0.1-0.3% (Rauschen)
- Bindless würde über Push-Descriptors hinaus <0.5% bringen
- Workload-Mismatch: Bindless für draw-heavy Rendering, nicht Compute
- Mars/Venus bandwidth-bound, nicht descriptor-bound
- Aufwand revidiert: 2-4 → 3-5 Wochen (Shader-Rewrite aller .comp-Files)

AtomicBot-ai#75 Non-blocking Pipeline Scheduling ⏭️ SPÄTER:
- PR ggml-org#19922 closed (2026-03-03, unmerged, 4+ Mo stale)
- Fork hat bereits Upstream-Pipeline-Parallelismus
- Konflikt mit TheTom#79 TP (✅+23-32% tg, split-mode-exklusiv)
- NVIDIA ungetestet, PP-Regression auf 4x MI50 gemeldet
- 2-GPU-Setup → geringer Bubble-Hebel
- Aufwand revidiert: 3-4 → 4-6 Wochen

TheTom#76 CPU Backend Operator Fusion ⏭️ SPÄTER:
- RMS_NORM+MUL Fusion bereits im Fork (PR ggml-org#22423 upstream-merged)
- MoE Gated FFN riskant: PR ggml-org#20596 zeigt Regressionen auf Consumer-CPUs
  (M2: 0.98-1.00x, qwen3moe 30B: 0.75-0.96x bei t=2-4)
- Nur auf 96-Core-EPYC konsistente Gains (1.04-1.08x)
- Styx/Uranus haben Consumer-CPUs → wahrscheinlich Regression
- Re-Eval wenn PR ggml-org#20596 gemerged mit Regression-Freiheit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ggml changes relating to the ggml tensor library for machine learning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants