Repository navigation
conversation cache: a shared prefix is copied out of a parked conversation instead of taking it over, so the parked one keeps its history - #1164
Conversation
…th of each K/V buffer in reserve (was up to 16 MiB per buffer, 0.4-0.5 GB per conversation) A conversation that was restored and is parked again keeps its unchanged K/V pages and appends the new ones (92a3d5f). The appended segment reserved room for later turns, growing geometrically up to 16 MiB - per buffer, and a parked image has one per K/V layer and part (k, v, their scales, the indexer rows, the draft layer). With Qwen3.8-Flash-Next (IQ3_S, int8 K/V) a 32K-token main agent parked again took 1.49 GB instead of 0.97 GB, a 17K-token subagent 1.16 GB instead of 0.74 GB. The cache counts that capacity against its budget, so with 2816 MiB the reserve decides which conversations are evicted. The reserve is now at most an eighth of what the buffer held before the append (at least 64 KiB, and at most 16 MiB as before). Small turns still fill the reserve instead of adding a transfer each: the 4096-append test keeps its segment bound, and 256 turns of 1 MiB onto a 16 MiB buffer take 28 segments (17 before). The docs said parking falls back to a full capture when the reserve would evict another conversation; that check went in 6cd29d9. With the reserve bounded the two captures differ by at most an eighth of the K/V, and the docs now say what the reserve is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKZxC8sUotEzdWXws2FTUC
…ne borrows it, the parked conversation stays (sibling subagents no longer cost each other their history) A request whose best match was a checkpoint inside a parked conversation took that whole conversation into the session and overwrote everything after the checkpoint. With subagents that share a system prompt and tool list, a new sibling arriving while another conversation was live (the parent, or a subagent of another kind) therefore destroyed a running sibling's cache: its next turn read its history again from the end of the system prompt. Now such a checkpoint is borrowed: the K/V of the first tokens (main layers and draft layer) comes from the parked image (conversation_snapshot_restore_prefix), the checkpoint is copied and mounted as for any checkpoint resume, and the parked conversation keeps its place. Parking the outgoing conversation pins it; when the outgoing image fits only in its room, the outgoing one keeps that room and the parked one is taken whole as before. A match on a parked conversation's live state or its deepest checkpoint (the same conversation going on, its last reply re-rendered) is taken as before. conversation_cache_test: the borrow/take decision, pinning against slot and byte eviction and Niko1221#342's superseded drop, the refusal before evicting anything. conversation_snapshot_test: a borrowed prefix gives the same K/V bytes and running state as a whole restore rewound to that checkpoint and leaves the image unchanged (needs a GPU). On 0.1.40.1: the draft layer is a pointer in the snapshot functions (a layer split's stages have none, and without --mtp no image has one); the borrowed prefix takes it the same way. A layer split never borrows (its stages restore whole images only), a borrow clears a batch slot as a source like a taken conversation does, and the outgoing conversation is parked for a batch slot's restore as before. With --kv-grow the K/V has grown to this prompt before the borrow, and the borrowed prefix is never longer than the prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKZxC8sUotEzdWXws2FTUC
…upstream PR Niko1221#1164), also in a pool From upstream PR Niko1221#1164 (BlueKingMuch, open): a request whose best match is a checkpoint inside another parked conversation borrows its K/V up to there and a copy of the checkpoint; the parked conversation stays (sibling subagents no longer cost each other their history). POOL: CONV_BORROW (id, n) - every worker takes the same prefix from its part of the image, which stays; the mounted checkpoint's CKPT_RESTORE gives them its running state. A worker that cannot borrow makes the prompt read from the start. pool_link_test covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
Upstream now carries our picks of Niko1221#851 (AVX2 Q8_K quantizer), Niko1221#789 (expert uploads beside the shared expert), Niko1221#813 (--ple-io ram on Windows), Niko1221#1185 (window graphs free older batch layouts), Niko1221#1201/Niko1221#1118 (doorbell per window), Niko1221#1101/Niko1221#1057 (Stager sleep: now the default on Linux only, STRATA_STAGER_SLEEP=1 forces it) and Niko1221#1063 (the shared draft head's type); their versions are kept. Conflicts resolved: - prefill: upstream's own CPU share (STRATA_PREFILL_CPU_SHARE, opt-in) sits beside CPU assist. Its setter is set_cpu_share_pool; when it is asked for, it replaces CPU assist for the chunks it covers, otherwise CPU assist runs as before (on by default). The debug NaN check skips both kinds of CPU rows. - conversation cache: a pool-borrowed entry and upstream's pinned shared prefix (pin=N) are both kept from eviction; the borrow (Niko1221#1164) and pin=N's sibling path do not park at once. - generate: pool batch groups launch only up to their last active slot (Niko1221#1249's G.S), also on the workers; --adapt-async's poll between verdicts runs beside the pool's verified-path-first loop. - server: upstream's chunked bodies (Niko1221#893) and the pool's read-once body are one helper; do_POST's BadBody wrapper around the pool's routed-request check. - native dense: the pool's served_bytes beside upstream's weight_bytes_for (Niko1221#1238). - expert source: the commit-limit retry (Niko1221#1064) beside upstream's paced page-locking (Niko1221#1250). - setup: no ready-made engine (StrataPool compiles its own); REPO names StrataPool for the SHA-256 lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
|
Tested a local port of this + #1163 onto v0.1.40.2 on Windows, Ryzen 9 7940HS / RTX 4070 Laptop 8 GB / 64 GB RAM, IQ3_S, 64K context. Both builds used the same toolchain, 36 GiB resident expert budget, two conversation slots and a 2 GiB cache. Ten turns switched between a parent and two workers with deliberately different inventory answers (422 vs 1422). Every answer was correct:
The final return to worker B read 8,082 tokens on the control versus 28 on the patched build. This is one complete sequence per build, with sampled high-reasoning output; the generated histories differ, so the 15% wall-time reduction is workload evidence rather than a deterministic speed guarantee. Separate pinned-document queries also passed. One integration detail for the rebase: v0.1.40.2 now has permanently pinned prefixes. I preserved both Follow-up with a larger shared system prefix: the earlier 100-record fixture did not reach the default 2,048-token root threshold, so it tested storage/eviction behavior rather than borrowing. I repeated the ten-turn schedule with a 2,833-token common root and checked the engine's actual Both arms reused that root on B's first turn. On returning to A and B later, the control read 6,203 and 6,523 tokens, while the #1163 + #1164 port read only 26 and 27. All distinct-answer checks passed. Across the sequence, newly read tokens fell from 33,731 to 21,058, and prompt processing from 116.85 to 75.36 s. The candidate logged the borrowed restore; the control did not. This is still one sampled sequence per arm and a combined port, not an isolated attribution to #1164. Output totals differed (1,235 vs 725), so the 174.05 vs 108.67 s total wall times also include generation differences. The specific improvement supported by the cache trace is avoiding the two history rereads after borrowing the common root. |
conversation_cache.hpp: make_room evicts neither a conversation that holds a pinned shared prefix (pin=N, new in 0.1.40.2) nor the parked conversation a borrow restores from. generate.cpp: the pin=N sibling check and the borrow's parking stand side by side; a borrow parks the outgoing conversation as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKZxC8sUotEzdWXws2FTUC
…ead of borrowed 0.1.40.2 added pinned shared prefixes (pin=N): a parked conversation that holds one is never evicted, and N queries keep one conversation holding the pin. A borrow copies the donor's checkpoints up to the match, so a match that reached a pinned prefix gave the borrower the pin as well: once parked, a second conversation held it and was never evicted either. Such a match now takes the conversation, as before the borrow. The refusal before evicting anything counts the conversations that hold a pinned prefix along with the donor. When they filled the slots between them, the outgoing conversation found nothing to evict and was not parked; now the donor is taken whole as before and the outgoing conversation keeps its place. The borrow's guard is guard()/unguard()/guard_blocked() instead of pin()/unpin()/pin_blocked(), since "pinned" now means pin=N. conversation_cache_test: a pinned prefix, the donor and an ordinary conversation (only the ordinary one is evicted), the refusal with both in every slot, and matches on and past a pinned prefix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKZxC8sUotEzdWXws2FTUC
|
Thanks @midhatn for porting and testing this, and for repeating it with a root above the 2,048-token threshold: the returns to A and B (26 and 27 tokens read instead of 6,203 and 6,523) are the case #1164 is for. I've updated this PR for 0.1.40.3: a merge of v0.1.40.3 and one commit on top. Your integration point is what the merge does too: parking the outgoing conversation evicts neither a conversation that holds a pinned prefix (pin=N, since 0.1.40.2) nor the conversation a borrow comes from. The commit adds two details:
The borrow's guard is now guard()/unguard() instead of pin()/unpin(), since "pinned" means pin=N. Measured on 0.1.40.3, this against #1163 on 0.1.40.3, with the setup from the description (RTX 4080 SUPER 32 GB, Windows 11, IQ3_S, 262K context,
With this PR every return resumes its conversation (2.8 to 3.0 s), and all ten answers are the same in both runs. With a pinned prefix (a 20,014-token document sent with pin=N, five questions on it, and between them four subagents with a shared 6,018-token system prompt), each request reads as much as on #1163 alone and gives the same answer. The subagents borrow the system prompt from their parked sibling, where #1163 alone takes the sibling whole; the questions never borrow, they resume at the pinned prefix (29 tokens read, 0.4 s). At the end three conversations are parked instead of two, after two evictions (none on #1163 alone); the document was never among them (every later question still resumed at it). Tests on Windows with CUDA: |
|
Thanks for incorporating the pinned-document/donor/ordinary-entry case. I reviewed For clarity, my measurements above are from the earlier local port. I have not rerun your new head, so they should not be counted as validation of the added pin-duplication and full-slot refusal cases. The source and test additions look aligned with those cases; no additional change requested from this review. |
…ne borrows it, the parked conversation stays (#1164) Sibling subagents with the same system prompt and tools no longer cost each other their history: only the K/V up to the checkpoint and the checkpoint itself are copied into the session (conversation_snapshot_restore_prefix); the parked conversation stays where it is and is pinned while the outgoing one parks. A match on a parked conversation's live state or newest checkpoint is that conversation going on and is taken as before. Rebased onto 0.1.44.1 by hand: the pinned shared prefix (#879) and evict_oldest() now also skip the borrowed donor, serials follow every erase, and park-gain (#2265) and the pin= sibling rule gate the outgoing park. Co-authored-by: BlueKingMuch <53972523+BlueKingMuch@users.noreply.github.com>
…ead of borrowed (#1164) A borrow copies the donor's checkpoints up to the match, so a match that reached a pinned shared prefix gave the borrower the pin too. Such a match now takes the conversation as before. The borrow's guard is guard()/unguard()/guard_blocked(), and the refusal before evicting counts the pinned conversations along with the donor. Rebased by hand onto 0.1.44.1 (evict_oldest() honours the guarded donor). Co-authored-by: BlueKingMuch <53972523+BlueKingMuch@users.noreply.github.com>
|
Thank you, this is in for 0.1.45, co-authored with you. It needed a hand rebase because 0.1.42 added pinned shared prefixes with their own eviction rule. On our RTX 3060 (IQ3_XXS) the sibling pattern A, B1, A, B2, A went from 13.35 s to 0.80 s on A's last turn (5 of 5 pairs), same greedy output; the sibling's own turn costs about 0.2 s more. |
Conflicts: upstream took the Turing kernels this fork had cherry-picked (Niko1221#1821 Niko1221#1825 Niko1221#2069-Niko1221#2072 Niko1221#2081 Niko1221#2091 Niko1221#2093), now opt-in with STRATA_SM75_PERF=1: their versions kept (native_mmvq, qsa_select, qsa_prompt_attn, prefill, gemm, kernels). Upstream's sm_75 table replaces ours (STRATA_SM75_TABLE gone; STRATA_MMVQ_IL_ROWS can set rows). conversation_cache.hpp: the disk tier's on_evict on upstream's serial-aware evict_oldest. generate.cpp: the disk tier's read-back before Niko1221#1164's borrow (a disk restore never borrows), --conversation-cache-park-gain beside the disk options. CMakeLists: prefill_mmq_bench kept beside upstream's new tests. Tokenizer: our long-text cache beside max_special_len. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream 0.1.45 took in ten of the PRs we carried: Niko1221#567 (prompt encoder), Niko1221#1164 (sibling borrow), Niko1221#1166, Niko1221#1368, Niko1221#1367, Niko1221#1120 (split overlap), Niko1221#1912, Niko1221#1888, Niko1221#1949, Niko1221#1305. Their versions are kept and our copies dropped: verify.cpp's duplicate prelaunch/handoff block, verify_kernels.hpp, the qsa prompt kernels, gr_upmix_parity, the tokenizer and its tests (tools/test_strata_tokenizer_upstream.py is gone: upstream's test file is the one now), the second quantize_scatter declaration. README credits updated. Pool parts kept on top: - conversation cache: upstream renamed the borrow guard (pin -> guard); clear() resets it. - prefill: the remote_next hand-off beside upstream's hand-off pieces/worker (a pool stage has no pieces: the whole chunk goes to the workers). - generate.cpp: --pool-affinity measure over a node's own layers; the node's dense-range load beside STRATA_HC_REQ8; the carve on primary_device over the pool range; checkpoints keep pool_id (STRATA_CKPT_POOL/THREADS' parallel save is skipped on a pool PC); the workers' retain after eviction and after a resume; the borrow/restore of parked conversations on the workers with upstream's --conversation-cache-park-gain; Niko1221#652's emit count in upstream's merged windows. - expert_source: our budget retry beside upstream's lend budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
…tream c9e571e・2b296987) 同じシステムプロンプトと道具で始まるサブエージェントは、置いた会話を丸ごと取って後ろを上書き していたので、その会話の次のターンは履歴を読み直していた。そこまでの K/V とチェックポイントだけ を写し(conversation_snapshot_restore_prefix)、置いた会話はそのまま残す。最新の状態や最も深い チェックポイント、固定した前置き(pin=N)との一致はその会話の続きなので、これまでどおり取る。 XeStrata ではファイルからの読み(--conversation-save)を先に見て、層の分割では借りない。 B70・IQ3_XXS: 4K の会話 A の 3 ターン目は、間に A の前置きで始まる B を挟んでも 3,690 トークンを 使い直す(前は 3,555 で 170 を読み直した)。
|
Thanks for the pull request. Your change is already in If part of your work did not make it in and you want it considered, open a new pull request on top of |
…est resumes from its own checkpoint With the conversation cache on, every next turn of a chat whose client sends the last reply back without its thinking parked the whole live conversation (1.4-1.6 GB, 280-370 ms on a 52K-token conversation) and dropped that copy as superseded one request later. Opt-in STRATA_PARK_REWIND_MAX=N skips the park when the request resumes from a checkpoint of the session's own conversation and leaves N tokens or fewer behind. Unset, the path is unchanged. Xiaomi2008 8e4ccbe (fork Xiaomi2008/Strata, not an upstream PR), measured there on an RTX 5090: twelve appended turns on a 52K conversation 10.4 s -> 6.3 s of prompt reading. Ported onto 0.1.43.1 with Niko1221#1164's borrow: a borrow never counts as the session's own rewind (it parks the outgoing conversation as Niko1221#1164 does). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZy1yKaDDiA8a7svdwaHio
…h "parallel", the fork's changes and the measured costs docs/BATCHING.md "This fork" and docs/NVFP4.md "Several dialogs at once": the switch cost of dialogs that take turns without and with --conversation-cache-mib (8K / 32K / 100K), why "parallel" was unusable on the 262K config (14.6 GiB of slots), --batch-max-context in the options table, the control-vector window fix, the conversation-cache PRs taken (Niko1221#1503, Niko1221#1163, Niko1221#1164, Niko1221#1959, Xiaomi2008's STRATA_PARK_REWIND_MAX), the checks, one dialog's decode per config, and the recommended setting (the conversation cache; "parallel": 2 with 64K slots when requests really overlap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZy1yKaDDiA8a7svdwaHio
…gs, the rope warning, the final build's checks (81049f34)
What's new and "Upstream 0.1.43.1" add the EOS-literal fix ("stop_token_text"), the parallel-dialog fixes (upstream
PRs Niko1221#1503 Niko1221#1163 Niko1221#1164 Niko1221#1959; link to "Several dialogs at once") and the --max-context warning; the checks name the
release build 81049f34: 2K / 32K logits and tokens = 0.1.42-nvfp4.2's engine, the serve KL texts byte-identical,
ctest 116/119, serve 849, tools 821.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZy1yKaDDiA8a7svdwaHio
…ds, supersede, an index hash, a writer gate, UTF-8 paths The tray stops an engine with taskkill /F /T, so xrip's QUIT-time save (--conversation-disk-persist) never runs there and every parked conversation and the live one died with the process. - --conversation-disk-sync-s N (with --conversation-disk-persist; default 60, 0 = QUIT only): after N s without a request the engine writes every parked conversation that has no copy on the disk (from its RAM image) and the live one (streamed from the device, as SAVE: one GPU, no --batch), on the serving thread, given up within one 16 MiB block when a command arrives (SessionWriteOptions::cancel). A RAM image written this way carries its disk entry (SavedConversation::disk_id): its eviction drops the RAM copy without a second write. QUIT runs the same save, then xrip's park-and-spill for what it could not stream. - take(..., keep): with persist (and for a checkpoint match, Niko1221#1164's borrow) the entry stays on the disk after a read, as the most recently used: it is the copy that survives a crash until a newer one exists. - Supersede: a newly written entry removes the older copies of the same conversation (the RAM cache's Niko1221#342 rule on the keys), only after it is complete; the budget then takes the least recently used, never an entry being read. - An entry with the same keys is not written twice. - The index (format 2) ends with a hash: a torn or edited index is refused at start like a damaged one; file names in it must be bare names of this tier. - The background writer starts a job only while the engine is idle (set_busy), unless RAM or the entry is waited for (wait_idle, take): spills no longer write during a prompt read or decode. Windows: THREAD_PRIORITY_BELOW_NORMAL. - Paths: UTF-8 through std::u8string (std::filesystem reads a std::string in the ANSI code page on Windows). conversation_disk_test: 118 checks (keep, write_now, cancel, supersede, the gate, the index hash, disk_id). Measured (RTX 5090, Windows, NVFP4 GPTQ pack): a 16K sub-agent back from the disk in 0.41 s against 1.75 s read; idle save of 2 conversations 0.5 s; after taskkill /F /T and a restart all 3 dialogs back from the disk in 0.32-0.49 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZy1yKaDDiA8a7svdwaHio
Builds on #1163 (the first commit); this PR is the second.
An agent harness that runs subagents sends several conversations that start with the same system prompt and tool list. The conversation cache checkpoints that prefix, and when a new conversation finds it only inside a parked one (because another conversation is in the session at that moment), it moves that whole parked conversation into the session and overwrites it after the checkpoint. The parked conversation is gone, and its next turn reads its history again.
With this, the new conversation borrows: only the K/V up to the checkpoint and the checkpoint itself come over (
conversation_snapshot_restore_prefix), and the parked conversation stays where it is. A match on a parked conversation's live state or its newest checkpoint is that conversation going on, and it is moved into the session as before (ConversationCache::borrows). While the outgoing conversation is parked, the one borrowed from is pinned, so parking cannot evict it; if the outgoing conversation fits only in its room, it is moved in as before and the outgoing one keeps its place. The engine log says "borrowed".#1090 (prefix snapshots on disk) changes the same decision: a saved prefix wins over a parked conversation that matches no further, so that conversation is not taken out. With this PR it is not taken out either, and nothing is read from disk.
Measured
Same setup as #1163, also on 0.1.40.1 against v0.1.40.1 (a layer split never borrows - its stages restore whole images only - a borrow clears a batch slot as a source, as a taken conversation does, and without
--mtpit takes no draft K/V, as parking saves none): RTX 4080 SUPER 32 GB, Ryzen 7 5800X3D, 96 GB, Windows 11, IQ3_S, 262K context,--conversation-cache-mib 2816 --conversation-cache-slots 3, a fixed expert placement (--adapt-every 1000000 --expert-cache 8031), greedy, 64-token answers. A main agent P (30,000 tokens) and two subagents S1 and S2 with the same system prompt (15,000 each) take turns for two rounds, every return +2,300 tokens: P1 S1 P2 S2 S1b S2b P3 S1c S2c P4. Prompt tokens read, time to first token:With both, every return resumes its conversation (2.8 to 3.0 s). The second eviction is S1 at P4, by 5 MB: three conversations of 0.8 to 1.1 GiB and the one being switched in fill 2816 MiB.
Why it builds on #1163: borrowing keeps one more conversation in the cache, and with the reserve unbounded that pushed the main agent out - this PR alone on v0.1.40.1, same run: P3 read 34,728 tokens again (12.1 s instead of 2.9 s), all ten requests 54.5 s instead of 42.9 s.
The answers: with the fixed placement all ten equal #1163 alone (S2 borrowed gives the same tokens as S2 moved in the old way), and the main agent's four answers equal a run without subagents.
Tests:
conversation_cache_test(which matches borrow and which do not, pinning against slot and byte eviction and the drop of superseded copies, the refusal when the outgoing conversation fits only in the donor's room) andconversation_file_teston Windows;conversation_snapshot_test(on the GPU: the borrowed prefix and its checkpoint give the same bytes as restoring the whole conversation and rewinding to the checkpoint, and the parked image stays unchanged) on Windows with CUDA. The same run without--mtpborrows the same way.