Skip to content

sycl: single-device still requires sync#25741

Closed
malsbat wants to merge 1 commit into
ggml-org:masterfrom
aicss-genai:fattn-single-device-sync
Closed

sycl: single-device still requires sync#25741
malsbat wants to merge 1 commit into
ggml-org:masterfrom
aicss-genai:fattn-single-device-sync

Conversation

@malsbat

@malsbat malsbat commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Overview

Fix garbage output generated on second prompt in multi-turn conversation with model Qwen3-14B-Q4_K_M.

Additional information

This can be reproduced with:

$ ./build/bin/llama-cli -m /models/Qwen3-14B-Q4_K_M.gguf -ngl 100
> foo
# good output
> bar
# garbage output

The problematic commit, 32b741c, was identified with help from git bisect.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES, help to identify issue once the right commit was found.

Signed-off-by: Todd Malsbary <todd.malsbary@intel.com>
@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 Jul 15, 2026
@malsbat

malsbat commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@hmscider, please take a look at this, the repro steps are in the description. If this is the right fix let me know and I will mark it ready for review or close it if not.

@andreyzagoruy

Copy link
Copy Markdown

Can confirm garbled output for Gemma 4 12B as well

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

@malsbat
malsbat marked this pull request as ready for review July 17, 2026 15:28
@malsbat
malsbat requested a review from a team as a code owner July 17, 2026 15:28
@hibiday

hibiday commented Jul 17, 2026

Copy link
Copy Markdown

I arrived at similar code from a different (and probably incorrect) hypothesis at #25825. I confirmed that this code alone fixes the problem in Qwen3.5-0.8B.

0.02.364.580 I
0.02.364.622 I system_info: n_threads = 16 (n_threads_batch = 16) / 32 | CPU : LLAMAFILE = 1 | OPENMP = 1 | REPACK = 1 |
0.02.364.626 I perplexity: tokenizing the input ..
0.02.375.187 I perplexity: tokenization took 10.557 ms
0.02.375.229 I perplexity: calculating perplexity over 2 chunks, n_ctx=4096, batch_size=2048, n_seq=1
0.03.868.580 I perplexity: 1.49 seconds per pass - ETA 0.03 minutes
[1]7.5567,[2]7.1586,
0.04.596.944 I Final estimate: PPL = 7.1586 +/- 0.30513
| model                          |       size |     params | backend    | ngl |  fa |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | --: | --: | --------------: | -------------------: |
| qwen35 0.8B Q8_0               | 763.78 MiB |   752.39 M | SYCL       |  99 |   1 |          pp2048 |     11506.75 ± 25.84 |

build: bbe884c-pr25741 (9925)

johnkarlhill added a commit to johnkarlhill/llama.cpp that referenced this pull request Jul 18, 2026
Extends the oneDNN SDPA path (PR ggml-org#25222) to handle non-F16 KV caches by
dequantizing or converting K/V to dense FP16 on-device before feeding
them into the SDPA graph. The fused systolic kernel then runs identically
to the native FP16 path.

Supported KV types:
  - Q4_0, Q4_1, Q5_0, Q5_1, Q8_0: to_fp16_sycl / to_fp16_nc_sycl
  - F32: cont_to_f16_sycl<float>
  - BF16 and IQ types are excluded (no conversion kernel available)

Gate: non-F16 requires K >= 1024 and Q >= 32 (prefill only).
F16 KV runs at any length (existing behavior).

Also includes the stream sync fix (stream->wait_and_throw() unconditional,
PR ggml-org#25741 by @malsbat) and removal of V_is_K_view aliasing (K and V are
always dequantized to separate buffers).

Co-Authored-By: Claude <noreply@anthropic.com>
@meatposes

Copy link
Copy Markdown

We hit this same bug in production (Arc Pro B70, Qwen3.6-27B Q4_K, 131k ctx) and root-caused it
while this PR was open. The sync does fix the symptom, but I believe the actual defect is
narrower, and fixing it directly lets you keep the single-device fast path (and its ~6% PP)
that the original commit was trying to preserve.

Root cause: a use-after-return, not missing device synchronization.

const sycl::half scale_h = (sycl::half) (1.0f / kq_scale);   // stack local
ggml_sycl_pool_alloc<sycl::half> scbuf(ctx.pool(), 1);
stream->memcpy(scbuf.get(), &scale_h, sizeof(sycl::half));   // async, sourced from the stack

The queue is in-order and the dnnl stream wraps the same queue, so every device-side hazard
(staging kernels -> SDPA -> permute -> pool-slot reuse) already serializes correctly. The one
thing that escapes ordering is host-side: this memcpy's source is a stack local. The copy is
queued behind the K/V staging kernels; once n_kv is large enough (>= ~26k on B70 -- staging is
tens of MB) the function returns before the copy runs, the frame is recycled, and the SDPA
receives a garbage scale. Output collapses to a single repeated token and the poisoned KV cache
corrupts the rest of the session. Short contexts win the race by accident, which is why it looks
like a "second prompt" / long-context bug, and test-backend-ops never sees it (FLASH_ATTN_EXT
caps at kv=1024, and single-op runs sync between ops).

wait_and_throw() fixes this only as a side effect: blocking the host keeps the frame alive
until the copy completes. That works, but it re-pays a host round-trip on every prefill FA call,
and it leaves the UAF in place -- if anyone later re-loosens the sync to reclaim the perf, the
garbage returns.

Direct fix: upload the scale synchronously, once, into a persistent device scalar cached per
(device, value) -- the scale is constant per model. With that, the single-device no-sync path is
safe. Verified on B70: temp-0 output byte-identical to the native FA path through 32k-deep
prefill, prefill depth-flat at 820-840 t/s (vs ~350 native at 32k depth), i.e. the fix keeps the
full speed this feature was added for.

Deterministic reproducer (unmodified build, GGML_SYCL_F16=ON + GGML_SYCL_DNN=ON): send a single
~28k-token prompt ending "Reply with exactly: HELLO WORLD" at temperature 0.
GGML_SYCL_FA_ONEDNN=1 -> repeated-token garbage / server 500; =0 -> "HELLO WORLD". With the
scale fix applied and the sync still removed, both answer correctly.

Happy to open the alternative as a follow-up PR (patch is +46/-10: the scale fix, the multi-GPU
wait kept unchanged, an optional GGML_SYCL_FA_ONEDNN_MAX_KV ceiling, and large-KV
test-backend-ops cases covering the kv=1024 blind spot) -- or fold any part of it into this one
if @malsbat prefers.

@meatposes

Copy link
Copy Markdown

Follow-up PR with the root-cause fix is up: #25880

@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 Jul 20, 2026
@malsbat

malsbat commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @meatposes for the detailed write-up. I only wanted to highlight the issue and suggest a possible fix in this PR. I'm happy to abandon this for your solution which doesn't lose the performance benefit. I'll defer to @arthw for how he'd prefer to handle this.

@arthw

arthw commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@malsbat
If #25880 can fix this issue without adding more wait(), I prefer to use it.
Then this PR can be closed.

Thank you very much! :)

@arthw arthw removed the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Jul 21, 2026
NickM-27 pushed a commit to NickM-27/llama.cpp that referenced this pull request Jul 21, 2026
…ntion path

The scale was uploaded with an async memcpy sourced from a stack local. On the
in-order queue that copy is ordered behind the K/V staging kernels; once n_kv is
large enough (>= ~26k observed on Arc Pro B70) the staging outlives the host
stack frame and the copy reads recycled memory, feeding the SDPA a garbage scale.
Output then collapses to a single repeated token and the KV cache is poisoned
for the rest of the session.

Short contexts win the race by accident, and test-backend-ops caps
FLASH_ATTN_EXT at kv=1024, which is why CI never caught it. The previous
device_count > 1 wait_and_throw() gate (and reverting it, PR ggml-org#25741) fixes the
symptom only by keeping the frame alive across the copy at the cost of a host
sync on every FA call.

Fix: cache one device scalar per (device, value) -- the scale is constant per
model -- and upload it synchronously once. The single-device fast path (no
per-call host sync) is then safe: every device-side hazard already serializes
on the in-order queue. The multi-GPU conservative wait is kept unchanged.

Also:
- GGML_SYCL_FA_ONEDNN_MAX_KV env (0 = unlimited): optional n_kv ceiling that
  routes very long sequences to the native FA kernel.
- test-backend-ops: FLASH_ATTN_EXT F16 cases up to kv=65536 (Qwen3.6-27B
  geometry hsk=hsv=256 GQA 6, and hsk=128 GQA 4), closing the kv=1024 blind
  spot. Note the race itself needs a live multi-op pipeline to reproduce;
  single-op runs pass even on broken builds.

Verified on Arc Pro B70 (bmg_g31), Qwen3.6-27B Q4_K, -c 131072: output
byte-identical at temp 0 to the native FA path through 32k-deep prefill, with
prefill depth-flat at 820-840 t/s (vs 340-350 native at 32k depth).

Assisted-by: Claude Fable 5
@malsbat

malsbat commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Closing, #25880 fixes the issue for me also.

@malsbat malsbat closed this Jul 21, 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 SYCL https://en.wikipedia.org/wiki/SYCL - GPU programming language

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants