Skip to content

[Engine] shared KV pool for all lanes (--kv-pool-tokens): at 2 x 256k the first token comes 2-5x sooner and lane moves ~10x faster; off = identical to main - #1614

Open
noon-at-cgn wants to merge 20 commits into
Niko1221:mainfrom
noon-at-cgn:pr/kv-pool-141

Conversation

@noon-at-cgn

@noon-at-cgn noon-at-cgn commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Branch pr/kv-pool-141, head 1e1cd762: 20 commits on upstream main 41cf05a0 (v0.2.5-6, engine 0.1.50). Opt-in: without --kv-pool-tokens the output is byte-identical to main (identity gate below). Scope issue: #3351.

Problem and result

With KV streaming (--kv-resident), the main session and every --batch slot pin the K/V of the whole --max-context in RAM, however short the conversation: 3.09 GiB each at 262,144 cells with --kv int8. A conversation that moves between the main session and a slot is copied, which at 256k takes seconds, and two long lanes push a RAM-limited host to its memory guard.

--kv-pool-tokens N pins N cells once. Sessions hold only the 4,096-cell chunks their conversation has reached, and a move between lanes swaps chunk tables instead of copying.

Two lanes of about 256k tokens (2 x RTX 3080 20 GB, UD-Q4_K_XL, same binary in both arms, only the flag differs; measured on 0.2.0-beta, engine 0.1.45.3):

with the pool without the pool
First token of an appended short question (both lanes) 1.1-1.2 s 2.3-6.7 s
Both lanes decoding an appended turn, aggregate 59.1 and 61.5 tok/s 25.5 and 37.7 tok/s
Moving a ~256k conversation to or from a slot 0.36-0.5 s (moved) 3.5-5.2 s (copied)
Host memory guard (stops the engine below 1.5 GiB MemAvailable) never fired stopped the engine in 2 of 3 starts
Idle MemAvailable (mean of 3 starts each) 14.5 GiB 11.4 GiB

At about 133k per lane the move took 0.1-0.25 s with the pool and 0.9-2.0 s without. With the pool a third lane (three conversations of about 104k) fits, every needle answered.

Short context is unchanged: one stream 84.5 vs 81.9 tok/s, two streams 93.7 vs 89.8 (means of three starts each, within the round-to-round spread). The gains are at long context with more than one lane.

On this PR's head (0.2.5) the pool runs the same workload end to end (2 x 256k, numbers under Validation). I did not repeat the no-pool arm on 0.2.5, so the comparison above is the 0.2.0-beta one.

Related discussion

Approach

A session keeps a chunk table, a device copy for the kernels and a host copy for DMA and copies. Unreached chunks point at one spare "trash" chunk. Without the flag the table is the identity layout. A lane reserves chunks as it writes. When the pool is full, an idle slot's cached conversation gives way first, then a new request gets a retryable 503 (kv_pool_full), and a decoding lane that cannot grow ends with length and "truncated": true. It works across a layer split: each card pins its own layers' share and a conversation uses the same chunks on every card. The MTP drafter ring is not pooled.

  • Every host-side K/V reader and writer goes through the chunk table (KvHostPools::block, block_host, for_each_piece): append kernels, resolve and stage copies, verify, MTP, prefill, state hash, conversation_kv_copy, conversation_kv_restore_prefix, session files, K8V4 halves. I checked the 967 upstream commits since the last base for new host K/V sites and found none.
  • With the pool, the batch slots borrow the main session's RoPE table (64 MiB per slot at 262k). Without it each slot keeps its own, as on main (1e1cd762; see Failed).
  • The checkpoint chain moves with its chunks (own commit), so a client that edits its last question still finds a checkpoint below the edit.
  • The pool's host allocation goes through kv_host_alloc (WSL2 fallback, pin budget).
  • Docs: docs/BATCHING.md, docs/DETAILS.md, docs/MULTI_GPU.md.

Validation

Head 1e1cd762 unless a line says otherwise. ebb61958 is the same series before the last two changes (the RoPE gate, and (void) cudaFree in kv_pool.cpp; that file compiles to identical machine code either way).

Passed:

  • Flag off = main, byte for byte. Main 41cf05a0 vs this head without --kv-pool-tokens, production config otherwise (2 x RTX 3080 20 GB, layer split 23, parallel 2, --spec 4 --batch-mtp, --max-context 262144 --kv int8 --kv-resident). Greedy (temperature 0, top_k 1, seed 1234), 256 tokens, prompts of 989, 31,280 and 128,209 tokens, same bytes for both. The stock config is not reproducible even main against main (adaptive experts), so both arms ran the deterministic settings from docs/DETAILS.md (--adapt-every 100000 --adapt-async 0 --pcie-frac 0) with STRATA_STATE_HASH=1. Two starts of each: all three outputs byte-identical to main's, the STATE_HASH fields equal, the expert cache at 4893 / 4161 slots like main's. At ebb61958 the same held with --spec 2.
  • Pool on, 2 x 256k, end to end (production config with --kv-pool-tokens 524288, one start): KV pool: 524288 cells shared by the sessions and the stages of the layer split, 6.24 GiB of pinned RAM. Two concurrent 256k prompts, then appended turns: 4/4 needles correct, slot moves 444-842 ms, appended-turn decode 53.0 tok/s aggregate, single-lane questions 43.5 and 50.5 tok/s, no kv_pool_full, host MemAvailable never below 17.2 GiB.
  • CUDA build (CUDA 13.3, sm_86, -DSTRATA_ENABLE_CUDA=ON -DSTRATA_NATIVE_EXPERTS=ON -DSTRATA_MMQ_KQUANTS=ON): rc 0, same warning count as main.
  • CPU tests (CUDA_VISIBLE_DEVICES=-1): kv_chunks_test; conversation_cache_test (4791); conversation_memory_test (31); conversation_file_test (201); conversation_validation_test (972); conversation_transfer_test (1548 host-only); session_save_reclaim_test (119); memory_guard_test (134).
  • GPU tests: ctest -j1 at ebb61958: 161 of 168 passed, 4 skipped (CPU without AVX-512, ggml without trellis types), 3 failed, all pre-existing (Failed). At 1e1cd762 the pool-related subset again: 17/17 (conversation_snapshot_test with the pooled variant, kv_chunks, the kv_* parity tests, batch_rows, verify_batch_parity).
  • Serve tests at ebb61958: python -m unittest discover -s serve -t .: 1050 tests, 15 skipped, OK.
  • HIP build at ebb61958 (rocm/dev-ubuntu-24.04:7.2.4, AMD clang 22 / HIP 7.2, flags of packaging/build_hip_linux.sh): strata for gfx1100 and for gfx1151;gfx1201, and all targets with tests on, rc 0 on the branch and on main. The branch adds one test (kv_chunks_test). The only new warning, cudaFree result unused in kv_pool.cpp, is fixed on this head. kv_chunks_test and conversation_cache_test (4791) pass on the host.
  • SYCL build at ebb61958 (oneAPI DPC++ 2026.1.1, flags of packaging/build_sycl_linux.sh): strata and all targets, rc 0, warnings identical to main. The SYCL tree compiles its own migrated copies and does not build the pool: of the touched files only layer.hpp, kv_stream.hpp and conversation_cache.hpp are compiled there, and they build cleanly.

Failed:

  • At ebb61958, flag off was not identical to main: the batch slots borrowed the RoPE table without the pool too, so each slot was 0.07 GiB smaller, --expert-cache auto cached 43 more experts per card, and greedy output differed. With a VRAM reserve that equalised the expert count the outputs matched, which located the cause. 1e1cd762 borrows only with the pool; the gate above is from that head.
  • Pre-existing, also on main: ple_parity (its Q2_0 GGUF is not on this host), ggml_tq_overlay (cmake script "Unknown arguments", same on main), isa_guard_check (iq512x_rows violation, same on main's library). This branch does not touch src/kernels/cpu, cmake or tests/cmake.
  • CI metal-link fails in metal/src/mtl_rt.mm (MTLGPUFamilyMetal4, MTLCompileOptions.mathMode: macOS 26 API, in main since ad9eb890) on the macos-14 runner's SDK. Other PRs on current main fail the same job; this branch does not touch metal/.
  • CI Linux / runtime requirements only at 1e1cd762: 1051 serve tests, 1 failure, test_lifeline.test_the_portable_watcher_ends_the_child_when_the_server_is_killed ("the child outlived a killed server (watcher)"). It depends on how the tests are started: on the lm-server box it fails on main 41cf05a0 too, 5 of 5 runs from an ssh shell (python -m unittest serve.test_lifeline), while the whole serve suite above passed when run from a systemd unit. This branch does not touch the lifeline code or its test.

Not run:

  • HIP and SYCL hardware (no AMD or Intel GPU here); HIP and SYCL builds of 1e1cd762 (the change since ebb61958 is 3 lines of host C++ in generate.cpp, which SYCL does not compile, plus the (void) cast); Metal; Windows and the WSL2 cudaHostRegister path.
  • The full ctest and the serve tests at 1e1cd762 (subset and CPU tests only); the no-pool 256k arm on 0.2.5.
  • Identity of two concurrent lanes: main against main already differs there under the deterministic settings, so there is nothing to compare against.
  • STRATA_SLOT_CKPT_MOVE=1 with the pool; STRATA_SLOT_COPY_DIRECT and borrow end to end; session-file SAVE/RESTORE with a pool end to end (unit tests only); pool-full paths under load; more than 3 lanes; a single 512k conversation.

All raw outputs (prompts, per-request JSON, engine logs, binaries' sha256) are kept and can be attached.

Limits

  • The pool is for long conversations on more than one lane. At short context nothing measurable changes.
  • Measured on one box (2 x RTX 3080 20 GB, one model and quantization). The no-pool 256k timings on 0.2.0-beta are one full run and two partial ones, because the host guard stopped two of three no-pool starts.
  • N is the operator's choice; too small a pool means refused requests or shortened answers. Setup and launch do not know --kv-pool-tokens; pass it in args.
  • The MTP drafter ring is not pooled. CUDA only was run on hardware.

@noon-at-cgn

Copy link
Copy Markdown
Contributor Author

@xdbxdbx this is your #1011 rebased onto 0.1.41. GitHub closed #1011 automatically when main was force-pushed on 2026-10-06, and I could not find a refile, so I rebased it and added what I needed to run it on two cards: the pool across a layer split, K8V4 and session-file support, and one fix for a stale K/V reuse image. Your four commits keep your authorship and are first in the branch. If you would rather refile it yourself or change anything, tell me and I will adjust or hand it over.

@noon-at-cgn
noon-at-cgn marked this pull request as ready for review October 8, 2026 19:45
@noon-at-cgn noon-at-cgn changed the title One shared KV pool for all lanes (--kv-pool-tokens): two 256k-context lanes in one 512k-token pool on two 20 GB cards, KV memory follows the tokens in use One shared KV pool for all lanes (--kv-pool-tokens): two 256k lanes in one 512k pool, 3.1 GiB less pinned RAM at 2 lanes, no speed change Oct 9, 2026
noon-at-cgn added a commit to noon-at-cgn/Strata that referenced this pull request Oct 9, 2026
Niko1221#1637 and the four older ones), 0.1.41 batch-MTP, adaptive-tier and pool measurements
@noon-at-cgn noon-at-cgn changed the title One shared KV pool for all lanes (--kv-pool-tokens): two 256k lanes in one 512k pool, 3.1 GiB less pinned RAM at 2 lanes, no speed change kv: one shared KV pool for all lanes (--kv-pool-tokens), 3.1 GiB less pinned RAM at 2 lanes Oct 9, 2026
@noon-at-cgn

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (v0.1.44.1) and resolved the conflicts with the ported memory guard, batch MTP on a split and the adaptive tier. It is the same 14 commits in the same order; xdbxdbx's four keep their authorship. One extra change inside the existing reuse-image commit: main's new ConversationCache::clear() now drops the reuse identity as well, so a cleared cache cannot hand back stale K/V. I checked main's changes since the old base for new K/V host-addressing sites the pool does not translate and found none (the MTP drafters' rings stay outside the pool, as before). Built at the commits that had conflicts and ran the CPU, GPU parity and serve tests. I have not yet run the pool end to end on this version of main; I will add the result here.

@noon-at-cgn

Copy link
Copy Markdown
Contributor Author

Result of running the pool end to end on this branch, and a fix I added (three commits on top, head 6ea1d2a).

End to end (2 x RTX 3080 20 GB, layer split 23, parallel 2, --max-context 262144 --kv-pool-tokens 524288 --batch-mtp --adapt-async 1 --aux-cpus auto, 90 GiB container, upstream 296b59d + the 14 pool commits): the start log shows the 524,288-cell pool (6.24 GiB pinned), one batch group, two batch-MTP drafters and equal lend slots, with no WARNING or ERR over the run. Speed, one start, same A/A tools as before: solo decode 84.0 tok/s (n=24), two streams 91.3 tok/s (n=20), prompt reads at 25k/51k/104k 2,096 / 2,433 / 2,743 tok/s; the earlier build on 0.1.41 with the pool gave 81.7 and 89.2 tok/s over three starts, so this is equal within restart scatter. Two concurrent ~256k prompts with a needle each: both needles correct in both rounds, no KV pool full, container MemAvailable never below 11.2 GiB. Moving a 133k-token conversation takes 0.11 s (0.34-0.43 s at 256k) against about 1.8 s to give it back without the pool.

A problem I found and the fix. An appended turn (same history, the answer, a new message) reused the cache on both lanes, from 30k to 256k tokens. A prompt with the same prefix but an edited last message reused nothing with the pool on: admitting a conversation into a slot dropped main's periodic checkpoint chain and kept only the last-turn checkpoint, which lies after the edit. The fix has two parts, in separate commits:

  • fdbcf102 (engine): the chain moves with the K/V in both directions. 2 lanes x ~133k, edited last question: 0 / 0 reused before; 118,784 on both lanes after, first token after 24.5 s and 13.3 s instead of 142 s and 53 s. At 256k per lane both lanes reuse 225,792 only with --conversation-cache-mib of at least 4528 (4608 tested). At 4096 one lane reads all 256k again.
  • 64e7f138 (serve): pick_slot treats a prompt that shares at least 3/4 of a slot's held tokens (and at least 1024) as that conversation's next turn. Before, the first request of an edited round took the other lane's slot and destroyed its conversation. This commit also applies with the pool off, and the 3/4 threshold is my judgment; if you would rather have it separate or tuned, I can move it.
  • 6ea1d2a4 (docs): what is reused and what it costs.

Costs and limits: each cached slot keeps up to 6 checkpoints, about 0.7 GB of host RAM per slot. An edit that lies below the first periodic checkpoint reuses nothing (30k tokens: 0 on both lanes; 60k: 31,232). Not measured: speed with the fix against without it, three or more lanes, pool-off at 256k. The CPU, GPU parity and serve tests pass; the C++ change was tested on the engine only in the runs above.

@Niko1221

Copy link
Copy Markdown
Owner

A memory feature with no speed claim (the author measured no change); handed to the cache review.

@Niko1221

Copy link
Copy Markdown
Owner

Thanks for the pull request.

We're resetting the tracker for 0.2.0-beta. There are more than 1,300 open issues and pull requests, too many for us to review properly, so we're closing everything that isn't being worked on right now. Closed here does not mean rejected.

To reopen it takes about two minutes:

  1. Rebase on main if it no longer applies, and re-run your checks on 0.2.0-beta.
  2. Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other].
  3. Fill in the pull request template: the problem and result, and the validation (what you ran, on which revision, what you saw).
  4. Comment /reopen. A check runs and either reopens the pull request or lists what is still missing.

The rules are in CONTRIBUTING.md.

@Niko1221 Niko1221 closed this Oct 19, 2026
@noon-at-cgn noon-at-cgn changed the title kv: one shared KV pool for all lanes (--kv-pool-tokens), 3.1 GiB less pinned RAM at 2 lanes [Engine] shared KV pool for all lanes (--kv-pool-tokens): 256k lane moves 10x faster, 3 GiB less pinned RAM Oct 20, 2026
@noon-at-cgn

Copy link
Copy Markdown
Contributor Author

/reopen

@github-actions

Copy link
Copy Markdown

The check passed, but GitHub would not reopen it (Validation Failed: ). If the branch was deleted, restore it or open a new pull request and link this one.

@noon-at-cgn

Copy link
Copy Markdown
Contributor Author

/reopen

@github-actions github-actions Bot reopened this Oct 20, 2026
@github-actions

Copy link
Copy Markdown

Reopened, thank you. A maintainer will pick it up from here. If it conflicts with main, rebase it first; that speeds up review.

@noon-at-cgn
noon-at-cgn marked this pull request as draft October 20, 2026 04:30
bsorensen110 added a commit to bsorensen110/Strata that referenced this pull request Oct 22, 2026
…PoolFull 503) — integ batch 19 [23 hunks via claude-strata; integ's take_reuse_for/retained_n flow supersedes PR reuse site; copy_to_slot 4-arg; NUMA binding ported into qsa_host_alloc; compile-verify pending]
bsorensen110 added a commit to bsorensen110/Strata that referenced this pull request Oct 22, 2026
- conversation_state.cpp: drop the integ-era checkpoint_validate copy and the duplicated
  snapshot_sources wrapper left side-by-side by the Niko1221#3341/Niko1221#3541 merges; restore the
  checkpoint_payload_sizes helper (Metal-aware) that the dedup slice clipped
- conversation_cache.hpp: re-apply Niko1221#3615's min-tokens parser + READ gate, clobbered by a
  later header-side merge
- generate.cpp: single parked Match (the Niko1221#3584 best_with_partial + Niko1221#3615 read gate wins;
  slot_source=-1 moves after it), carry_chain declared for Niko1221#1614's chain-carry in the
  integ move_cks block, one splice-aware read_from, 'moved' hoisted to the trace's scope
- CMakeLists: the WIN32 ws2_32 link for stage_link.cpp sat inside the HIP-only guard

Verified: cmake --build build-cu130 clean, strata.exe links
xdbxdbx and others added 5 commits October 25, 2026 08:56
With KV streaming (--kv-resident) every session pins the K/V of the
whole context for each QSA layer, so N batch slots plus the main session
pin (N + 1) x the context even when the conversations together hold
little of it. --kv-pool-tokens N pins N cells per QSA layer once; a
session holds only the chunks (4096 cells) its conversation has reached.

- A per-session chunk table (device copy for the kernels, host copy for
  the DMA movers) maps a session's blocks into the pool; unreserved
  entries point at a trash chunk. Without a pool the layout is the
  identity, as before.
- Lanes grow as they write. When the pool is full an idle slot gives its
  K/V back (one holding nothing first, then the cached conversation used
  longest ago); then a new request is refused ("ERR KV pool full", which
  the server answers 503 with Retry-After); a decoding lane that cannot
  grow ends ("length", with "truncated": true in the response).
- After a slot takes a conversation over, the main session gives its
  copy back.
- The engine reports the pool on a POOL line; /metrics shows it as
  live.kv_pool.
- The batch slots borrow the main session's RoPE table (64 MiB each at
  262K).

Single GPU only: refused with a layer split.

Measured, IQ2_XS, RTX 4090 + 64 GB RAM, --resident-experts, 4 slots x
262144 cells, --kv-pool-tokens 524288: the pool pins 6.6 GB (5 x 3.3 GB
without it); 4 lanes of 512 tokens 83 t/s total; two lanes at 134K and
143K tokens deep at once; available RAM never below 15.1 GB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ported from upstream PR Niko1221#1011 by xdbxdbx (commit 66082ec), re-applied onto the current main line.
…ing pool chunks

Admission and a slot giving a conversation back exchange the two sessions'
chunk tables instead of copying the streamed K/V, so the pool never holds a
conversation twice (a 104K-token admission: 52 ms instead of ~1.1 s copied).
The prompt read that gives way to a waiting request (BYIELD) still copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ported from upstream PR Niko1221#1011 by xdbxdbx (commit 28ed67b), re-applied onto the current main line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ported from upstream PR Niko1221#1011 by xdbxdbx (commit 3c3e736), re-applied onto the current main line.
…3 paths

- A drafter streamed whole (its window covers the context) pins its own host
  copy; without one the conversation cache refused every snapshot.
- A chunk-table upload completes before the next launch reads it (the kernels
  run on non-blocking streams).
- pool_reserve gives nothing back when all the idle slots together would not
  cover the request, and a prompt read that gave way (BYIELD) gives way last
  instead of never.
- The pool's refusal is a 503 with Retry-After on non-streamed /v1/responses
  too; in a stream it carries code kv_pool_full (Anthropic: overloaded_error).
- A solo request that ends with finish "pool" ends there; it is not resent to
  a batch slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ported from upstream PR Niko1221#1011 by xdbxdbx (commit 2040789), re-applied onto the current main line.
…k table; a session RESTORE reserves its chunks

Three host-addressing sites that did not exist (or did not stream) when Niko1221#1011 was written:
- kv_hybrid_k_half/kv_hybrid_v_half build a fresh KvHostPools from the arrays only, which dropped the chunk table:
  a pooled --kv k8v4 layer wrote its cells at their session block numbers, into other sessions' chunks.
- conversation_kv_source (the SAVE <path> session file) read the authoritative pools linearly; it walks the chunks now
  (each_piece, as conversation_kv_save/restore/verify do).
- RESTORE <path> restored the file's K/V into the main session before any chunk of it was reserved (everything past
  the reservation went to the trash chunk): it reserves the file's cells first and refuses, retryable, when the
  lanes hold the pool.
configuration the pool is for here, so a lane (the main session or a batch slot) now spans the stages:

- KvChunkMap (kv_chunks.hpp/.cpp): the chunk bookkeeping on its own (free list, per-lane tables, reserve/shrink/swap),
  no CUDA, so it has a CPU test (tests/core/kv_chunks_test.cpp).
- KvPool: init takes every stage's reference session (each stage pins its own QSA layers' arrays, in its GPU's
  context); attach opens a lane with its stage-0 session, attach_stage adds the session of a later stage with a copy of
  the same table in that GPU's memory; detach closes a lane before its sessions go. Reserve/shrink/release/swap act on
  the lane, named by any of its sessions, and upload the table to every stage.
- generate.cpp: the pool is created once every stage's session exists; the slot sessions join their lane per stage;
  pool_move swaps the lane's tables once and copies/resets the indexer and residency of every stage; the slot copies
  skip the streamed layers of every stage (the pool carried them), not only CUDA0's.
…unk tables

- kv_pool_policy.hpp: kv_pool_eviction_plan (lowest rank first, the longest unused, the lower slot; nobody is asked
  when all that can give way together would not cover the request); pool_reserve in generate.cpp acts on its answer.
- KvHostPools::for_each_piece: the walk that cuts a byte range where a chunk ends, moved out of conversation_snapshot.cpp
  so the test can check every byte against the block mapping.
- tests/core/kv_chunks_test.cpp (target kv_chunks_test, no CUDA): KvChunkMap rules and a random walk of
  reserve/shrink/swap that checks every chunk is free or held exactly once, the host mapping, the eviction plan.
…s borrow RoPE only from a stage that has QSA layers

--pipeline-windows launches windows ahead of each other and never reserved chunks per window like the serial loop
does (everything past the prompt's reservation would have gone to the trash chunk): it reserves max-context up front
and decodes serially when the pool cannot back it (it runs without --batch, so no slot has to give way).
…ation it describes

With the shared KV pool, an admission move clears main's live conversation but left the reuse image retained at a
cache restore.  limit_reuse() kept it for the next request that resumed from another slot, and the next park reused
its bytes for cells of a different conversation (silent wrong K/V in a parked entry).  The reuse image now carries the
token ids and image keys of its conversation; take_reuse() hands it back only for the prefix both conversations
share, and the paths that move a conversation out of main drop it.  Test: conversation_cache_test.
… (RAM, speed, restarts; what was not measured)
With --kv-pool-tokens an admission moved the K/V to the slot but left it only the deepest checkpoint (the turn
checkpoint) and emptied the main session's chain; a give-back to the solo path dropped even that one. A later turn
that changed the last question (or re-rendered the reply) has no checkpoint below the change except the periodic ones,
so both lanes of two long conversations read everything again. The chain now travels with the chunks both ways, and
an uncached slot drops its chain.

Measured on 2 lanes x 133k with a changed last question: both lanes reuse 118,784 tokens (time to first token 24.5 and
13.3 s) where the unfixed binary reused 0 (52 and 142 s).
The pool section said a changed last question resumes from a checkpoint but not where that stops. Measured with two
lanes: an edit below the first periodic checkpoint reuses nothing (30k: 0, 60k: 31,232); 133k per lane works with the
default 4096 MiB conversation cache, 256k per lane needs at least 4528 MiB (4608 tested); each cached slot now keeps up
to 6 checkpoints, about 0.7 GB of host RAM per slot.
…the direct copy and the prefix restore through a shared pool's chunk tables

A second streamed state (its chunks interleaved with the first's) shares the host arrays; every K/V format the streamed
test covered runs once more with the host copy in chunks, and checks that a neighbour's writes stay out of a state's chunks.
…e while the pool moves chunks

STRATA_SLOT_CKPT_MOVE (opt-in) lets a request mount a checkpoint from a slot that is still decoding. With --kv-pool-tokens the
slot's chunk table would move to the main session and be released under the slot's feet, so the pool keeps the idle-slot rule.
…s at 104k; what was not run)

Same binary with and without the flag on 2 x RTX 3080: a ~256k conversation
moves in 0.36-0.5 s instead of 3.5-5.2 s copied, 3.2 GiB more idle MemAvailable,
no short-context speed effect. Older 0.1.41 / 0.1.44.1 figures are labelled as
older. States that the binary was built from eeaf7b3 and that the serve layer
had no pick_slot rule.
…kens

Without the flag each slot keeps its own table again, as on main. Borrowing it
saved 0.07 GiB of VRAM per slot, which --expert-cache auto turned into about 43
more cached experts per card, and so into different greedy output than main
with the pool off.
@noon-at-cgn noon-at-cgn changed the title [Engine] shared KV pool for all lanes (--kv-pool-tokens): 256k lane moves 10x faster, 3 GiB less pinned RAM [Engine] shared KV pool for all lanes (--kv-pool-tokens): at 2 x 256k the first token comes 2-5x sooner and lane moves ~10x faster; off = identical to main Oct 25, 2026
@noon-at-cgn
noon-at-cgn marked this pull request as ready for review October 25, 2026 15:55
@noon-at-cgn

Copy link
Copy Markdown
Contributor Author

Rebased on main 41cf05a0 (0.2.5) and marked ready. On the triage note above: the gains are at long context with more than one lane, not short-context speed. At 2 x 256k, the first token of an appended question took 1.1-1.2 s instead of 2.3-6.7 s, appended-turn decode ran at 59-62 instead of 26-38 tok/s, and without the pool the host memory guard stopped the engine in 2 of 3 starts. For the cache review:

  • Flag off: greedy output byte-identical to main at 1k / 32k / 128k (STATE_HASH equal). This gate found one default-path difference (slot RoPE sharing), now limited to the pool.
  • Backends: the HIP and SYCL builds pass.
  • Pool on: 2 x 256k end to end on 0.2.5.

The two red CI jobs (metal-link, test_lifeline watcher) fail on main as well; details are under Failed in the body. Scope questions: #3351.

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.

3 participants