Repository navigation
sched : copy the ranges of a multi-stream window, not the span they sit in - #77
Merged
GenerelSchwerz merged 2 commits intoSep 8, 2026
Conversation
…it in
A split input that is a window over a KV cache split into streams is one
range per stream: the ranges sit a whole cache apart and the cells
between one range and the next are never read by the graph.
ggml_backend_tensor_copy moves ggml_nbytes(), which is the span, so the
ordered copy moved every gap as well - 4.3x the bytes at 2 streams, 3.1x
at 4, 1.8x at 8.
Deliver the ranges instead, with the 2D copy the backends already have.
Blocking as the copy it replaces was blocking. A unified cache and a
single sequence are one flat range and take the same path they did.
Generation t/s, llama-batched-bench -npp 2048 -ntg 128, -c 32768,
-nkvo --kv-cpu-pinned, RTX 4070, Qwen3.8-27B-UD-IQ2_M:
-no-kvu -kvu
-npl 2 34.12 -> 51.73 51.51 -> 51.71
-npl 4 49.51 -> 71.71 71.91 -> 71.98
-npl 8 70.91 -> 84.31 83.42 -> 83.51
A cache split into streams reads the same cells as a unified one, and
the gap between the two columns was the gaps being copied; it closes.
llama-parallel, 8 seeded concurrent sequences over a non-unified cache
at --temp 0, transcript unchanged: 17f946c340db110b before and after.
Assisted-by: Claude Opus 5
Owner
|
Automated preliminary review by OpenAI Codex, for head Blocking: this changes the ordered scheduler copy path but adds no focused regression test. The existing |
test_ordered_multi_stream_ranges builds the window in the shape attention hands the scheduler - permuted, rows on dimension 1, streams on 3 - fills the host store with a long-period pattern and the split backend's buffer with a sentinel, and drives the ordered copy path. It then asserts that exactly the live ranges moved, that each arrives whole from its own source offset, and that the cells between one range and the next still hold the sentinel. On the parent commit it aborts on the first of those: the copy moves 704 bytes where the graph reads 512. The dummy backend gets an opt-in real_memory flag for it, so the buffers it hands out are backed by memory a test can look at. Off by default, so the existing tests keep the addresses they assert on. Assisted-by: Claude Opus 5
GenerelSchwerz
merged commit Sep 8, 2026
11f8c73
into
GenerelSchwerz:llama/dev
7 of 24 checks passed
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A split input that is a window over a KV cache split into streams is one range per stream: the ranges sit a whole
kv_sizeapart and the cells between one range and the next are never read by the graph.ggml_backend_tensor_copymovesggml_nbytes(), which is the span the ranges are cut from, so the ordered copy moved every gap along with them.At
-c 32768, a 2,048-token window per sequence:-nplThis copies the ranges instead, with the 2D copy the backends already have (
ggml_backend_tensor_set_2d_async, already public and already implemented by CUDA). It stays blocking, exactly as the copy it replaces was blocking. A unified cache and a single sequence are one flat range and keep the path they had.Measurements
RTX 4070 (sm_89), driver 610.57.04 / CUDA 13.3, i5-13400F,
Qwen3.8-27B-UD-IQ2_M.gguf,-ngl 99 -sm none -mg 0 -t 3 -fa on -ctk q8_0 -ctv q8_0 -b 512 -ub 512 -c 32768, host residency-nkvo --kv-cpu-pinned --recurrent-state-offload, everything undertaskset -c 0,2,4.llama-batched-bench -npp 2048 -ntg 128, generation t/s, one process per cell:-npl-no-kvubefore-no-kvuafter-kvubefore-kvuafterA cache split into streams reads the same cells as a unified one, and the whole gap between those two columns was the gaps being copied. It closes: after this, the two caches measure the same, and the predicted ratios above track the measured gains.
A unified cache is unaffected to within 0.4%, which is what a change that only touches multi-range windows should look like.
Validation
Byte-identical output.
llama-parallel, 8 concurrent sequences of 16 over a non-unified cache at--temp 0, which seeds its client schedule so the batches repeat run to run. Transcript hash17f946c340db110bbefore and after. Its clients ask different questions, which is what makes it a gate: with one prompt shared by every sequence the streams hold the same bytes and a cross-stream read is invisible.test_ordered_multi_stream_ranges, added here. It builds the window in the shape attention hands the scheduler (permuted, rows on dimension 1, streams on 3), fills the host store with a long-period pattern and the split backend's buffer with a sentinel, drives this copy path, and then asserts three things: exactlyn_stream * usedbytes moved, each stream's range arrived whole from its own source offset, and the cells between one range and the next still hold the sentinel. On the parent commit it aborts on the first of those - the copy moves 704 bytes where the graph reads 512 - and it passes here.The dummy backend gets an opt-in
real_memoryflag for it, so the buffers it hands out are backed by memory a test can look at. It is off by default, so the existing tests keep the synthetic addresses they assert on, and all 24 of them still pass.test-allocpasses in Release against a CUDA build, 25/25.git diff --checkclean.Notes
n_kv, and attention indexes the copy through its ownne/nb.🤖 Generated with Claude Code
https://claude.ai/code/session_01JycLdWs6KRgizbdnNAfZqM