Skip to content

sycl: add grouped MoE XMX GEMM - #29245

Merged
Titaniumtown merged 10 commits into
ggml-org:masterfrom
cwriter:grouped_moe_xmx
Oct 8, 2026
Merged

Titaniumtown merged 10 commits into
ggml-org:masterfrom
cwriter:grouped_moe_xmx

Conversation

@cwriter

@cwriter cwriter commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Overview

The current approach to handling experts in MoE models use one GEMM per expert. These activations are relatively narrow, and cannot use the compute hardware effectively (neither ALU0/1 nor the XMX tiles). Additionally, the small inputs result in particularly inefficient dequantization.
This can be fixed by grouping many small experts into one kernel, and handling them in one operation.

This patch proposes grouped and optionally fused GEMM kernels tuned on XMX (joint_matrix).
Approach:

  • Build a host-side schedule of active expert tiles and launch as one grouped kernel instead of one GEMM per expert
  • Dequantizes IQ4_NL or IQ3_S weights into f16 tiles in local memory
  • Packs routed f32 activations into the VNNI f16 layout consumed by XMX.

Additional information

Model: unsloth/Qwen3.8-Flash-Next-GGUF:UD-IQ4_XS.
The boost happens during pp.

Mode Prompt tok/s Decode tok/s
Grouped off 274.101 18.567
Grouped on 368.298 18.743
Change +34.37% +0.95%

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes, used Claude Opus 5 to implement, and OpenAI 5.6 Sol to extract as a PR.

@cwriter
cwriter requested a review from a team as a code owner September 21, 2026 18:19
@github-actions github-actions Bot added ggml changes relating to the ggml tensor library for machine learning SYCL https://en.wikipedia.org/wiki/SYCL - GPU programming language labels Sep 21, 2026
@Titaniumtown

Copy link
Copy Markdown
Contributor

Hm, I see that you have cases for IQ4_NL + IQ3_S weights. Is there some way to generalize the kernel to accept more quant types?

@arthw arthw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's good idea!

I didn't get the benefit with Qwen2-1.5Moe.Q4_K_M.gguf.

Could you share your test method (gguf, parameter) to reproduce the result?
What's your Intel GPU in the test?

Thank you!

@cwriter

cwriter commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Hi

Hm, I see that you have cases for IQ4_NL + IQ3_S weights. Is there some way to generalize the kernel to accept more quant types?

There is, I have implemented some more types (including Q8_0), although the effects are much more limited there.
The key issue of IQ4_NL (and IQ3_S, much more) is that the dequantization overloads the scatter/gather units, because no alignment of each weight can be assumed. This makes it particularly expensive to dispatch in single units (because the memory unit needs to deal with many small unaligned accesses, and then the compute is underfed). Building the schedule on the host avoids these limits.
In terms of code parameterization, I did not yet find a very good way of parameterizing the stages.

I thought it more useful to add the biggest gains now without blowing up the commit. If you prefer, I can update the commit with more types, but we're talking about ~2% for the remaining types for the IQ model I'm tuning. The trade-off will be the size of the commit.

I didn't get the benefit with Qwen2-1.5Moe.Q4_K_M.gguf.

Could you share your test method (gguf, parameter) to reproduce the result? What's your Intel GPU in the test?

I'm using 3x Arc Pro B60 on an IQ model unsloth/Qwen3.8-Flash-Next-GGUF:UD-IQ4_XS.
KV q8_0, context 131k. I would assume your Q4_K model does not exercise IQ tensors, therefore, this patch does not really help. There are some limitations in head dims as well, but I think any Qwen MoE-IQ-type model should exercise the path.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 23, 2026
@cwriter

cwriter commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

@arthw If I remember correctly, you're on an A770 16GB, right?
I think the unsloth/Qwen3-30B-A3B-UD-IQ3_XXS should fit your card at 13G.
I've tested it on a single Arc Pro B60, with the following results

build pp512 pp2048 tg128
master 671.1 677.2 49.3
this PR 1016.9(+51.5%) 1006.9(+48.7%) 49.3

@Titaniumtown I've added all IQ types for now. Would it be ok to do other types in a later commit?

A MUL_MAT_ID over a MoE layer ran one library GEMM per expert. With 128 experts
and 8 active, each is a sliver, so launches dominate and the XMX units idle.
This groups every expert of a node into one launch, laying out the work-groups
on the host from the row offsets it already has. Weights are dequantized inside
the GEMM, gathered into the XMX A tiles instead of being written out to f16 and
read back; activations are packed once into VNNI f16.

Nine weight formats have an A stage: iq4_nl, iq3_s, iq4_xs, iq3_xxs, iq2_xxs,
iq2_xs, iq2_s, iq1_s, iq1_m. Which of them are covered decides almost the whole
result, so GGML_SYCL_XMX_GATHER_TYPES selects them as a bitmask, one bit per
format, all set by default; 0 disables the paths and is the baseline to measure
against. Quant names do not give coverage away: unsloth's UD quants mix per
tensor, so a file named IQ3_XXS holds IQ3_XXS, IQ3_S and IQ4_XS experts, and a
Q4_K_M model has no IQ tensors at all and cannot benefit.

Both paths compute in f16 on XMX. The grouped MUL_MAT_ID path therefore trades
some precision against the per-expert library GEMM it replaces, which in a
default build (GGML_SYCL_F16=OFF) computes in f32. The plain MUL_MAT half only
engages when built with GGML_SYCL_F16=ON, as it sits inside that f16 branch.

One Arc Pro B60, unsloth/Qwen3-30B-A3B-GGUF UD-IQ3_XXS, -fa on, 4 interleaved
passes after a warm-up:

    GGML_SYCL_XMX_GATHER_TYPES   pp512    pp2048   tg128
    0 (off)                      671.1     677.2    49.3
    iq4_nl|iq3_s   (42/144)      720.1     725.3    49.4    +7.3%
    all formats   (144/144)     1016.9    1006.9    49.3   +51.5%

Token generation is unchanged, as expected for a prefill path.

test-backend-ops -o MUL_MAT_ID and -o MUL_MAT against the CPU backend: 125 and
191 iq* cases, zero failures with every bit set, identical to the disabled arm.

Assisted-by: Claude Opus 5

@arthw arthw left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It shows good performance on B60:

GGUF Test fa Base t/s Primary t/s Increase Rate (Primary vs Base)
Qwen3-30B-A3B-UD-IQ3_XXS.gguf pp512 0 611.26 918.15 50.21%
Qwen3-30B-A3B-UD-IQ3_XXS.gguf pp512 1 641.75 990.48 54.34%
Qwen3-30B-A3B-UD-IQ3_XXS.gguf tg128 0 46.80 46.85 0.11%
Qwen3-30B-A3B-UD-IQ3_XXS.gguf tg128 1 49.00 49.01 0.02%

Comment thread docs/backend/SYCL.md Outdated
Comment thread ggml/src/ggml-sycl/fused-gemm.cpp Outdated
Comment thread ggml/src/ggml-sycl/fused-gemm.cpp Outdated
cwriter added 3 commits September 29, 2026 18:48
The tile was hard-coded to f16 8x16x16 with sub-group 16, so devices
without that combination (Arc A770 and other Xe-HPG parts) never took
the path. The kernels, the B packing and the grouped schedule are now
templated on the shape, and each device uses the first shape in a
fixed preference order that it lists in matrix_combinations.

Shapes: f16 8x16x16, 16x16x16, 32x64x16, 32x64x32 (sub-group 16),
f16 8x8x16 (sub-group 8, Xe-HPG) and tf32 8x16x8 (promoted, f32 data
rounded to tf32). GGML_SYCL_XMX_GATHER_SHAPES masks them, one bit per
shape, to force or exclude one.

AOT: ocloc fails on a joint_matrix kernel whose sub-group size the
target lacks, so a spir64_gen GGML_SYCL_DEVICE_ARCH build compiles only
the shapes of the matching size, and intel_gpu_* targets compile each
tile body only where it fits. Before this, an AOT build for dg2 failed
with an IGC internal error.

Assisted-by: Claude Opus 5.5
Rewrite the GGML_SYCL_XMX_GATHER_TYPES entry in SYCL.md: an example that
enables two formats at once, which GPUs and models benefit, and the exact
conditions under which an enabled format actually takes the path.

Rename the dispatch result from `ok` to `launched`.

Assisted-by: Claude Opus 5.5
The joint_matrix combination was picked once per device and assumed f16
A and B. It is now picked per call from the src1 type, among the
combinations the device lists and GGML_SYCL_XMX_GATHER_SHAPES allows.
The rules, in order: a tile that does not spill, the fastest matrix type
(f16 and bf16 before tf32), a B type equal to the src1 type, a B type
at least as precise as f16, the device's native DPAS tile (8x16x16 on
Xe2), then the largest M x K. On B60 the default is now f16 8x16x16.

The kernels are templated on the A and B types separately. bf16 8x16x16
and 8x8x16 are added as bits 64 and 128, off by default.

The plain MUL_MAT path hands src1 to the pack in its own type (F32, F16
or BF16) instead of running a separate to-f16 pass first, so an f16
src1 is packed as a plain copy.

The same ops take the XMX paths as before.

Assisted-by: Claude Opus 5.5
@cwriter

cwriter commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

@arthw

I've added a patch that generalizes the kernels into different shapes (and types).

One key change was necessary to the CMakeLists.txt so the compiler would not panic in device-specific AOT, there is some bookkeeping around not compiling for non-targeted applications.

Other targets (like AMX) should also be possible to add, but I unfortunately do not have the suitable hardware for this.

@arthw

arthw commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Here is my test result on Intel GPUs.
The result is good!

Qwen3-30B-A3B-UD-IQ3_XXS.gguf on B580x2

Test PR (t/s) Base (t/s) Difference (PR − Base) Speedup (PR / Base)
pp512 768.24 ± 4.98 465.64 ± 3.46 +302.60 ~1.65×
tg128 42.22 ± 0.08 39.38 ± 0.13 +2.84 ~1.07×

On Arc770:

GGUF Test fa Base t/s Primary t/s Increase Rate (Primary vs Base)
Qwen3.5-35B-A3B-UD-IQ2_XXS.gguf pp512 0 486.06 560.79 15.37%
Qwen3.5-35B-A3B-UD-IQ2_XXS.gguf pp512 1 494.47 570.91 15.46%
Qwen3.5-35B-A3B-UD-IQ2_XXS.gguf tg128 0 52.55 52.64 0.17%
Qwen3.5-35B-A3B-UD-IQ2_XXS.gguf tg128 1 53.67 53.75 0.15%

On Arl-h: Intel Arc 140T GPU (16GB) Windows 11, Qwen3.5-35B-A3B-UD-IQ2_XXS.gguf
No more increase:

debug log: fg_device_combos: Intel(R) Arc(TM) 140T GPU (16GB): XMX dequant-GEMM combinations available 0x90, allowed 0x3f

Model Test (Metric) Base t/s PR t/s Absolute Change (t/s) Relative Change (%)
Qwen 35B Moe Q8_0 pp512 (Prompt Processing) 148.16 ± 1.04 149.86 ± 0.78 +1.70 +1.15%
Qwen 35B Moe Q8_0 tg128 (Text Generation) 22.28 ± 0.04 21.86 ± 0.05 -0.42 -1.89%

@arthw

arthw commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

@cwriter
1.
Is it possible to support common Q types, like Q8_0, Q4_K_M in same solution?

As the guide, is that GGML_SYCL_F16=ON needed for this feature?
But I test without GGML_SYCL_F16=ON, it can get perf increasing too.
Is it as expected?

Thank you!

@cwriter

cwriter commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

@arthw

  1. Yes, it is possible. Since it was asked for repeatedly, I will add it to this PR rather than staging for a new one. Please give me a moment, I'll add it (I take it you are aware that this increases the PR size)

  2. See the comment above, indeed there is a range (~precision) issue in the SYCL F16 bit path that this PR extends to F32 by virtue of not gating the casts (assuming F16 always). I will provide an update to ensure we maintain control of this. I should note that truly overflowing should happen exceedingly rarely, and my production branch did not see correctness issues in the output despite this detail

cwriter added 3 commits October 1, 2026 21:03
The XMX dequant-GEMM paths always used f16 operands, so an activation
above 65504 became inf. GGML_SYCL_DYNAMIC_PRECISION selects the operand
type: F16, BF16 or TF32 (f32 range), or F32, which turns the XMX paths
off and keeps the library GEMM and dmmv from converting src1 to f16.
The default is F16 with GGML_SYCL_F16 and F32 otherwise, so a build
without GGML_SYCL_F16 no longer takes the XMX paths unless asked.
GGML_SYCL_XMX_GATHER_SHAPES now allows all combinations by default and
masks those of the selected type.

The src1 precision request of an op [TAG_GGML_PREC] is met in every
mode. F16, Q8 and Q4 allow any type. BF16 needs the f32 range: such an
op runs on the mode type if it has it, else bf16, else tf32, else the
library f32 GEMM. F32 runs on the library f32 GEMM, unless
GGML_SYCL_DYNAMIC_REQUIRED_PRECISION allows tf32 (TF32) or tf32 and
bf16 (BF16); F16 there ignores the requests, for testing. The library
GEMM and dmmv do not convert src1 to f16 for a BF16 or F32 request.
The XMX paths accumulate in f32, so they take any accumulator request.

The precision rule follows 54acc96 (GGML_SYCL_ENFORCE_PREC), always
on and without reporting MUL_MAT_ID as unsupported.

Assisted-by: Claude Opus 5.5
The grouped MUL_MAT_ID path now also takes Q8_0, Q4_K, Q5_K and Q6_K
expert weights, so Q4_K_M, Q5_K_M and Q8_0 MoE models get the grouped
launch. New GGML_SYCL_XMX_GATHER_TYPES bits: 512 Q8_0, 1024 Q4_K,
2048 Q5_K, 4096 Q6_K. K must be a multiple of 32 for Q8_0 and 256 for
the k-quants.

The plain MUL_MAT path keeps these types on the library GEMM: it decodes
the weights again for every 32 columns of the batch, which for these
formats costs more than the library's one dequantization.

The k-quant experts can be in the reorder (SoA) layout of reorder_qw()
after a decode step. Both layouts are decoded, from the same
per-sub-block decode.

Assisted-by: Claude Opus 5.5
The grouped dequant-GEMM decodes the reorder (SoA) layout of Q4_K, Q5_K
and Q6_K experts faster than the canonical one, but only the decode
path (mmvq) installed it. So the first prefill, and any prefill without
a decode before it, used the slower layout. Install it in the grouped
path too when the XMX paths can take the type. Every reader of these
tensors already handles the reorder, as it happens after the first
decode step anyway.

Assisted-by: Claude Opus 5.5
@cwriter

cwriter commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Provided the update. There is a small caveat: Unlike for the IQ kernels, the QK kernels make no sense for dense MUL_MAT because the GEMM path is already well-optimized, and the on-line dequant work is stressing the ALUs (specifically ALU1). There might be other optimizations possible, but at n=64, Q4_K would perform at 90%, Q8_0 at 64%, Q5_K at 50% and Q6_K at 30%. If these formats were reordered, it could provide some benefit as well. However, I believe when MKL can provide good performance, there is little point in self-rolling it. (There is some indication that the XMX path is faster for low dimensions (N<32), but I'd rather optimize the MUL_MAT in a non-MoE patch.

With the updated patch, for Qwen3-30B-A3B, I get the following performance: (pp512, tg is unchanged, running on 1-2 B60 (Q8_0 does not fit a single card))

Quant original PR QK-extended PR
Q4_K_M 915 t/s 1234 t/s (+35%)
Q8_0 (note: running on 2x B60) 747 t/s 880 t/s (+18%)
UD-IQ3_XXS 1091 t/s 1091 t/s (no regression)

The precision change is also included. The patch makes SYCL respect the set tensor precision from the GGML core. However, ggml is missing the TF32 rank.
A call setting ggml_prec_set_src(op, prec, 1) makes SYCL read the ggml_sycl_src1_prec() and applies the setting. For debugging, GGML_SYCL_DYNAMIC_REQUIRED_PRECISION=[F32]|TF32|BF16|F16 allows overriding the precision globally. Setting a value here allows the backend to rank-down, down to the specified type (F32 is zero loss, F16 is essentially ignoring the required precision completely). For example, an Op requesting BF16 will be treated as such for settings F32|TF32|BF16, but will downgrade with F16. The current SYCL backend behavior is F16, this sets the new default to F32 (respect set precisions) - however, only the XMX path currently follows this setting.

GGML_SYCL_DYNAMIC_PRECISION is the logical counterpart, it selects the type used for operations that do not specify a minimal precision. Currently, it is only applied to these new XMX kernels, and it is relevant for F16 builds.

GGML_SYCL_DYNAMIC_PRECISION XMX paths introduced by this PR Library GEMM / DMMV Q4_K_M pp512 on Arc Pro B60
F16 Normal F16 tiles May cast src1 to f16 (unchanged, potentially lossy) 1234 t/s
BF16 BF16 tiles (reduces mantissa, increases exponent bits) Same as F16 (unchanged) 947 t/s
TF32 TF32 on supporting hardware (Xe2, Xe3, Xe-HPC), otherwise, fallback to f32 Same as F16 (unchanged) 690 t/s
F32 Fallback to library F32 GEMM No cast to f16 (full precision) 820 t/s

As the table shows, even nominally same-performance bf16 takes a significant hit. TF32 is particularily slow even on supporting hardware, where the library gemm beats it significantly (I assume it's due to kernel spilling, but I did not want to further investigate it for now - there really is limited gains).
Perhaps this also makes it clear why I'm generally hesitant to increase the range for some rare edgecases: It's a very significant performance hit to not go f16.
GGML_SYCL_DYNAMIC_PRECISION defaults to the build setting: On F16 builds, it defaults to F16, on F32 builds, it defaults to F32.

I'll be happy to add more improvements, but I think that this PR is already growing out of control.

@arthw

arthw commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

@cwriter
It's great to see you have updated this PR so quick!
I think this is good enough—it even exceeded my expectations.

This PR involve the XMX direct programming (joint_matrix) firstly.
There will be more features based on XMX to be implemented in the future!

Thank you for the good job!

@arthw arthw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's good job!

Involve the XMX directly programming (based on joint_matrix).

Thank you!

@arthw arthw added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Oct 7, 2026
@ggerganov

Copy link
Copy Markdown
Member

@Titaniumtown Would you like to do a review before merging?

@Titaniumtown

Copy link
Copy Markdown
Contributor

I will look over the changes now. Been busy with work IRL.

Comment thread docs/backend/SYCL.md Outdated
Comment thread ggml/src/ggml-sycl/fused-gemm.cpp
Comment thread ggml/src/ggml-sycl/fused-gemm.cpp Outdated
Assisted-by: Claude Opus 5.5
@Titaniumtown
Titaniumtown merged commit 847f447 into ggml-org:master Oct 8, 2026
34 of 35 checks passed
@Titaniumtown

Copy link
Copy Markdown
Contributor

Thank you for your contribution!

edwardyoon pushed a commit to edwardyoon/focus-llama that referenced this pull request Oct 8, 2026
Co-authored-by: cwriter <cwriter@localhost>
(cherry picked from commit 847f447)
@BrewTestBot BrewTestBot mentioned this pull request Oct 15, 2026
1 task done
iki007 added a commit to iki007/llama.cpp that referenced this pull request Oct 21, 2026
…and gate fusion

Upstream merged its own versions of work this branch carries (ggml-org#29375 Q5_K reorder-layout MMVQ and fused GLU,
ggml-org#29245 grouped MoE XMX GEMM, ggml-org#29608 pinned upload staging, ggml-org#29687 delta-net alpha gate fusion), and the commits
before this one were replayed onto it with their own side taken in every conflicting hunk. This commit puts the
upstream side back where both belong:

- common.hpp, fusion.cpp, the op tests: both sides' additions (tile schedule of the grouped GEMM, the fused unary
  predicate, test_top_k_inf)
- mul_mat_vec_q_reorder_ncols takes both new template flags (has_tiles, shared_weights); the Q5_K multi-column
  dispatch is upstream's (weights shared and rows paired on Xe2)
- ggml_sycl_op_mul_mat_sycl tries upstream's fused dequant GEMM first and keeps the bf16 src0 route after it
- the gate / up / GLU fusion uses upstream's reorder pairs (same-type Q4_K, Q5_K on Xe2 up to 5 columns) and
  keeps this branch's rule that pairs both reordered run unfused
- MUL_MAT_ID keeps this branch's sorted loop; upstream's grouped GEMM is built but not called from it

Qwen3.8-27B on two Arc Pro B70 against the tip before the rebase (llama-bench, depth 0, three mirrored runs
each): 2 to 32 rows equal or 1-4% faster (inside today's 2-4% run-to-run spread), 48 / 64 rows 190 -> 160 /
200 -> 168 ms per batch (the fused dequant GEMM), 1024 rows equal. Perplexity unchanged on the 27B (one card
and two, 1024-row and 8-row batches) and on Ornith. 17/17 op suites clean (MUL_MAT 2711, FLASH_ATTN_EXT 4211).

Assisted-by: Claude Opus 5.5
iki007 added a commit to iki007/llama.cpp that referenced this pull request Oct 22, 2026
…and gate fusion

Upstream merged its own versions of work this branch carries (ggml-org#29375 Q5_K reorder-layout MMVQ and fused GLU,
ggml-org#29245 grouped MoE XMX GEMM, ggml-org#29608 pinned upload staging, ggml-org#29687 delta-net alpha gate fusion), and the commits
before this one were replayed onto it with their own side taken in every conflicting hunk. This commit puts the
upstream side back where both belong:

- common.hpp, fusion.cpp, the op tests: both sides' additions (tile schedule of the grouped GEMM, the fused unary
  predicate, test_top_k_inf)
- mul_mat_vec_q_reorder_ncols takes both new template flags (has_tiles, shared_weights); the Q5_K multi-column
  dispatch is upstream's (weights shared and rows paired on Xe2)
- ggml_sycl_op_mul_mat_sycl tries upstream's fused dequant GEMM first and keeps the bf16 src0 route after it
- the gate / up / GLU fusion uses upstream's reorder pairs (same-type Q4_K, Q5_K on Xe2 up to 5 columns) and
  keeps this branch's rule that pairs both reordered run unfused
- MUL_MAT_ID keeps this branch's sorted loop; upstream's grouped GEMM is built but not called from it

Qwen3.8-27B on two Arc Pro B70 against the tip before the rebase (llama-bench, depth 0, three mirrored runs
each): 2 to 32 rows equal or 1-4% faster (inside today's 2-4% run-to-run spread), 48 / 64 rows 190 -> 160 /
200 -> 168 ms per batch (the fused dequant GEMM), 1024 rows equal. Perplexity unchanged on the 27B (one card
and two, 1024-row and 8-row batches) and on Ornith. 17/17 op suites clean (MUL_MAT 2711, FLASH_ATTN_EXT 4211).

Assisted-by: Claude Opus 5.5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation ggml changes relating to the ggml tensor library for machine learning merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. SYCL https://en.wikipedia.org/wiki/SYCL - GPU programming language

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants