Skip to content

kv-cells: resolve get_prev_tokens in O(log n) from the sequence position index - #28040

Merged
ServeurpersoCom merged 1 commit into
ggml-org:masterfrom
ServeurpersoCom:kvcells-prev-tokens-index
Sep 1, 2026
Merged

kv-cells: resolve get_prev_tokens in O(log n) from the sequence position index#28040
ServeurpersoCom merged 1 commit into
ggml-org:masterfrom
ServeurpersoCom:kvcells-prev-tokens-index

Conversation

@ServeurpersoCom

@ServeurpersoCom ServeurpersoCom commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Overview

get_prev_tokens was scanning every used cell of the cache once per ubatch to recover the few tokens preceding it. llama_kv_cells already keeps a per sequence map of positions, so it only needs the cell index next to the position to answer the same question with an upper_bound.

Putting the cell index in the key of a set rather than next to a refcount also removes the reason the counter existed in the first place, since a position occurring twice for the same sequence is now two distinct entries.

This stays inside llama_kv_cells as suggested in #28011, and llama-kv-cache.cpp loses the hash map, the M-RoPE fallback array and the descending walk, so the nearest cell at or before a position now resolves gaps through the same path as everything else.

Model side, needle retrieval at 55k context is unchanged, and 12 sequences with -np 4 greedy on a fixed seed produce byte identical output to master.

RTX PRO 6000, Qwen3.8-Flash-Next UD-Q4_K_XL, fa on, warm runs, alternated A/B in both orders:

55k context generation 74.5 -> 77.6 t/s
132k context generation 52.0 -> 56.7 t/s

Additional information

Follow up to #28011
Alternative to #27992

Requirements

@ServeurpersoCom

ServeurpersoCom commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Net effect on the tree is -62 lines: the hash map, the M-RoPE fallback array and the descending walk in llama-kv-cache.cpp all become unnecessary once the index answers the question directly.

I need to check the mtmd to be sure.

-> Multimodal is the case the removed fallback existed for, so it was checked explicitly: single image and two images of different sizes, greedy, byte identical output to master, with the position gaps showing up in the logs as expected.

@ServeurpersoCom
ServeurpersoCom requested a review from CISC August 30, 2026 17:18
@pwilkin
pwilkin requested a review from ngxson August 30, 2026 18:13
@ggerganov ggerganov self-assigned this Aug 30, 2026
@ServeurpersoCom

ServeurpersoCom commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

I check prefill performance, I think it has an impact.

On a long-running llama-server, prefill throughput on a resumed conversation shows much lower than a full prefill on the same content after a restart. Possibly a reporting artefact of the cache reuse path rather than an actual slowdown, needs a proper look. Nothing to do with this PR.

Edit:

OK I found my unrelated problem. I must use this on my router for this model :

; Performance critical: without these two, the 27 GiB n-gram table stays mmap'd on the SSD
; and prefill collapses from 2400 to 1000 t/s once another model has evicted the page cache
lazy-mode = off               ; Read the n-gram table up front (default: auto, lazy above 4 GiB)
ot = per_layer_token_embd=CPU ; Required with lazy-mode off, 27 GiB does not fit in VRAM

get_prev_tokens() rebuilt a (seq, pos) -> token hash map on every
ubatch by walking all used cells, while llama_kv_cells already keeps
an ordered index of the positions of each sequence in seq_pos, updated
on every cell mutation to serve seq_pos_min() and seq_pos_max().

The index now stores (pos, cell) pairs in a std::set instead of a
position -> count map, so a repeated position (cache reuse via rm + add,
vision inputs with shared positions) yields distinct entries and the
removal of a cell erases its own pair. The new seq_pos_tok_le() returns
the token of the cell at the largest position <= p in logarithmic time,
which is exactly what the old window lookup and its M-RoPE gap fallback
computed together.

get_prev_tokens() shrinks to a direct lookup per (token, offset) and
for_each_token_in() goes away with its only caller. The kv-cache keeps
no n-gram logic of its own.

Measured on Qwen3.8-Flash-Next UD-Q4_K_XL at 71k context, alternating
two binaries with the first run discarded: tg 69.3 -> 72.7 t/s (+4.9%),
pp unchanged at ~2720 t/s, greedy output identical, needle retrieved.
@ServeurpersoCom
ServeurpersoCom force-pushed the kvcells-prev-tokens-index branch from e68fafc to db997ff Compare September 1, 2026 12:03
@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

Rebased for a quick test

@ServeurpersoCom

ServeurpersoCom commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on the latest master and rechecked out of caution, same binaries on both sides except libllama, three alternated pairs per context with the first run discarded. RTX PRO 6000, Qwen3.8-Flash-Next UD-Q4_K_XL, fa on, needle prompt, greedy output byte identical to master with the needle retrieved.

context pair generation, master generation, this PR prefill, master prefill, this PR
55k 1 73.9 76.7 2546 2518
55k 2 73.8 76.8 2476 2496
55k 3 73.8 76.6 2466 2463
55k mean 73.8 76.7 (+3.9%) 2496 2492
132k 1 50.3 56.4 1854 1856
132k 2 50.4 56.4 1854 1856
132k 3 50.4 56.4 1854 1856
132k mean 50.4 56.4 (+12.0%) 1854 1856

The runs sit within 0.1 t/s of each other, so the gap is well clear of the noise. It grows with context, which is what replacing the scan with a lookup predicts, and prefill stays where it was.

Worth noting this comes on top of #28011, which already took generation from 56.3 to 74.3 t/s at 55k and from 33.6 to 50.9 t/s at 132k: the early exit removed most of the scan, and the index removes what was left of it.

@ggerganov ggerganov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks OK to me. Though this reminds me we should add exhaustive unit tests for llama_kv_cells soon.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants