Repository navigation
Conversation
The RAM conversation cache drops a parked conversation when RAM pressure or --conversation-cache-slots evicts it. With --conversation-cache-spill-dir DIR it is written to DIR as an ordinary session file (the same format and model/config identity as the slot save/restore API) plus a small metadata sidecar, so a later request - or a restart - reads it back instead of re-reading the prompt. The spill reuses conversation_file.cpp rather than a second on-disk format: a spilled conversation and a hand-saved session file are interchangeable, and the same identity rules refuse a foreign file. The sidecar holds only the token and image lists, so best() matches a disk conversation without reading its K/V. --conversation-cache-similarity and --conversation-cache-n-min filter weak hits; a disk hit must fit the RAM budget and the min-free floor before it is read back. On a clean QUIT the parked conversations spill before exit. Single-GPU only: with --layer-split the disk tier stays off. conversation_spill_test covers spill/match/load round-trip, similarity/n_min, identity refusal, budget eviction, reopen reindex, and the RAM cache handing its evictions to the tier (make_room callback + spill_all).
The RESTORE path materialized the whole snapshot in host memory, so a deep session could not be restored on a machine that serves it: upstream's own documented 503 'when the RAM to read the file is not there'. The restore is now two passes over the same file, format v1 unchanged: - the read pass parses and validates the whole file (running state, K/V headers, every count against this session's limits, the payload hash) without materializing the K/V; it reports each layer's part sizes. - the apply pass re-reads the file and copies the K/V into the authoritative pools a 16 MiB block at a time. It is bound to the read pass: a file whose layer count or any part size differs is refused before that part is applied, and the payload hash is compared to the trailer at the end. The first applied block is the gate - before it a failure refuses cleanly, after it the engine fail-stops rather than decode from bytes it cannot vouch for. Peak RAM is two 16 MiB buffers and the running-state metadata, so a session of any depth restores under the same floor. Measured on a 198,830-token IQ3_S session: 3.05 GiB restored in 3.4-3.5 s, next turn 99.99% reused. The residency refill after the pool writes is mandatory for the modes that keep K/V in VRAM and runs per layer (kv_stream_reset / kv_ring_restore). (cherry picked from commit d438ca5)
…disk # Conflicts: # include/strata/core/conversation_cache.hpp
…pt: make_room may have evicted the entry the stale index named
…k, no RAM budget A conversation switched away from is streamed from its pools straight into --conversation-cache-spill-dir (the SAVE path's capture: the deepest checkpoint and the running state are copied, the K/V is not) and a returning one is streamed back with the two-pass RESTORE path (Niko1221#1269): the read pass checks the file before anything on the device changes, the apply pass writes the pools 16 MiB at a time. No parked host image and no RAM budget: the disk limit is the only one, so a long conversation is never refused for its size. The new file is written before this conversation's older copies are dropped (drop_superseded gains a keep path), so a failed write loses nothing. The agent's next turn of the live conversation (it resumes at the newest turn checkpoint) writes nothing. Shutdown saves the live conversation, flushed. Single GPU, without --batch or --peer-device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp
…-disk-only, measured on Strix Halo 18 checks: a streamed spill writes the same file as the image spill, matches and reads back with the same K/V, refuses a host K/V image or an empty conversation, leaves no file or index entry when a source fails, and drop_superseded spares the `keep` copy (the rule alone would drop it). 59 checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp
…ge cache Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp
|
"Not tested: CUDA (no NVIDIA card here) " put in my testing queue |
|
routhjim, ANBAL534, pspranger-throw damn guys. leave some for me. Very nice |
|
It works on my CUDA machine. "--conversation-cache-disk-mib", One thing to notice is after remove --conversation-cache-mib flag. the conversation cache section in front end monitor page gone. I ask my agent on Strata fix it. But since I'm just a vibe coder, I'm to ashamed to put is on any PR. I'm more than sure all the contributor can work something better. Below is the AI summery: Problem Changes (3 files, kept as two local commits on our dev branch — happy to PR them separately if useful upstream) Engine (generate.cpp): the INFO status line now reports conversation_cache_disk_only=0/1, so the server knows the mode without re-parsing the run's args. Tests: existing test_monitor suite passes; disk-line counters verified against a replay of a real session log. |
… prefix entry A conversation saved to disk also writes its root - the system prompt and tools, up to the first turn boundary at least --prompt-cache-root tokens in (the engine's root rule) - once per distinct root, as strata-conv-prefix-N: the root's K/V cells and, as live state, the root checkpoint's own running state. A new conversation that opens the same way (a coding agent's next session) resumes from it instead of reading the preamble again. Prefix entries persist: drop_superseded never removes them, the disk budget evicts ordinary conversations first, and a reopen (restart) keeps them as prefixes; has_prefix dedupes. The sidecar format is unchanged (the kind is in the name). conversation_spill_test: 14 new checks, 73 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp
…pin sibling saves the conversation it leaves A request's "strata_prefix" (pin=N) marks where the part its sessions share ends - for a coding agent that runs into the first user message (its <system-reminder> blocks), past the first turn boundary. The prefix entry is the deepest pinned checkpoint on the live path when there is one, the root rule's otherwise. In disk-only mode a request that resumes at a pinned prefix is another conversation, so the one it leaves is saved (the RAM cache's "N queries cost one prefix" skip would drop it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp
…point first
A disk-only read pass that was refused for RAM, storage or I/O went to "discard unusable disk conversation" and
deleted the file, so each refusal lost a conversation. Only a file that fails validation (or is gone) is removed
now; any other refusal keeps it for a later turn ("disk hit skipped", with the MiB asked for and the floor).
The read pass's RAM preflight was sized for --prompt-cache checkpoints (6 by default), capped at the file size; a
disk-only save writes one. The read admits one first; a file the RAM cache spilled into the same directory can
hold more and is read once more under the engine's own limit before it counts as bad. On Strix Halo (~420 MiB
files, 64K context) the preflight asked 260 MiB, about 450 before. Measured: three restores refused under a forced
floor kept all six files, and the next start restored the three.
The INFO line reports conversation_cache_disk_only=0/1 for the monitor.
Reported-by: ed-hch (CUDA test of Niko1221#1480)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp
…k-only The card keyed on --conversation-cache-mib > 0, so in disk-only mode it showed no cache and told the user to add --conversation-cache-mib. ConvCacheLog now reads the disk-only lines (spill dir ready / disabled, disk-saved, prefix-saved, disk-restored, refusals with the last reason, discarded files); with conversation_cache_disk_only=1 the card shows a bar for the spill directory against --conversation-cache-disk-mib and those counters instead of the RAM bars, and its note describes the disk tier, or says the directory did not open. test_monitor: 2 new tests, 12 pass; checked against a live engine (8 saves, 6 restores, 3 prefixes on the card). Suggested by ed-hch, whose local fix covered the same ground; this is a separate implementation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp
|
@ed-hch thank you for running it on CUDA, and for the numbers: a working day of 105-142K-token conversations is more than I could test here. You found two gaps. I put both into this PR (credit to you in the commits): The monitor card. The engine's INFO line now carries The read-pass refusals. Two problems were behind them:
Checked here on Strix Halo:
I can't test CUDA here, so a rerun of your setup would be welcome when you have time. Measured with the help of Claude (Anthropic). |
Thank you for making it perfect but I afraid I couldn't test until next week. Hope other can provide more data point. edit: just one more thing: should a note be added to remind this feature will cause extra write to SSD which might accelerate its ageing? |
…disk # Conflicts: # include/strata/core/conversation_cache.hpp
…ot checkpoint, takes what RAM cannot hold On top of Niko1221#1271 / Niko1221#1269 / Niko1221#1480 (merged unchanged): - an evicted conversation is handed to a writer thread (spill_async) instead of written - and flushed - inside the request that evicted it; the file is indexed, so matched, only once wait() has joined the write (before a disk match, a streamed save and at shutdown). When the physical-RAM admission refuses a park or a disk hit while a write still holds an evicted image, the request waits for that write first. - a spilled file keeps the deepest checkpoint (as SAVE) and the shallowest one (the chain's root - the end of the system prompt a new chat resumes from); the ones between only serve edits further back. - with the RAM cache on, a conversation it cannot park (over its budget, or refused by the RAM admission) is streamed to the directory as --conversation-cache-disk-only writes it, and a disk hit over the RAM budget is streamed back as that mode reads it, instead of both being dropped; put() hands what it evicts to the tier too (it could evict past make_room's estimate without spilling). - --conversation-cache-disk-min-tokens N (default 0): shorter conversations are not written. - every spill write leaves --session-min-free-mib free on the disk. conversation_spill_test: 34 new checks (93 pass, also under ThreadSanitizer); the other conversation tests pass. End to end on an RTX 2080 Ti (docs/DETAILS.md): later turns 5.20 -> 4.51 s median with a 4 GiB RAM cache, and 14.83 -> 5.01 s with 256 MiB (0 -> 12 of 12 turns resumed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CMakeLists.txt # src/program/generate.cpp
Status line keeps both conversation_cache_min_tokens (Niko1221#1271) and conversation_cache_disk_only. The disk-only save/read admission now reads available_host_bytes() (upstream daa7655 replaced conversation_available_memory()).
|
Hi! We're on the same layer (our PR #2510), same Your Proposal: converge the spill file on the v2 (backward-compatible) and keep the disk-only mode as a policy flag on top of it. Could we coordinate on that, crediting you and #1271 for the v1 base and the disk-only idea? |
|
i love this community |
|
The disk-only mode is the part of this stack ours does not have, and by your own numbers, routhjim, it is the Heads-up, so nobody is surprised when the maintainer picks one: our branches add the same three new paths One piece of ours that may be useful to you: we keep a separate, lower RAM floor for anything already on disk Either route serves the goal; the merge order is the maintainer's call. |
|
Thank you. See #1090; we have no Strix Halo box now to measure this. |
|
Frequent 1–3K-token side conversations can fill the disk budget and evict valuable long sessions. In if (disk_only) {
if (!disk_continuing && !conversations.wants((int64_t) live.size()))
return true;
if (!disk_continuing) disk_save_live(false, "switch");
return true;
}
if (conversation_spill.enabled() && disk_only) {
if (conversations.wants((int64_t) live.size()))
disk_save_live(true, "shutdown");
}No read-side gate and no gate on At threshold 10000 (the production value was later changed to 6000), the 2053-token session was skipped (disk files 7→7), while the 11653-token session was saved (7→8; 394 MiB in 593.9 ms). CPU checks passed: 78 spill, 4772 cache, 223 file. Could this be incorporated into #1480? I can send a small PR against your branch if preferred. This fork includes adaptations, so I’m not implying this commit cleanly cherry-picks onto upstream. |
|
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:
The rules are in CONTRIBUTING.md. |
…n disk with no RAM budget (Niko1221#1480)
Stacked on #1271 (spill directory) and #1269 (streaming restore). The branch is #1271 (at 4af468f, which brings in v0.1.41) with #1269 (at d258f18) merged in. Only the last eight commits are new:
I will rebase once those two land, or fold this into #1271 if its author prefers.
#1271 keeps the conversations the RAM cache evicts on disk. A conversation reaches disk only through a RAM eviction (
make_room), so the RAM budget still decides what is kept. On a machine where the expert cache takes nearly all memory (Strix Halo: 72 GiB of experts in 128 GB), the conversation cache needs that budget most and can afford it least.--conversation-cache-disk-only, with--conversation-cache-spill-dir, keeps the conversation cache on disk only:conversation_snapshot_sources+ the streamingsession_file_write). No host image is made.--conversation-cache-mibis not used.--conversation-cache-disk-mibis the only limit, so no conversation is refused for its size. A read or save still checks free RAM for its transient buffers (one checkpoint's running state and two 16 MiB blocks) against--conversation-cache-min-free-mib. When that refuses, the file is kept for a later turn.drop_supersededgains akeeppath, because its rule alone would drop the new copy too. A failed write loses nothing.--batchor--peer-device; otherwise the flag says so and is off.Where it helps. A coding agent run as several sessions on one local engine: Claude Code through an Anthropic-compatible proxy, or a manager such as Happier that starts sessions from a phone. Each session is a long conversation (20-120K tokens), and they alternate. With one GPU and no RAM to spare, every switch read the whole conversation again. One real Claude Code session of 121K tokens took 181 s to read and 2.0 s to restore from disk. A new session started from a 13K-token preamble that is the same every time, and read all of it.
Prefix entries. When a conversation is saved, its shared opening is also saved on its own as
strata-conv-prefix-N, once per distinct opening. The opening is the deepest"strata_prefix"checkpoint on the live path when the request pinned one. Otherwise it is the root by the engine's rule: the first turn boundary at least--prompt-cache-roottokens in. A new conversation that opens the same way resumes from it.has_prefixdedupes them.Measured with real Claude Code sessions. The client marks
"strata_prefix"after Claude Code's leading<system-reminder>blocks, so the system prompt, tools and reminders form the shared part.The rest of each first turn is the user's own message and a per-session block, so it has to be read.
Why this matters beyond the budget. With #1271 as it is, a conversation larger than the free RAM budget is neither parked nor spilled:
If its disk copy was restored a turn earlier, that copy was already removed as superseded, so the conversation is gone, and its next turn reads 15,954 tokens from the start (18.9 s, in the run below). The long conversations are the ones that gain the most from a disk tier. Disk-only mode never asks the RAM budget.
Measured on a Ryzen AI Max+ 395 (Radeon 8060S, gfx1151, 128 GB, Linux 7.2, ROCm 7.14.1, the iGPU alone).
docs/STRIX_HALO.md.--mmap-experts --expert-cache auto --spec 4 --mtp mtp/rt --lookup-chain 3 --kv int8 --max-context 65536. Spill directory on the internal NVMe./metricstotals around each request.--conversation-cache-mib 2048 --conversation-cache-slots 1--conversation-cache-disk-onlyfsync, thenposix_fadvise(DONTNEED);fincoreshowed 0 bytes resident each time). Restores took 275-361 ms (read pass 128-162 ms), and all 12 later turns resumed with the same 88-94% reuse.RssAnon, sampled every 0.1 s) peaked 0.6 GiB above a run without the cache during a switch, and ended 0.15 GiB above it. No conversation stays in RAM.Tests.
conversation_spill_test: 32 new checks, 73 pass.test_monitor: 2 new tests, 12 pass.drop_supersededspares thekeepcopy, while the rule alone would drop it.drop_superseded, evicted last, deduped, and still prefixes after a reopen.The engine paths were run end to end as above.
Not tested:
--kv-grow: the restore callskvg_ensureas the RAM restore does, but I did not run it with elastic K/V.Measured with the help of Claude (Anthropic).
🤖 Generated with Claude Code
https://claude.ai/code/session_013XBY17SSRKGmDXc2YFsrmp