Skip to content

sycl: fix prompt-processing regression after Q4_0/Q8_0 weight reorder - #30374

Merged
Titaniumtown merged 3 commits into
ggml-org:masterfrom
marcusds:sycl-q4_0-reorder-pp
Oct 17, 2026
Merged

Titaniumtown merged 3 commits into
ggml-org:masterfrom
marcusds:sycl-q4_0-reorder-pp

Conversation

@marcusds

@marcusds marcusds commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Overview

This PR fixes a prompt-processing slowdown on Q4_0 and Q8_0 models that starts after the first decode and persists for the rest of the session, so every multi-turn chat runs at the slow rate.

On SYCL, once a Q4_0 or Q8_0 weight has been reordered by decode, every later prompt batch dequantizes it to f16 through dequantize_row_q4_0_sycl_reorder / dequantize_row_q8_0_sycl_reorder before the GEMM. Those kernels gave each work-item a whole 32-value block in 16-wide work-groups, so a sub-group's loads were 16 or 32 bytes apart and its stores 64 bytes apart. Prompt processing dropped by ~40% on Q4_0 and ~25% on Q8_0 after the first decode and stayed there (the fix speeds it back up by 66% and 33%).

The kernels now use the same per-element mapping as dequantize_block<dequantize_q4_0> / dequantize_block<dequantize_q8_0> for the plain layout (one work-item per packed byte for Q4_0, two values per work-item for Q8_0) in 256-wide work-groups, so reads and writes are contiguous across a sub-group. They launch with ceil_div + downsample_sycl_global_range like convert_unary_nc_sycl, and loop over the global range so a shrunken work-group still covers every element.

The math stays in float exactly as in the old kernels, so the dequantized values are bit-identical on both the f16 and fp32 paths. The dequantize_q4_0_reorder / dequantize_q8_0_reorder helpers are deliberately not used here, since they compute in half with GGML_SYCL_F16, which would change fp32 output.

The fp32 GEMM fallback now asserts that a reordered src0 is converted with its full row range, since the reorder kernels locate the scales from the element count. The fp16 path already had this check.

Additional information

I noticed a 40% slowdown on Arc B50, and was also able to measure the slowdown on UHD 730 iGPU.

Repro with: llama-bench -m <model> -ngl 99 -fa 1 -ctk q8_0 -ctv q8_0 -ub 1024 -p 512 -n 32 -d 0,1

Arc Pro B50 (Battlemage), Qwen3.5-9B Q4_0:

test master PR
pp512 (before any decode) 1029 1028
pp512 @ d1 (after decode, reordered) 607 1005
tg32 33 33
tg32 @ d1 33 33
server prompt, 1.5k tokens, after decode 767 1150

Arc Pro B50, Qwen3.5-2B Q8_0:

test master PR
pp512 (before any decode) 4084 4092
pp512 @ d1 (after decode, reordered) 3117 4161
tg32 81 81
tg32 @ d1 81 81
server prompt, 1.5k tokens, after decode 3506 4335

Correctness

Arc Pro B50, weights reordered by warmup, master vs this PR:

check Qwen3.5-9B Q4_0 Qwen3.5-2B Q8_0
wikitext-2 perplexity, full run, master 8.5301 ± 0.0587 12.7760 ± 0.0948
wikitext-2 perplexity, full run, PR 8.5301 ± 0.0587 12.7760 ± 0.0948
greedy 128 tokens after a 7.3k-token prompt identical identical
greedy 128 tokens after a 17k-token prompt identical identical
  • test-backend-ops -o MUL_MAT: 2272/2272 pass on SYCL0.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes: coded and debugged with the assistlaude Fable 5.1

@marcusds
marcusds marked this pull request as ready for review October 12, 2026 08:04
@marcusds
marcusds requested a review from a team as a code owner October 12, 2026 08:04
@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 Oct 12, 2026
@ggml-gh-bot

ggml-gh-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

Hi @marcusds, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • Multiple open PRs from a new contributor: We limit new contributors (those without a previously merged PR) to 1 open PR at a time. You currently have 2 open PRs.

Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

@marcusds

marcusds commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @marcusds, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

* **Multiple open PRs from a new contributor**: We limit new contributors (those without a previously merged PR) to **1 open PR at a time**. You currently have 2 open PRs.

Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

This is the only open PR I have.

@marcusds
marcusds force-pushed the sycl-q4_0-reorder-pp branch from 6fb59d9 to dd59f65 Compare October 13, 2026 05:33
@marcusds marcusds changed the title sycl: coalesce the reordered Q4_0 dequantize kernel sycl: fix prompt-processing regression after Q4_0/Q8_0 weight reorder Oct 13, 2026
@marcusds
marcusds force-pushed the sycl-q4_0-reorder-pp branch from dd59f65 to 2517f14 Compare October 13, 2026 05:45
@anantshri

Copy link
Copy Markdown
Contributor

Tested on Intel Arc Pro B70 (BMG, 32 GB), oneAPI 2026.1.1, Level Zero. Base = master-side SYCL for the files this PR touches (my branch carries unrelated SYCL patches; none of them touch the Q4_0/Q8_0 reorder dequant kernels).

The Q8_0 half of this reproduces here, and larger than on B50:

llama-bench -ub 1024 -p 512 -n 32 -d 0,1 -r 2, Spark-X2.5-4B-Uncensored Q8_0:

test base this PR
pp512 (d0) 5846.8 5840.8
pp512 (d1, after reorder) 3196.5 (-45%) 5583.2 (-4%)
tg32 99.7 99.7

So +75% on post-decode pp512 for this model, decode unchanged. The plain Spark-X2.5-4B Q8_0 did not show the d1 collapse on either binary here (7121 -> 7493 base), so whatever gates the slow path does not trigger for that one on B70; the uncensored model is the clean repro.

test-backend-ops test -b SYCL0 -o MUL_MAT: 2272/2272, same as your run.

One nitpick below.

One question on the correctness section: the description says the dequantized values are unchanged, verified on Q4_0. On Q8_0 I see a small but consistent perplexity shift with the PR (wikitext 4 chunks, -b 512 -ub 512, same model as above):

chunk base this PR delta
1 28.2832 28.4347 +0.54%
2 39.1280 39.2821 +0.39%
3 27.0185 27.0770 +0.21%
4 25.7834 25.8045 +0.08%

All shifts positive. Could dequantize_q8_0_reorder differ from the old kernel in rounding order (e.g. computing the product differently before the f16 store)? It is small enough to be harmless in practice, but the "values unchanged" claim looks Q4_0-specific, and it would be good to either match the old kernel bit-for-bit or note the difference in the description.

@marcusds
marcusds force-pushed the sycl-q4_0-reorder-pp branch from 2517f14 to 0693472 Compare October 13, 2026 18:07
@marcusds

Copy link
Copy Markdown
Contributor Author

@anantshri

Thank you for taking the time to review this.

You were right about the rounding. My review cleanup had switched both kernels to the shared dequantize_q4_0_reorder / dequantize_q8_0_reorder helpers, which compute in half precision when GGML_SYCL_F16 is on, while the old reorder kernels multiplied in float. I've put float math back in both kernels (keeping the new thread mapping), so the output is now bit-identical to the old kernels.

I couldn't reproduce the shift on my side though, even using (toandev/Spark-X2.5-4B-Uncensored-GGUF, Q8_0). Arc Pro B50, wikitext-2, -b 512 -ub 512 --chunks 4:

chunk master PR
1 61.0808 61.0808
2 50.1810 50.1810
3 41.6652 41.6652
4 58.0788 58.0788

My absolute values are about double yours, so we may not be running the same file or setup. Could you share which GGUF you used?

The slowdown and fix do reproduce with your model here (llama-bench -ub 1024 -p 512 -n 32 -d 0,1 -r 2):

test master PR
pp512 2218 2208
pp512 @ d1 (after decode, reordered) 1479 2192

Full-run checks on the updated branch, Arc Pro B50, master vs PR:

check Qwen3.5-9B Q4_0 Qwen3.5-2B Q8_0
wikitext-2 perplexity, full run 8.5301 / 8.5301 12.7760 / 12.7760
greedy 128 tokens after 7.3k and 17k-token prompts identical identical
test-backend-ops -o MUL_MAT 2272/2272 pass 2272/2272 pass

Also, you mentioned a nitpick below, but maybe it wasn't posted?

@anantshri

Copy link
Copy Markdown
Contributor

One Correction: the perplexity table in my earlier comment was mislabeled — those runs actually used the plain Spark-X2.5-4B.gguf (also Q8_0), not the uncensored file. My command line had a duplicated -m and the last one wins, so both legs ran the stock weights. The uncensored file is the one the llama-bench table used, and that part was correct. Sorry for the confusion - your inability to reproduce on the uncensored model makes sense now.

The model you asked about: toandev/Spark-X2.5-4B-Uncensored-GGUF, pinned revision 8d13862e2cfa9e1e67b1f19bf17759f41bd583f2, file Spark-X2.5-4B-Uncensored-Q8_0.gguf, 4,731,537,696 bytes, sha256 9962319f89dd1ad769450a4a52546c50c485cd353a63d3be764d5f3a624f3a03. My perplexity corpus is the classic wiki.test.raw with -c 512 -b 512 -ub 512 --chunks 4, which likely explains the different absolute values from your wikitext-2 run.

Your fix verified on B70. I cherry-picked the updated head (06934726) onto the same base as before and re-ran the comparison with identical env on both legs:

uncensored model, wiki.test.raw, -c 512 -b 512 -ub 512 --chunks 4:

chunk base this PR
1 38.6121 38.6049
2 46.8385 46.8364
3 33.8615 33.8623
4 31.9362 31.9349

stock model, same flags:

chunk base this PR
1 28.3146 28.1473
2 39.2074 39.0146
3 27.0321 26.9610
4 25.7836 25.7515

Deltas are now mixed-sign and within the run-to-run spread I measure on the base binary alone (the stock model moves ~0.5% between identical base runs at these flags), so I read this as the float-math restoration having removed the systematic positive shift — consistent with your half-precision-helper diagnosis, which also explains why I saw it only under GGML_SYCL_F16 builds.

The performance fix is unchanged on the new head, same repro as before (-ub 1024 -p 512 -n 32 -d 0,1 -r 2, uncensored): pp512@d1 3196 -> 5605 (base 5850@d0), and test-backend-ops -o MUL_MAT is 2272/2272.

On the nitpick: that line was a leftover from an earlier draft - the correctness question was the only substantive point, and it is now resolved. Nothing outstanding from my side; the PR looks good from here.

@marcusds
marcusds force-pushed the sycl-q4_0-reorder-pp branch from 0693472 to 12acc41 Compare October 14, 2026 05:51

@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!

There is perf increase of PP512 on Arc770 for Q4_0 and Q8_0, no impact of Q4_K.

GGUF Test fa Base t/s Primary t/s Increase Rate (Primary vs Base)
llama-2-7b.Q4_0.gguf pp512 0 807.15 904.61 12.07%
llama-2-7b.Q4_0.gguf pp512 1 955.34 956.66 0.14%
llama-2-7b.Q4_0.gguf tg128 0 47.74 47.84 0.21%
llama-2-7b.Q4_0.gguf tg128 1 54.37 54.30 -0.13%
GGUF Test fa Base t/s Primary t/s Increase Rate (Primary vs Base)
Meta-Llama-3-8B.Q8_0.gguf pp512 0 797.55 865.22 8.48%
Meta-Llama-3-8B.Q8_0.gguf pp512 1 877.20 876.86 -0.04%
Meta-Llama-3-8B.Q8_0.gguf tg128 0 36.79 36.78 -0.03%
Meta-Llama-3-8B.Q8_0.gguf tg128 1 39.29 39.31 0.05%

Thank you!

Comment thread ggml/src/ggml-sycl/ggml-sycl.cpp Outdated
Comment thread ggml/src/ggml-sycl/convert.cpp
Comment thread ggml/src/ggml-sycl/convert.cpp
Once the first token has been generated, prompt processing of a Q4_0 model
drops by about 40% and stays there, so every multi-turn chat runs at the
slow rate. Decode reorders the Q4_0 weights, and from then on every prompt
batch dequantizes them to f16 through dequantize_row_q4_0_sycl_reorder
before the GEMM. That kernel gave each thread a whole 32-value block, so
neighbouring threads read and wrote 16/32 elements apart.

Use one thread per qs byte instead, the same mapping as
dequantize_block<dequantize_q4_0>, which in the reorder layout makes both
the reads and the writes contiguous across threads. The math stays in
float like the old kernel, so the output is bit-identical; the
dequantize_q4_0_reorder helper computes in half with GGML_SYCL_F16, which
would change f32 output. The kernel launches with ceil_div and
downsample_sycl_global_range like convert_unary_nc_sycl, and loops over
the global range so a shrunken work-group still covers every byte. The
fp32 GEMM fallback now asserts that a reordered src0 is converted with its
full row range, since the reorder kernels locate the scales from the
element count.

Qwen3.5-9B Q4_0, Arc Pro B50, llama-bench -fa 1 -ctk/-ctv q8_0 -ub 1024:
  pp512 before any decode   1029 -> 1028 t/s
  pp512 after a decode       607 -> 1005 t/s
  tg32                        33 ->   33 t/s
llama-server, 1490-token prompt after a decode: 767 -> 1150 t/s.
wikitext-2 perplexity and greedy output on 7k/17k-token prompts are
identical to master; test-backend-ops MUL_MAT passes.
Same regression as the reordered Q4_0 kernel: once decode has reordered a
Q8_0 weight, every later prompt batch dequantizes it through a kernel that
gave each thread a whole 32-value block, so neighbouring threads read and
wrote 32 elements apart and prompt processing stayed slow for the rest of
the session.

Use two values per thread, the same mapping as
dequantize_block<dequantize_q8_0>; in the reorder layout the qs of all
blocks are contiguous, so both the reads and the writes are contiguous
across threads. The math stays in float like the old kernel, so the output
is bit-identical; the dequantize_q8_0_reorder helper computes in half with
GGML_SYCL_F16, which would change f32 output. Launch with ceil_div and
downsample_sycl_global_range like convert_unary_nc_sycl and loop over the
global range so a shrunken work-group still covers every value.

Qwen3.5-2B Q8_0, Arc Pro B50, llama-bench -fa 1 -ctk/-ctv q8_0 -ub 1024:
  pp512 before any decode   4084 -> 4092 t/s
  pp512 after a decode      3117 -> 4161 t/s
  tg32                        81 ->   81 t/s
llama-server, 1490-token prompt after a decode: 3506 -> 4335 t/s.
wikitext-2 perplexity and greedy output on 7k/17k-token prompts are
identical to master; test-backend-ops MUL_MAT passes.
It only fires for row-split tensors, which are never reordered.
@marcusds
marcusds force-pushed the sycl-q4_0-reorder-pp branch from 3190c44 to 35e0908 Compare October 16, 2026 09:40

@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!

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 16, 2026
iki007 added a commit to iki007/llama.cpp that referenced this pull request Oct 21, 2026
…own copy of our work

Upstream added an IQ3_S / IQ3_XXS weight reorder (ggml-org#29107), a Q8_0 MoE reorder (ggml-org#29337) and wide
q4_K / q5_K dequant stores (ggml-org#29696), all of which this branch already carries. The replay kept our
side of each conflicting hunk but left upstream's copies next to ours; this removes them.

- IQ3: our reorder, MMVQ, MoE GEMV and dequant stay, upstream's go. Upstream's IQ3_S layout puts d
  before the scales and ours after, so the two must not mix.
- Q8_0 MoE: one reorder_qw_q8_0_moe (upstream's, same layout); our MoE reorder GEMV is kept.
- dequant: our wide K-quant and q8_0 reorder dequant are kept (they also cover the reordered
  layouts); upstream's q4_0 reorder fix (ggml-org#30374) stays.

It also restores upstream's side where the replay took ours: the f16 src1 pointer in
ggml_sycl_op_mul_mat_sycl (ggml-org#29889), SET_ROWS on all threads (ggml-org#30454), and the lilicorr block size
and top_k in the DFlash draft (ggml-org#30506).

Assisted-by: Claude Opus 5.5
iki007 added a commit to iki007/llama.cpp that referenced this pull request Oct 22, 2026
…own copy of our work

Upstream added an IQ3_S / IQ3_XXS weight reorder (ggml-org#29107), a Q8_0 MoE reorder (ggml-org#29337) and wide
q4_K / q5_K dequant stores (ggml-org#29696), all of which this branch already carries. The replay kept our
side of each conflicting hunk but left upstream's copies next to ours; this removes them.

- IQ3: our reorder, MMVQ, MoE GEMV and dequant stay, upstream's go. Upstream's IQ3_S layout puts d
  before the scales and ours after, so the two must not mix.
- Q8_0 MoE: one reorder_qw_q8_0_moe (upstream's, same layout); our MoE reorder GEMV is kept.
- dequant: our wide K-quant and q8_0 reorder dequant are kept (they also cover the reordered
  layouts); upstream's q4_0 reorder fix (ggml-org#30374) stays.

It also restores upstream's side where the replay took ours: the f16 src1 pointer in
ggml_sycl_op_mul_mat_sycl (ggml-org#29889), SET_ROWS on all threads (ggml-org#30454), and the lilicorr block size
and top_k in the DFlash draft (ggml-org#30506).

Assisted-by: Claude Opus 5.5
edwardyoon pushed a commit to edwardyoon/focus-llama that referenced this pull request Oct 24, 2026
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 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