Skip to content

llama : synchronize before mutating graph state in process_ubatch - #28058

Draft
allenzz-dev wants to merge 2 commits into
ggml-org:masterfrom
allenzz-dev:fix-kv-race-integrated-gpu
Draft

allenzz-dev wants to merge 2 commits into
ggml-org:masterfrom
allenzz-dev:fix-kv-race-integrated-gpu

Conversation

@allenzz-dev

Copy link
Copy Markdown

Fixes #28056 (cross-request KV-cache contamination on integrated GPUs — full root-cause analysis and probe evidence there).

What

Two commits:

  1. llama : synchronize before mutating graph state in process_ubatch — the core fix. The sync before set_inputs was gated on cparams.pipeline_parallel (always false on a single GPU), and the graph-rebuild path (sched_reset + alloc_graph) had no sync at all. Since graph_compute is fire-and-forget (ggml_backend_sched_graph_compute_async) and prefill ubatches produce no outputs, llama_decode returns with compute still in flight and the next ubatch's set_inputs overwrites input tensors (k_idxs/v_idxs/kq_mask/…) the device is still reading. On integrated GPUs those inputs are read zero-copy from pinned host memory (no protective split-input copy like on dGPUs), so the race scatters in-flight K/V writes to wrong cells and leaves stale K/V from a previous request visible under the new request's mask. The fix makes the synchronize unconditional and hoists it above the can_reuse branch so it also covers the rebuild path.

  2. server : do not cache-reuse on memories that cannot erase a mid-range — related hardening found while investigating. A recurrent state cannot represent a partial erase, but llama_memory_recurrent::seq_rm returned true for one while leaving the removed tokens in the state, so the --cache-reuse KV surgery appeared to succeed and stale recurrent state leaked into the new prompt positions on hybrid/recurrent models. This rejects the unrepresentable erase and gates can_cache_reuse on the context being pure attention (seq_rm type PART). Happy to split this into a separate PR if preferred.

Verification

Reproducer (AMD gfx1151 APU / Strix Halo, HIP, Qwen3.6-35B-A3B hybrid, llama-server -b 2048 -ub 2048 -fa on -np 1, deterministic sampling): chat about a distinctive topic → send an unrelated ~21k-token document + questions. Before: 4/4 responses quote the chat verbatim, and per-cell K-cache hash probes show chat-era data scattered into the document's cell range while document chunks fail to land. After: 0/12 contaminated across 3 runs, and the probed cell shows the healthy trajectory (boot value through the chat era, document data lands at its chunk and stays put).

Performance

Zero measured cost on the affected hardware — the async pipeline only queued deeper, the GPU was already saturated:

metric before after
21k-token prefill 1230.8 t/s 1231.8 t/s
2k-token prefill 957–970 t/s 957–966 t/s
decode 48.5 t/s 48.4 t/s

A conservative alternative would gate the sync on the integrated/zero-copy condition; unconditional measured free here and also covers any other configuration where graph inputs are device-visible without a copy.

🤖 Generated with Claude Code

…ml-org#28056)

The synchronize before set_inputs was gated on cparams.pipeline_parallel,
which is always false on a single GPU, and the graph-rebuild path
(sched_reset + alloc_graph) had no synchronization at all. graph_compute
ends with ggml_backend_sched_graph_compute_async, and prefill ubatches
produce no outputs, so llama_decode returns with the compute still in
flight and the next ubatch's set_inputs overwrites input tensors
(k_idxs/v_idxs/kq_mask/...) the device is still reading.

On integrated GPUs these inputs live in pinned host memory read zero-copy
by the device (discrete GPUs get a protective sync+copy via split
inputs), so the race scatters in-flight K/V writes to wrong cells and
leaves stale K/V from a previous request visible under the new request's
mask - observed as cross-request KV-cache contamination in llama-server
on an AMD gfx1151 APU (details in ggml-org#28056).

Make the synchronize unconditional and hoist it above the can_reuse
branch so it also covers the rebuild path. Measured cost on the affected
hardware is zero: the async pipeline only queued deeper (21k-token
prefill 1230.8 -> 1231.8 t/s, decode 48.5 -> 48.4 t/s).
The --cache-reuse KV surgery needs mid-range seq_rm in addition to
seq_add. A recurrent state cannot represent a partial erase, but
llama_memory_recurrent::seq_rm returned true for one while leaving the
removed tokens in the state, so the surgery appeared to succeed and
stale recurrent state leaked into the new prompt positions on
hybrid/recurrent models.

Reject the unrepresentable erase in seq_rm, and gate can_cache_reuse on
the context being pure attention (seq_rm type PART).
@allenzz-dev
allenzz-dev requested review from a team and ggerganov as code owners August 31, 2026 00:32
@ggml-gh-bot

ggml-gh-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

Hi @allenzz-dev, thanks for your contribution!

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

  • PR Template not respected: Please respect the template when creating a new pull request. Make sure to fill out all required sections.

  • AI-generated content: While code is allowed to be generated by AI, please write the PR description and commit messages on your own without the help of AI.


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

@ggml-gh-bot ggml-gh-bot Bot added the draft PR will be changed to draft by github-actions bot label Aug 31, 2026
@github-actions
github-actions Bot marked this pull request as draft August 31, 2026 00:36
@github-actions github-actions Bot removed the draft PR will be changed to draft by github-actions bot label Aug 31, 2026
crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Sep 1, 2026
Squashed cherry-pick of ggml-org#28058, which fixes ggml-org#28056
(cross-request KV-cache contamination on integrated GPUs).

The sync before set_inputs was gated on cparams.pipeline_parallel (always
false on a single GPU), and the graph-rebuild path had no sync at all.
graph_compute is fire-and-forget and prefill ubatches produce no outputs,
so llama_decode returns with compute still in flight and the next ubatch's
set_inputs overwrites input tensors (k_idxs/v_idxs/kq_mask/...) the device
is still reading. On integrated GPUs those inputs are read zero-copy from
pinned host memory, with no protective split-input copy as on dGPUs, so
the race scatters in-flight K/V writes to wrong cells and leaves stale K/V
from a previous request visible under the new request's mask.

Also carries the PR's second commit: llama_memory_recurrent::seq_rm now
rejects an unrepresentable mid-range partial erase instead of returning
true, and the server only enables cache-reuse on pure attention contexts.

Reproduced locally on gfx1150 (Strix Point, ROCm 10) with LFM2.5-VL-3B
Q8_0: with -b 2048 -ub 512, any prompt prefilled across several ubatches
returned the *previous* request's answer verbatim. Vulkan on the same
host was unaffected, as were -b 512 -ub 512 and -b 2048 -ub 2048.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@crusaderky

Copy link
Copy Markdown
Contributor

I tested this on gfx1150 rather than gfx1151 and on a third model family. It fixes the issue.

Radeon 890M (Strix Point) on Ubuntu 26.04, ROCm 10.0, HIP.
LFM2.5-VL-3B-GGUF:Q8_0, arch lfm2, hybrid short-conv + attention. -c 32768 -ngl 99 --parallel 1 --kv-unified -fa on -ctk q8_0 -ctv q8_0.

crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Sep 2, 2026
Squashed cherry-pick of ggml-org#28058, which fixes ggml-org#28056
(cross-request KV-cache contamination on integrated GPUs).

The sync before set_inputs was gated on cparams.pipeline_parallel (always
false on a single GPU), and the graph-rebuild path had no sync at all.
graph_compute is fire-and-forget and prefill ubatches produce no outputs,
so llama_decode returns with compute still in flight and the next ubatch's
set_inputs overwrites input tensors (k_idxs/v_idxs/kq_mask/...) the device
is still reading. On integrated GPUs those inputs are read zero-copy from
pinned host memory, with no protective split-input copy as on dGPUs, so
the race scatters in-flight K/V writes to wrong cells and leaves stale K/V
from a previous request visible under the new request's mask.

Also carries the PR's second commit: llama_memory_recurrent::seq_rm now
rejects an unrepresentable mid-range partial erase instead of returning
true, and the server only enables cache-reuse on pure attention contexts.

Reproduced locally on gfx1150 (Strix Point, ROCm 10) with LFM2.5-VL-3B
Q8_0: with -b 2048 -ub 512, any prompt prefilled across several ubatches
returned the *previous* request's answer verbatim. Vulkan on the same
host was unaffected, as were -b 512 -ub 512 and -b 2048 -ub 2048.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Sep 14, 2026
Squash of ggml-org#28058, which fixes ggml-org#28056
(cross-request KV-cache contamination on integrated GPUs)
TrevorS added a commit to TrevorS/llama.cpp that referenced this pull request Sep 16, 2026
…stream ggml-org#28058)

Upstream commits 148f18d and 06e43d5 by allenzz-dev, applied as one
diff. The sched synchronize before set_inputs was conditional on pipeline
parallelism; on an integrated GPU the device reads graph inputs zero-copy
from pinned host memory, so an unsynchronized overwrite scattered in-flight
K/V writes to wrong cells and left stale K/V visible under the new mask
(cross-request contamination, upstream ggml-org#28056). GB10 reports integrated=1.
Also: a recurrent memory rejects a mid-range partial erase instead of
silently keeping the removed tokens, and the server's cache-reuse surgery
only runs on pure attention contexts.
crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Sep 25, 2026
Squash of ggml-org#28058, which fixes ggml-org#28056
(cross-request KV-cache contamination on integrated GPUs)
crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Sep 28, 2026
Squash of ggml-org#28058, which fixes ggml-org#28056
(cross-request KV-cache contamination on integrated GPUs)
crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Oct 1, 2026
Squash of ggml-org#28058, which fixes ggml-org#28056
(cross-request KV-cache contamination on integrated GPUs)
@paidaxu

paidaxu commented Oct 8, 2026

Copy link
Copy Markdown

Tested on NVIDIA Jetson Orin NX (CUDA iGPU): this PR fixes the bug

I hit the same bug on an NVIDIA Jetson. #29450 says NVIDIA testing is still needed, so here are my results.

Setup

  • Jetson Orin NX 16 GB, L4T R35.6.0, CUDA 11.4 (sm_87), 25 W mode
  • llama.cpp: an older copy bundled with a TinyLLaVA project. It is older than cuda : Disable host buffers on integrated GPUs (#15034) #16308, so integrated = prop.integrated is still active on CUDA.
  • Model: TinyLLaVA-Phi-2-SigLIP-3.1B (phi2, Q8_0) + mmproj Q8_0
  • llama-cli / llama-mtmd-cli, --temp 0

Problem
With -ngl 33 (all layers on GPU), if the prompt is longer than one ubatch, the output is only GGGGGGGG... from the first token. An image prompt (~750 tokens) and a long text prompt (~700 tokens) both fail. Short prompts work.

Setting (-ngl 33 unless noted) Output
705-token prompt, default -ub 512 (2 ubatches) GGGG
same, -ub 1024 or 2048 (1 ubatch) OK
same, -ngl 32 OK
same, CUDA_LAUNCH_BLOCKING=1 OK
same, GGML_CUDA_DISABLE_GRAPHS=1 GGGG
same, GGML_CUDA_FORCE_CUBLAS=1 or GGML_CUDA_FORCE_MMQ=1 GGGG
same, F16 text model GGGG

On a 1505-token prompt the result is random-looking across -ub, like a race:

-ub 64 128 256 512 1024 2048
output GGGG GGGG OK OK GGGG OK

llama-eval-callback shows the same values for -ngl 33 and -ngl 32 in all 1874 ops, with no NaN/Inf. So this is not an overflow.

What I found in the scheduler
  • With GGML_SCHED_DEBUG=2 and -ngl 33, the CUDA0 split has 0 inputs. GPU nodes in every layer read the graph inputs directly from the CUDA_Host buffer: inp_pos (ROPE), k_idxs / v_idxs (SET_ROWS), and inp_out_ids (GET_ROWS).
  • Prefill ubatches have n_outputs = 0, so there is no sync after them. The next ubatch has a different size, so it takes the rebuild path, and set_inputs() overwrites these inputs while the GPU is still running the previous ubatch.
  • -ngl 32 works only by luck. The output weight is copied to the GPU in each ubatch, and that copy calls ggml_backend_synchronize().

With this PR
I added the unconditional ggml_backend_sched_synchronize() before the can_reuse check in process_ubatch, built with the same options, and tested with -ngl 33:

  • 39/39 runs OK. I tested the 705-token prompt at -ub 512, the 1505-token prompt at -ub 64/128/256/512/1024, and 2 real images at -ub 64/512, 3 times each, plus 9 speed runs.
  • Without the patch, the same failing cases still give GGGG (6/6, and the image at -ub 512 failed 3/3).

Speed, median of 3:

Case prompt before prompt after change gen before gen after
image (747 tok), -ub 512 4344 ms 4353 ms +0.19% 7.68 t/s 7.68 t/s
text (1505 tok), -ub 512 6377 ms 6411 ms +0.53% 7.01 t/s 7.01 t/s
text (1505 tok), -ub 2048 7629 ms 7636 ms +0.09% 7.00 t/s 7.01 t/s

(Before the patch the image output was GGGG, so that row is timing only.)

So on Jetson, this PR fixes the bug. Prompt processing is 0.1–0.5% slower, and generation speed does not change.

I can run more tests on this board if needed.

I used an AI assistant to help with the English and the analysis. I ran all the tests myself on our lab's Jetson.

crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Oct 13, 2026
Squash of ggml-org#28058, which fixes ggml-org#28056
(cross-request KV-cache contamination on integrated GPUs)
crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Oct 31, 2026
Squash of ggml-org#28058, which fixes ggml-org#28056
(cross-request KV-cache contamination on integrated GPUs)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Eval bug: Cross-request KV-cache contamination on integrated GPUs: missing sync in process_ubatch races zero-copy graph inputs against in-flight compute

3 participants