kv cache : optimize restoring non-contiguous cells - #27991
Conversation
When restoring state into non-contiguous destination cells (e.g. a prompt-cache snapshot into a fragmented ring), state_read_data issued one small copy per KV cell - ~1.4M copies of a few KiB each for a 40k+ token restore, taking 25-63 s on the CUDA backend. The snapshot stores cell rows in cell order, so a maximal run of consecutive destination indices maps to one contiguous block and can be restored with a single copy. Precompute the runs once and use them in all three scatter loops (K, V, transposed V). Byte-identical. The on-device reader copies with a byte cursor when the read and write chunking differs, so the batched reads are safe for it as well. Batching makes equal tensor counts with a different split reachable (save ranges [2,1] vs restore runs [1,2]); the next commit teaches the reader's 1:1 path to fall back to the byte cursor in that case. Verified in a production setup: 1,363,616 copies / 25-63 s -> 224 copies / 221-424 ms for the same restores (42,603 cells, 4 runs). Assisted-by: Claude Code (unsloth/qwen3.8-27b)
…iffer the on-device reader copies saved state back with a 1:1 copy by tensor index whenever the write and read sides recorded the same number of tensors, guarded by a per-tensor size assert. equal tensor counts do not imply equal chunking: a state restore may batch its reads per contiguous run of destination cells while the save used per-range reads, so both sides can record two tensors that split the same data differently, and the assert aborts in all builds. compare the per-tensor sizes and only take the 1:1 path when the chunking actually matches, otherwise fall through to the existing byte-cursor copy. both sides enumerate the same logical data in the same order, so the cursor copy is well-defined across tensor boundaries. Assisted-by: Claude Code (unsloth/qwen3.8-27b)
7ef363b to
5b56771
Compare
decode the same prefix on two sequences, interleaving the seq 0 cells between the seq 1 cells, so the seq 1 cells are isolated from each other in the kv cache (three cells, two saved ranges). save the seq 1 state, free the interleaved seq 0 cells, and restore: the destination is then non-contiguous (two runs), and the restore-side chunking has the same tensor count as the save-side with a different split, so the scatter path is batched per contiguous run and the on-device reader's byte-cursor fallback is exercised. the restored state is saved again on the host and compared byte for byte with the first save: the blob is serialized in sequence cell order, so the two saves are identical if and only if the scatter restore wrote exactly the same KV content. this documents the byte-identical guarantee of the run-batched scatter reads. one test per io backend: the host (CPU) path and the on-device path. Assisted-by: Claude Code (unsloth/qwen3.8-27b)
|
I think you need to gate the new tests only for https://github.com/ggml-org/llama.cpp/actions/runs/33293259535/job/99212684620?pr=27991#step:8:12896 |
5b56771 to
e1e96a8
Compare
| // free seq 0's cells so the ring is fragmented: the restore destination (seq 1's interleaved cells) stays non-contiguous | ||
| if (!llama_memory_seq_rm(llama_get_memory(ctx.get()), 0, -1, -1)) { |
There was a problem hiding this comment.
That also works and it's actually better than what I suggested.
|
Hm, the webgpu error would need an extra look: https://github.com/ggml-org/llama.cpp/actions/runs/33296367390/job/99216781100?pr=27991#step:4:3861 |
|
cc @ggml-org/ggml-webgpu or @fairydreaming in case you have a suggestion |
@ggerganov From what I see WebGPU I tested this with this fix that rounds global offset to a multiple of 4 and adds local offset that is used during the final Edit: fixed a bug in the fix. |
|
Added #28045 with the WebGPU fix, let's see how it goes. |
|
@itsnotoger merge the current master into your branch, WebGPU fix is already there |
|
Here qwen3.6 35b a3b mtp. I noticed a little bit gain in eval rate (just few t/s) and prefill (about 50t/s) velocities. Thank you |
* kv cache : batch state restore scatter reads per contiguous run When restoring state into non-contiguous destination cells (e.g. a prompt-cache snapshot into a fragmented ring), state_read_data issued one small copy per KV cell - ~1.4M copies of a few KiB each for a 40k+ token restore, taking 25-63 s on the CUDA backend. The snapshot stores cell rows in cell order, so a maximal run of consecutive destination indices maps to one contiguous block and can be restored with a single copy. Precompute the runs once and use them in all three scatter loops (K, V, transposed V). Byte-identical. The on-device reader copies with a byte cursor when the read and write chunking differs, so the batched reads are safe for it as well. Batching makes equal tensor counts with a different split reachable (save ranges [2,1] vs restore runs [1,2]); the next commit teaches the reader's 1:1 path to fall back to the byte cursor in that case. Verified in a production setup: 1,363,616 copies / 25-63 s -> 224 copies / 221-424 ms for the same restores (42,603 cells, 4 runs). Assisted-by: Claude Code (unsloth/qwen3.8-27b) * context : fall back to the byte cursor when read and write chunking differ the on-device reader copies saved state back with a 1:1 copy by tensor index whenever the write and read sides recorded the same number of tensors, guarded by a per-tensor size assert. equal tensor counts do not imply equal chunking: a state restore may batch its reads per contiguous run of destination cells while the save used per-range reads, so both sides can record two tensors that split the same data differently, and the assert aborts in all builds. compare the per-tensor sizes and only take the 1:1 path when the chunking actually matches, otherwise fall through to the existing byte-cursor copy. both sides enumerate the same logical data in the same order, so the cursor copy is well-defined across tensor boundaries. Assisted-by: Claude Code (unsloth/qwen3.8-27b) * tests : cover state restore scatter reads on host and on-device paths decode the same prefix on two sequences, interleaving the seq 0 cells between the seq 1 cells, so the seq 1 cells are isolated from each other in the kv cache (three cells, two saved ranges). save the seq 1 state, free the interleaved seq 0 cells, and restore: the destination is then non-contiguous (two runs), and the restore-side chunking has the same tensor count as the save-side with a different split, so the scatter path is batched per contiguous run and the on-device reader's byte-cursor fallback is exercised. the restored state is saved again on the host and compared byte for byte with the first save: the blob is serialized in sequence cell order, so the two saves are identical if and only if the scatter restore wrote exactly the same KV content. this documents the byte-identical guarantee of the run-batched scatter reads. one test per io backend: the host (CPU) path and the on-device path. Assisted-by: Claude Code (unsloth/qwen3.8-27b)
Optimize KV cache for non-contiguous cells (agentic use)
Overview
I have been running qwen3.5+ models via Claude Code for many weeks, bottlenecked by some phase after tool results are visible in the harness, but before the PP progresses. Any tool-calling heavy conversation was dominated by this phase.
Config: --kv-unified, --cache-idle-slots (default), --parallel 2.
Performance Analysis
Fix
Commit 4529c66 introduced the slow path, to enable non-contiguous cell support, but throughput remains very slow.
To improve this, we identify contiguous runs in the non-contiguous data, and upload each run as a single read operation.
Tests
The second commit adds test cases for non-contiguous cells (AI-authored). Case 6 tests scattered cells restoring from host, case 7 on device.
Blast radius
Prior to this change, the save/restore API could expect equal read/write counts in
llama-context, which no longer holds. A test case for this is also added.Additional information
From experiments with additional logging:
Notably:
set_data_exttakes 62 seconds. The restore-time correlates strongly with cell-count.In a production setting (Windows, 200k context), transfers usually take from 20 to 60 seconds.
From there on, the bottleneck was identified to the documented "slow path". When this path is hit, potentially millions of
io.read_tensor()calls are being performed individually, resulting in effective memory transfer speeds around ~50 MB/s.Fixes #20854
Discussion
I believe my use case is the common one, yet I don't see this bottleneck talked about much. A harness usually comes with: 1. a main conversation thread, and 2. an auto-classifier for tool call safety evaluation. This is essentially the most basic use case, even if you do not use subagents. Although you can configure a harness with longer timeouts and a single slot, I believe allowing parallel requests makes sense here.
Moreover, unified KV cache also makes sense, since many users will run models just at the edge of what their VRAM allows.
Due to aforementioned at least two conversation threads perpetually occurring, the allocator produces non-contiguous cells in the KV cache. My understanding is that (large, ~2 GB) RAM cached checkpoints need host/device transfers, which is where this bottleneck lies. I want to make the case that this is not a minor optimization for specific use cases, but an important general one.
The commonly user-cited metrics PP and TG do not capture this bottleneck, which I think shadows the importance of this optimization.
Requirements